diff options
Diffstat (limited to '')
-rw-r--r-- | sys_linux.c | 1001 |
1 files changed, 1001 insertions, 0 deletions
diff --git a/sys_linux.c b/sys_linux.c new file mode 100644 index 0000000..0b24445 --- /dev/null +++ b/sys_linux.c @@ -0,0 +1,1001 @@ +/* + chronyd/chronyc - Programs for keeping computer clocks accurate. + + ********************************************************************** + * Copyright (C) Richard P. Curnow 1997-2003 + * Copyright (C) John G. Hasler 2009 + * Copyright (C) Miroslav Lichvar 2009-2012, 2014-2018 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of version 2 of the GNU General Public License as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + ********************************************************************** + + ======================================================================= + + This is the module specific to the Linux operating system. + + */ + +#include "config.h" + +#include "sysincl.h" + +#include <sys/utsname.h> + +#if defined(FEAT_PHC) || defined(HAVE_LINUX_TIMESTAMPING) +#include <linux/ptp_clock.h> +#endif + +#ifdef FEAT_SCFILTER +#include <sys/prctl.h> +#include <seccomp.h> +#include <termios.h> +#ifdef FEAT_PPS +#include <linux/pps.h> +#endif +#ifdef FEAT_RTC +#include <linux/rtc.h> +#endif +#ifdef HAVE_LINUX_TIMESTAMPING +#include <linux/sockios.h> +#endif +#endif + +#ifdef FEAT_PRIVDROP +#include <sys/prctl.h> +#include <sys/capability.h> +#endif + +#include "sys_linux.h" +#include "sys_timex.h" +#include "conf.h" +#include "local.h" +#include "logging.h" +#include "privops.h" +#include "util.h" + +/* Frequency scale to convert from ppm to the timex freq */ +#define FREQ_SCALE (double)(1 << 16) + +/* Definitions used if missed in the system headers */ +#ifndef ADJ_SETOFFSET +#define ADJ_SETOFFSET 0x0100 /* add 'time' to current time */ +#endif +#ifndef ADJ_NANO +#define ADJ_NANO 0x2000 /* select nanosecond resolution */ +#endif + +/* This is the uncompensated system tick value */ +static int nominal_tick; + +/* Current tick value */ +static int current_delta_tick; + +/* The maximum amount by which 'tick' can be biased away from 'nominal_tick' + (sys_adjtimex() in the kernel bounds this to 10%) */ +static int max_tick_bias; + +/* The kernel USER_HZ constant */ +static int hz; +static double dhz; /* And dbl prec version of same for arithmetic */ + +/* Flag indicating whether adjtimex() can step the clock */ +static int have_setoffset; + +/* The assumed rate at which the effective frequency and tick values are + updated in the kernel */ +static int tick_update_hz; + +/* ================================================== */ + +inline static long +our_round(double x) +{ + long y; + + if (x > 0.0) + y = x + 0.5; + else + y = x - 0.5; + + return y; +} + +/* ================================================== */ +/* Positive means currently fast of true time, i.e. jump backwards */ + +static int +apply_step_offset(double offset) +{ + struct timex txc; + + txc.modes = ADJ_SETOFFSET | ADJ_NANO; + txc.time.tv_sec = -offset; + txc.time.tv_usec = 1.0e9 * (-offset - txc.time.tv_sec); + if (txc.time.tv_usec < 0) { + txc.time.tv_sec--; + txc.time.tv_usec += 1000000000; + } + + if (SYS_Timex_Adjust(&txc, 1) < 0) + return 0; + + return 1; +} + +/* ================================================== */ +/* This call sets the Linux kernel frequency to a given value in parts + per million relative to the nominal running frequency. Nominal is taken to + be tick=10000, freq=0 (for a USER_HZ==100 system, other values otherwise). + The convention is that this is called with a positive argument if the local + clock runs fast when uncompensated. */ + +static double +set_frequency(double freq_ppm) +{ + struct timex txc; + long required_tick; + double required_freq; + int required_delta_tick; + + required_delta_tick = our_round(freq_ppm / dhz); + + /* Older kernels (pre-2.6.18) don't apply the frequency offset exactly as + set by adjtimex() and a scaling constant (that depends on the internal + kernel HZ constant) would be needed to compensate for the error. Because + chronyd is closed loop it doesn't matter much if we don't scale the + required frequency, but we want to prevent thrashing between two states + when the system's frequency error is close to a multiple of USER_HZ. With + USER_HZ <= 250, the maximum frequency adjustment of 500 ppm overlaps at + least two ticks and we can stick to the current tick if it's next to the + required tick. */ + if (hz <= 250 && (required_delta_tick + 1 == current_delta_tick || + required_delta_tick - 1 == current_delta_tick)) { + required_delta_tick = current_delta_tick; + } + + required_freq = -(freq_ppm - dhz * required_delta_tick); + required_tick = nominal_tick - required_delta_tick; + + txc.modes = ADJ_TICK | ADJ_FREQUENCY; + txc.freq = required_freq * FREQ_SCALE; + txc.tick = required_tick; + + SYS_Timex_Adjust(&txc, 0); + + current_delta_tick = required_delta_tick; + + return dhz * current_delta_tick - txc.freq / FREQ_SCALE; +} + +/* ================================================== */ +/* Read the ppm frequency from the kernel */ + +static double +read_frequency(void) +{ + struct timex txc; + + txc.modes = 0; + + SYS_Timex_Adjust(&txc, 0); + + current_delta_tick = nominal_tick - txc.tick; + + return dhz * current_delta_tick - txc.freq / FREQ_SCALE; +} + +/* ================================================== */ + +/* Estimate the value of USER_HZ given the value of txc.tick that chronyd finds when + * it starts. The only credible values are 100 (Linux/x86) or powers of 2. + * Also, the bounds checking inside the kernel's adjtimex system call enforces + * a +/- 10% movement of tick away from the nominal value 1e6/USER_HZ. */ + +static int +guess_hz(void) +{ + struct timex txc; + int i, tick, tick_lo, tick_hi, ihz; + double tick_nominal; + + txc.modes = 0; + SYS_Timex_Adjust(&txc, 0); + tick = txc.tick; + + /* Pick off the hz=100 case first */ + if (tick >= 9000 && tick <= 11000) { + return 100; + } + + for (i=4; i<16; i++) { /* surely 16 .. 32768 is a wide enough range? */ + ihz = 1 << i; + tick_nominal = 1.0e6 / (double) ihz; + tick_lo = (int)(0.5 + tick_nominal*2.0/3.0); + tick_hi = (int)(0.5 + tick_nominal*4.0/3.0); + + if (tick_lo < tick && tick <= tick_hi) { + return ihz; + } + } + + /* oh dear. doomed. */ + LOG_FATAL("Can't determine hz from tick %d", tick); + + return 0; +} + +/* ================================================== */ + +static int +get_hz(void) +{ +#ifdef _SC_CLK_TCK + int hz; + + if ((hz = sysconf(_SC_CLK_TCK)) < 1) + return 0; + + return hz; +#else + return 0; +#endif +} + +/* ================================================== */ + +static int +kernelvercmp(int major1, int minor1, int patch1, + int major2, int minor2, int patch2) +{ + if (major1 != major2) + return major1 - major2; + if (minor1 != minor2) + return minor1 - minor2; + return patch1 - patch2; +} + +/* ================================================== */ + +static void +get_kernel_version(int *major, int *minor, int *patch) +{ + struct utsname uts; + + if (uname(&uts) < 0) + LOG_FATAL("uname() failed"); + + *patch = 0; + if (sscanf(uts.release, "%d.%d.%d", major, minor, patch) < 2) + LOG_FATAL("Could not parse kernel version"); +} + +/* ================================================== */ + +/* Compute the scaling to use on any frequency we set, according to + the vintage of the Linux kernel being used. */ + +static void +get_version_specific_details(void) +{ + int major, minor, patch; + + hz = get_hz(); + + if (!hz) + hz = guess_hz(); + + dhz = (double) hz; + nominal_tick = (1000000L + (hz/2))/hz; /* Mirror declaration in kernel */ + max_tick_bias = nominal_tick / 10; + + /* In modern kernels the frequency of the clock is updated immediately in the + adjtimex() system call. Assume a maximum delay of 10 microseconds. */ + tick_update_hz = 100000; + + get_kernel_version(&major, &minor, &patch); + DEBUG_LOG("Linux kernel major=%d minor=%d patch=%d", major, minor, patch); + + if (kernelvercmp(major, minor, patch, 2, 2, 0) < 0) { + LOG_FATAL("Kernel version not supported, sorry."); + } + + if (kernelvercmp(major, minor, patch, 2, 6, 27) >= 0 && + kernelvercmp(major, minor, patch, 2, 6, 33) < 0) { + /* In tickless kernels before 2.6.33 the frequency is updated in + a half-second interval */ + tick_update_hz = 2; + } else if (kernelvercmp(major, minor, patch, 4, 19, 0) < 0) { + /* In kernels before 4.19 the frequency is updated only on internal ticks + (CONFIG_HZ). As their rate cannot be reliably detected from the user + space, and it may not even be constant (CONFIG_NO_HZ - aka tickless), + assume the lowest commonly used constant rate */ + tick_update_hz = 100; + } + + /* ADJ_SETOFFSET support */ + if (kernelvercmp(major, minor, patch, 2, 6, 39) < 0) { + have_setoffset = 0; + } else { + have_setoffset = 1; + } + + DEBUG_LOG("hz=%d nominal_tick=%d max_tick_bias=%d tick_update_hz=%d", + hz, nominal_tick, max_tick_bias, tick_update_hz); +} + +/* ================================================== */ + +static void +reset_adjtime_offset(void) +{ + struct timex txc; + + /* Reset adjtime() offset */ + txc.modes = ADJ_OFFSET_SINGLESHOT; + txc.offset = 0; + + SYS_Timex_Adjust(&txc, 0); +} + +/* ================================================== */ + +static int +test_step_offset(void) +{ + struct timex txc; + + /* Zero maxerror and check it's reset to a maximum after ADJ_SETOFFSET. + This seems to be the only way how to verify that the kernel really + supports the ADJ_SETOFFSET mode as it doesn't return an error on unknown + mode. */ + + txc.modes = MOD_MAXERROR; + txc.maxerror = 0; + + if (SYS_Timex_Adjust(&txc, 1) < 0 || txc.maxerror != 0) + return 0; + + txc.modes = ADJ_SETOFFSET | ADJ_NANO; + txc.time.tv_sec = 0; + txc.time.tv_usec = 0; + + if (SYS_Timex_Adjust(&txc, 1) < 0 || txc.maxerror < 100000) + return 0; + + return 1; +} + +/* ================================================== */ + +static void +report_time_adjust_blockers(void) +{ +#if defined(FEAT_PRIVDROP) && defined(CAP_IS_SUPPORTED) + if (CAP_IS_SUPPORTED(CAP_SYS_TIME) && cap_get_bound(CAP_SYS_TIME)) + return; + LOG(LOGS_WARN, "CAP_SYS_TIME not present"); +#endif +} + +/* ================================================== */ +/* Initialisation code for this module */ + +void +SYS_Linux_Initialise(void) +{ + get_version_specific_details(); + + report_time_adjust_blockers(); + + reset_adjtime_offset(); + + if (have_setoffset && !test_step_offset()) { + LOG(LOGS_INFO, "adjtimex() doesn't support ADJ_SETOFFSET"); + have_setoffset = 0; + } + + SYS_Timex_InitialiseWithFunctions(1.0e6 * max_tick_bias / nominal_tick, + 1.0 / tick_update_hz, + read_frequency, set_frequency, + have_setoffset ? apply_step_offset : NULL, + 0.0, 0.0, NULL, NULL); +} + +/* ================================================== */ +/* Finalisation code for this module */ + +void +SYS_Linux_Finalise(void) +{ + SYS_Timex_Finalise(); +} + +/* ================================================== */ + +#ifdef FEAT_PRIVDROP +void +SYS_Linux_DropRoot(uid_t uid, gid_t gid, SYS_ProcessContext context, int clock_control) +{ + char cap_text[256]; + cap_t cap; + + if (prctl(PR_SET_KEEPCAPS, 1)) { + LOG_FATAL("prctl() failed"); + } + + UTI_DropRoot(uid, gid); + + /* Keep CAP_NET_BIND_SERVICE if the NTP server sockets may need to be bound + to a privileged port. + Keep CAP_NET_RAW if an NTP socket may need to be bound to a device on + kernels before 5.7. + Keep CAP_SYS_TIME if the clock control is enabled. */ + if (snprintf(cap_text, sizeof (cap_text), "%s %s %s", + (CNF_GetNTPPort() > 0 && CNF_GetNTPPort() < 1024) ? + "cap_net_bind_service=ep" : "", + (CNF_GetBindNtpInterface() || CNF_GetBindAcquisitionInterface()) && + !SYS_Linux_CheckKernelVersion(5, 7) ? "cap_net_raw=ep" : "", + clock_control ? "cap_sys_time=ep" : "") >= sizeof (cap_text)) + assert(0); + + /* Helpers don't need any capabilities */ + if (context != SYS_MAIN_PROCESS) + cap_text[0] = '\0'; + + if ((cap = cap_from_text(cap_text)) == NULL) { + LOG_FATAL("cap_from_text() failed"); + } + + if (cap_set_proc(cap)) { + LOG_FATAL("cap_set_proc() failed"); + } + + cap_free(cap); +} +#endif + +/* ================================================== */ + +#ifdef FEAT_SCFILTER +static +void check_seccomp_applicability(void) +{ + int mail_enabled; + double mail_threshold; + char *mail_user; + + CNF_GetMailOnChange(&mail_enabled, &mail_threshold, &mail_user); + if (mail_enabled) + LOG_FATAL("mailonchange directive cannot be used with -F enabled"); +} + +/* ================================================== */ + +void +SYS_Linux_EnableSystemCallFilter(int level, SYS_ProcessContext context) +{ + const int syscalls[] = { + /* Clock */ + SCMP_SYS(adjtimex), + SCMP_SYS(clock_adjtime), +#ifdef __NR_clock_adjtime64 + SCMP_SYS(clock_adjtime64), +#endif + SCMP_SYS(clock_gettime), +#ifdef __NR_clock_gettime64 + SCMP_SYS(clock_gettime64), +#endif + SCMP_SYS(gettimeofday), + SCMP_SYS(settimeofday), + SCMP_SYS(time), + + /* Process */ + SCMP_SYS(clone), + SCMP_SYS(exit), + SCMP_SYS(exit_group), + SCMP_SYS(getpid), + SCMP_SYS(getrlimit), + SCMP_SYS(getuid), + SCMP_SYS(rt_sigaction), + SCMP_SYS(rt_sigreturn), + SCMP_SYS(rt_sigprocmask), + SCMP_SYS(set_tid_address), + SCMP_SYS(sigreturn), + SCMP_SYS(wait4), + SCMP_SYS(waitpid), + + /* Memory */ + SCMP_SYS(brk), + SCMP_SYS(madvise), + SCMP_SYS(mmap), + SCMP_SYS(mmap2), + SCMP_SYS(mprotect), + SCMP_SYS(mremap), + SCMP_SYS(munmap), + SCMP_SYS(shmdt), + + /* Filesystem */ + SCMP_SYS(_llseek), + SCMP_SYS(access), + SCMP_SYS(chmod), + SCMP_SYS(chown), + SCMP_SYS(chown32), + SCMP_SYS(faccessat), + SCMP_SYS(fchmodat), + SCMP_SYS(fchownat), + SCMP_SYS(fstat), + SCMP_SYS(fstat64), + SCMP_SYS(getdents), + SCMP_SYS(getdents64), + SCMP_SYS(lseek), + SCMP_SYS(lstat), + SCMP_SYS(lstat64), + SCMP_SYS(newfstatat), + SCMP_SYS(readlink), + SCMP_SYS(readlinkat), + SCMP_SYS(rename), + SCMP_SYS(renameat), + SCMP_SYS(renameat2), + SCMP_SYS(stat), + SCMP_SYS(stat64), + SCMP_SYS(statfs), + SCMP_SYS(statfs64), + SCMP_SYS(unlink), + SCMP_SYS(unlinkat), + + /* Socket */ + SCMP_SYS(accept), + SCMP_SYS(bind), + SCMP_SYS(connect), + SCMP_SYS(getsockname), + SCMP_SYS(getsockopt), + SCMP_SYS(recv), + SCMP_SYS(recvfrom), + SCMP_SYS(recvmmsg), +#ifdef __NR_recvmmsg_time64 + SCMP_SYS(recvmmsg_time64), +#endif + SCMP_SYS(recvmsg), + SCMP_SYS(send), + SCMP_SYS(sendmmsg), + SCMP_SYS(sendmsg), + SCMP_SYS(sendto), + SCMP_SYS(shutdown), + /* TODO: check socketcall arguments */ + SCMP_SYS(socketcall), + + /* General I/O */ + SCMP_SYS(_newselect), + SCMP_SYS(close), + SCMP_SYS(open), + SCMP_SYS(openat), + SCMP_SYS(pipe), + SCMP_SYS(pipe2), + SCMP_SYS(poll), + SCMP_SYS(ppoll), +#ifdef __NR_ppoll_time64 + SCMP_SYS(ppoll_time64), +#endif + SCMP_SYS(pselect6), +#ifdef __NR_pselect6_time64 + SCMP_SYS(pselect6_time64), +#endif + SCMP_SYS(read), + SCMP_SYS(futex), +#ifdef __NR_futex_time64 + SCMP_SYS(futex_time64), +#endif + SCMP_SYS(select), + SCMP_SYS(set_robust_list), + SCMP_SYS(write), + + /* Miscellaneous */ + SCMP_SYS(getrandom), + SCMP_SYS(sysinfo), + SCMP_SYS(uname), + }; + + const int socket_domains[] = { + AF_NETLINK, AF_UNIX, AF_INET, +#ifdef FEAT_IPV6 + AF_INET6, +#endif + }; + + const static int socket_options[][2] = { + { SOL_IP, IP_PKTINFO }, { SOL_IP, IP_FREEBIND }, +#ifdef FEAT_IPV6 + { SOL_IPV6, IPV6_V6ONLY }, { SOL_IPV6, IPV6_RECVPKTINFO }, +#endif + { SOL_SOCKET, SO_BROADCAST }, { SOL_SOCKET, SO_REUSEADDR }, +#ifdef SO_REUSEPORT + { SOL_SOCKET, SO_REUSEPORT }, +#endif + { SOL_SOCKET, SO_TIMESTAMP }, { SOL_SOCKET, SO_TIMESTAMPNS }, +#ifdef HAVE_LINUX_TIMESTAMPING + { SOL_SOCKET, SO_SELECT_ERR_QUEUE }, { SOL_SOCKET, SO_TIMESTAMPING }, +#endif + }; + + const static int fcntls[] = { F_GETFD, F_SETFD, F_GETFL, F_SETFL }; + + const static unsigned long ioctls[] = { + FIONREAD, TCGETS, +#if defined(FEAT_PHC) || defined(HAVE_LINUX_TIMESTAMPING) + PTP_EXTTS_REQUEST, PTP_SYS_OFFSET, +#ifdef PTP_PIN_SETFUNC + PTP_PIN_SETFUNC, +#endif +#ifdef PTP_SYS_OFFSET_EXTENDED + PTP_SYS_OFFSET_EXTENDED, +#endif +#ifdef PTP_SYS_OFFSET_PRECISE + PTP_SYS_OFFSET_PRECISE, +#endif +#endif +#ifdef FEAT_PPS + PPS_FETCH, +#endif +#ifdef FEAT_RTC + RTC_RD_TIME, RTC_SET_TIME, RTC_UIE_ON, RTC_UIE_OFF, +#endif +#ifdef HAVE_LINUX_TIMESTAMPING + SIOCETHTOOL, +#endif + }; + + scmp_filter_ctx *ctx; + int i; + + if (context == SYS_MAIN_PROCESS) { + /* Check if the chronyd configuration is supported */ + check_seccomp_applicability(); + + /* Start the helper process, which will run without any seccomp filter. It + will be used for getaddrinfo(), for which it's difficult to maintain a + list of required system calls (with glibc it depends on what NSS modules + are installed and enabled on the system). */ + PRV_StartHelper(); + } + + ctx = seccomp_init(level > 0 ? SCMP_ACT_KILL : SCMP_ACT_TRAP); + if (ctx == NULL) + LOG_FATAL("Failed to initialize seccomp"); + + /* Add system calls that are always allowed */ + for (i = 0; i < (sizeof (syscalls) / sizeof (*syscalls)); i++) { + if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, syscalls[i], 0) < 0) + goto add_failed; + } + + if (context == SYS_MAIN_PROCESS) { + /* Allow opening sockets in selected domains */ + for (i = 0; i < sizeof (socket_domains) / sizeof (*socket_domains); i++) { + if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(socket), 1, + SCMP_A0(SCMP_CMP_EQ, socket_domains[i])) < 0) + goto add_failed; + } + + /* Allow selected socket options */ + for (i = 0; i < sizeof (socket_options) / sizeof (*socket_options); i++) { + if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(setsockopt), 3, + SCMP_A1(SCMP_CMP_EQ, socket_options[i][0]), + SCMP_A2(SCMP_CMP_EQ, socket_options[i][1]), + SCMP_A4(SCMP_CMP_LE, sizeof (int))) < 0) + goto add_failed; + } + + /* Allow selected fcntl calls */ + for (i = 0; i < sizeof (fcntls) / sizeof (*fcntls); i++) { + if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fcntl), 1, + SCMP_A1(SCMP_CMP_EQ, fcntls[i])) < 0 || + seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(fcntl64), 1, + SCMP_A1(SCMP_CMP_EQ, fcntls[i])) < 0) + goto add_failed; + } + + /* Allow selected ioctls */ + for (i = 0; i < sizeof (ioctls) / sizeof (*ioctls); i++) { + if (seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(ioctl), 1, + SCMP_A1(SCMP_CMP_EQ, ioctls[i])) < 0) + goto add_failed; + } + } + + if (seccomp_load(ctx) < 0) + LOG_FATAL("Failed to load seccomp rules"); + + LOG(context == SYS_MAIN_PROCESS ? LOGS_INFO : LOGS_DEBUG, "Loaded seccomp filter"); + seccomp_release(ctx); + return; + +add_failed: + LOG_FATAL("Failed to add seccomp rules"); +} +#endif + +/* ================================================== */ + +int +SYS_Linux_CheckKernelVersion(int req_major, int req_minor) +{ + int major, minor, patch; + + get_kernel_version(&major, &minor, &patch); + + return kernelvercmp(req_major, req_minor, 0, major, minor, patch) <= 0; +} + +/* ================================================== */ + +#if defined(FEAT_PHC) || defined(HAVE_LINUX_TIMESTAMPING) + +#define PHC_READINGS 10 + +static int +process_phc_readings(struct timespec ts[][3], int n, double precision, + struct timespec *phc_ts, struct timespec *sys_ts, double *err) +{ + double min_delay = 0.0, delays[PTP_MAX_SAMPLES], phc_sum, sys_sum, sys_prec; + int i, combined; + + if (n > PTP_MAX_SAMPLES) + return 0; + + for (i = 0; i < n; i++) { + delays[i] = UTI_DiffTimespecsToDouble(&ts[i][2], &ts[i][0]); + + if (delays[i] < 0.0) { + /* Step in the middle of a PHC reading? */ + DEBUG_LOG("Bad PTP_SYS_OFFSET sample delay=%e", delays[i]); + return 0; + } + + if (!i || delays[i] < min_delay) + min_delay = delays[i]; + } + + sys_prec = LCL_GetSysPrecisionAsQuantum(); + + /* Combine best readings */ + for (i = combined = 0, phc_sum = sys_sum = 0.0; i < n; i++) { + if (delays[i] > min_delay + MAX(sys_prec, precision)) + continue; + + phc_sum += UTI_DiffTimespecsToDouble(&ts[i][1], &ts[0][1]); + sys_sum += UTI_DiffTimespecsToDouble(&ts[i][0], &ts[0][0]) + delays[i] / 2.0; + combined++; + } + + assert(combined); + + UTI_AddDoubleToTimespec(&ts[0][1], phc_sum / combined, phc_ts); + UTI_AddDoubleToTimespec(&ts[0][0], sys_sum / combined, sys_ts); + *err = MAX(min_delay / 2.0, precision); + + return 1; +} + +/* ================================================== */ + +static int +get_phc_sample(int phc_fd, double precision, struct timespec *phc_ts, + struct timespec *sys_ts, double *err) +{ + struct timespec ts[PHC_READINGS][3]; + struct ptp_sys_offset sys_off; + int i; + + /* Silence valgrind */ + memset(&sys_off, 0, sizeof (sys_off)); + + sys_off.n_samples = PHC_READINGS; + + if (ioctl(phc_fd, PTP_SYS_OFFSET, &sys_off)) { + DEBUG_LOG("ioctl(%s) failed : %s", "PTP_SYS_OFFSET", strerror(errno)); + return 0; + } + + for (i = 0; i < PHC_READINGS; i++) { + ts[i][0].tv_sec = sys_off.ts[i * 2].sec; + ts[i][0].tv_nsec = sys_off.ts[i * 2].nsec; + ts[i][1].tv_sec = sys_off.ts[i * 2 + 1].sec; + ts[i][1].tv_nsec = sys_off.ts[i * 2 + 1].nsec; + ts[i][2].tv_sec = sys_off.ts[i * 2 + 2].sec; + ts[i][2].tv_nsec = sys_off.ts[i * 2 + 2].nsec; + } + + return process_phc_readings(ts, PHC_READINGS, precision, phc_ts, sys_ts, err); +} + +/* ================================================== */ + +static int +get_extended_phc_sample(int phc_fd, double precision, struct timespec *phc_ts, + struct timespec *sys_ts, double *err) +{ +#ifdef PTP_SYS_OFFSET_EXTENDED + struct timespec ts[PHC_READINGS][3]; + struct ptp_sys_offset_extended sys_off; + int i; + + /* Silence valgrind */ + memset(&sys_off, 0, sizeof (sys_off)); + + sys_off.n_samples = PHC_READINGS; + + if (ioctl(phc_fd, PTP_SYS_OFFSET_EXTENDED, &sys_off)) { + DEBUG_LOG("ioctl(%s) failed : %s", "PTP_SYS_OFFSET_EXTENDED", strerror(errno)); + return 0; + } + + for (i = 0; i < PHC_READINGS; i++) { + ts[i][0].tv_sec = sys_off.ts[i][0].sec; + ts[i][0].tv_nsec = sys_off.ts[i][0].nsec; + ts[i][1].tv_sec = sys_off.ts[i][1].sec; + ts[i][1].tv_nsec = sys_off.ts[i][1].nsec; + ts[i][2].tv_sec = sys_off.ts[i][2].sec; + ts[i][2].tv_nsec = sys_off.ts[i][2].nsec; + } + + return process_phc_readings(ts, PHC_READINGS, precision, phc_ts, sys_ts, err); +#else + return 0; +#endif +} + +/* ================================================== */ + +static int +get_precise_phc_sample(int phc_fd, double precision, struct timespec *phc_ts, + struct timespec *sys_ts, double *err) +{ +#ifdef PTP_SYS_OFFSET_PRECISE + struct ptp_sys_offset_precise sys_off; + + /* Silence valgrind */ + memset(&sys_off, 0, sizeof (sys_off)); + + if (ioctl(phc_fd, PTP_SYS_OFFSET_PRECISE, &sys_off)) { + DEBUG_LOG("ioctl(%s) failed : %s", "PTP_SYS_OFFSET_PRECISE", + strerror(errno)); + return 0; + } + + phc_ts->tv_sec = sys_off.device.sec; + phc_ts->tv_nsec = sys_off.device.nsec; + sys_ts->tv_sec = sys_off.sys_realtime.sec; + sys_ts->tv_nsec = sys_off.sys_realtime.nsec; + *err = MAX(LCL_GetSysPrecisionAsQuantum(), precision); + + return 1; +#else + return 0; +#endif +} + +/* ================================================== */ + +int +SYS_Linux_OpenPHC(const char *path, int phc_index) +{ + struct ptp_clock_caps caps; + char phc_path[64]; + int phc_fd; + + if (!path) { + if (snprintf(phc_path, sizeof (phc_path), "/dev/ptp%d", phc_index) >= sizeof (phc_path)) + return -1; + path = phc_path; + } + + phc_fd = open(path, O_RDONLY); + if (phc_fd < 0) { + LOG(LOGS_ERR, "Could not open %s : %s", path, strerror(errno)); + return -1; + } + + /* Make sure it is a PHC */ + if (ioctl(phc_fd, PTP_CLOCK_GETCAPS, &caps)) { + LOG(LOGS_ERR, "ioctl(%s) failed : %s", "PTP_CLOCK_GETCAPS", strerror(errno)); + close(phc_fd); + return -1; + } + + UTI_FdSetCloexec(phc_fd); + + return phc_fd; +} + +/* ================================================== */ + +int +SYS_Linux_GetPHCSample(int fd, int nocrossts, double precision, int *reading_mode, + struct timespec *phc_ts, struct timespec *sys_ts, double *err) +{ + if ((*reading_mode == 2 || !*reading_mode) && !nocrossts && + get_precise_phc_sample(fd, precision, phc_ts, sys_ts, err)) { + *reading_mode = 2; + return 1; + } else if ((*reading_mode == 3 || !*reading_mode) && + get_extended_phc_sample(fd, precision, phc_ts, sys_ts, err)) { + *reading_mode = 3; + return 1; + } else if ((*reading_mode == 1 || !*reading_mode) && + get_phc_sample(fd, precision, phc_ts, sys_ts, err)) { + *reading_mode = 1; + return 1; + } + return 0; +} + +/* ================================================== */ + +int +SYS_Linux_SetPHCExtTimestamping(int fd, int pin, int channel, + int rising, int falling, int enable) +{ + struct ptp_extts_request extts_req; +#ifdef PTP_PIN_SETFUNC + struct ptp_pin_desc pin_desc; + + memset(&pin_desc, 0, sizeof (pin_desc)); + pin_desc.index = pin; + pin_desc.func = enable ? PTP_PF_EXTTS : PTP_PF_NONE; + pin_desc.chan = channel; + + if (ioctl(fd, PTP_PIN_SETFUNC, &pin_desc)) { + DEBUG_LOG("ioctl(%s) failed : %s", "PTP_PIN_SETFUNC", strerror(errno)); + return 0; + } +#else + DEBUG_LOG("Missing PTP_PIN_SETFUNC"); + return 0; +#endif + + memset(&extts_req, 0, sizeof (extts_req)); + extts_req.index = channel; + extts_req.flags = (enable ? PTP_ENABLE_FEATURE : 0) | + (rising ? PTP_RISING_EDGE : 0) | + (falling ? PTP_FALLING_EDGE : 0); + + if (ioctl(fd, PTP_EXTTS_REQUEST, &extts_req)) { + DEBUG_LOG("ioctl(%s) failed : %s", "PTP_EXTTS_REQUEST", strerror(errno)); + return 0; + } + + return 1; +} + +/* ================================================== */ + +int +SYS_Linux_ReadPHCExtTimestamp(int fd, struct timespec *phc_ts, int *channel) +{ + struct ptp_extts_event extts_event; + + if (read(fd, &extts_event, sizeof (extts_event)) != sizeof (extts_event)) { + DEBUG_LOG("Could not read PHC extts event"); + return 0; + } + + phc_ts->tv_sec = extts_event.t.sec; + phc_ts->tv_nsec = extts_event.t.nsec; + *channel = extts_event.index; + + return 1; +} + +#endif |