From: Juergen Perlinger Date: Mon, 7 Mar 2022 06:54:01 +0000 (+0100) Subject: [Bug 3757] Improve handling of Linux-PPS in NTPD X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8f9ee7753849eabb4d9049a7ae0216faa42334f5;p=thirdparty%2Fntp.git [Bug 3757] Improve handling of Linux-PPS in NTPD bk: 6225ac09yK5G6Ve4ohnjnnYIIwdpEA --- diff --git a/ChangeLog b/ChangeLog index bed65246a..73b3d8cc1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,6 @@ --- * [Bug 3741] 4.2.8p15 can't build with glibc 2.34 +* [Bug 3757] Improve handling of Linux-PPS in NTPD --- (4.2.8p15) 2020/06/23 Released by Harlan Stenn diff --git a/configure.ac b/configure.ac index 5dc6aee02..f2892997c 100644 --- a/configure.ac +++ b/configure.ac @@ -4295,6 +4295,26 @@ case "$ntp_signd_path" in ;; esac +dnl check for 'magic pps' for Linux +AC_MSG_CHECKING([if we want 'magic' PPS support]) +AC_ARG_ENABLE( + [magicpps], + [AS_HELP_STRING( + [--enable-magicpps], + [+ try to auto-instantiate PPS devices on Linux] + )], + [ans=$enableval], + [ans=yes] +) +AC_MSG_RESULT([$ans]) +case "$ans" in + yes) + AC_DEFINE([ENABLE_MAGICPPS], [1], + [auto-instantiate missing PPS devices on Linux]) + AC_CHECK_FUNCS([openat fdopendir fstatat]) + ;; +esac + AC_CHECK_HEADERS([libscf.h]) LSCF= case "$ac_cv_header_libscf_h" in diff --git a/include/ntp_refclock.h b/include/ntp_refclock.h index e74e77aa7..7f88d9586 100644 --- a/include/ntp_refclock.h +++ b/include/ntp_refclock.h @@ -250,6 +250,12 @@ extern int refclock_ppsaugment( const struct refclock_atom*, l_fp *rcvtime , double rcvfudge, double ppsfudge); +#ifdef _WIN32 +#define ppsdev_open(ttyfd, ppspath, mode, flags) (ttyfd) +#else +extern int ppsdev_open(int ttyfd, const char *ppspath, + int mode, int flags); +#endif #endif /* REFCLOCK */ #endif /* NTP_REFCLOCK_H */ diff --git a/ntpd/Makefile.am b/ntpd/Makefile.am index b977d5e21..9b993c0fc 100644 --- a/ntpd/Makefile.am +++ b/ntpd/Makefile.am @@ -242,6 +242,7 @@ libntpd_a_SOURCES = \ ntp_monitor.c \ ntp_peer.c \ ntp_proto.c \ + ntp_ppsdev.c \ ntp_refclock.c \ ntp_request.c \ ntp_restrict.c \ diff --git a/ntpd/ntp_ppsdev.c b/ntpd/ntp_ppsdev.c new file mode 100644 index 000000000..17d6db8ec --- /dev/null +++ b/ntpd/ntp_ppsdev.c @@ -0,0 +1,384 @@ +/* + * ntp_ppsdev.c - PPS-device support + * + * Written by Juergen Perlinger (perlinger@ntp.org) for the NTP project. + * The contents of 'html/copyright.html' apply. + * --------------------------------------------------------------------- + * Helper code to work around (or with) a Linux 'specialty': PPS devices + * are created via attaching the PPS line discipline to a TTY. This + * creates new pps devices, and the PPS API is *not* available through + * the original TTY fd. + * + * Findig the PPS device associated with a TTY is possible but needs + * quite a bit of file system traversal & lookup in the 'sysfs' tree. + * + * The code below does the job for kernel versions 4 & 5, and will + * probably work for older and newer kernels, too... and in any case, if + * the device or symlink to the PPS device with the given name exists, + * it will take precedence anyway. + * --------------------------------------------------------------------- + */ +#ifdef __linux__ +# define _GNU_SOURCE +#endif + +#include "config.h" +#include "ntpd.h" + +/* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- */ +#if defined(__linux__) && defined(HAVE_OPENAT) && defined(HAVE_FDOPENDIR) +#define WITH_PPSDEV_MATCH +/* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +typedef int BOOL; +#ifndef TRUE +# define TRUE 1 +#endif +#ifndef FALSE +# define FALSE 0 +#endif + +static const int OModeF = O_CLOEXEC|O_RDONLY|O_NOCTTY; +static const int OModeD = O_CLOEXEC|O_RDONLY|O_DIRECTORY; + +/* ------------------------------------------------------------------ */ +/* extended directory stream + */ +typedef struct { + int dfd; /* file descriptor for dir for 'openat()' */ + DIR *dir; /* directory stream for iteration */ +} XDIR; + +static void +xdirClose( + XDIR *pxdir) +{ + if (NULL != pxdir->dir) + closedir(pxdir->dir); /* closes the internal FD, too! */ + else if (-1 != pxdir->dfd) + close(pxdir->dfd); /* otherwise _we_ have to do it */ + pxdir->dfd = -1; + pxdir->dir = NULL; +} + +static BOOL +xdirOpenAt( + XDIR *pxdir, + int fdo , + const char *path ) +{ + /* Officially, the directory stream owns the file discriptor it + * received via 'fdopendir()'. But for the purpose of 'openat()' + * it's ok to keep the value around -- even if we should do + * _absolutely_nothing_ with it apart from using it as a path + * reference! + */ + pxdir->dir = NULL; + if (-1 == (pxdir->dfd = openat(fdo, path, OModeD))) + goto fail; + if (NULL == (pxdir->dir = fdopendir(pxdir->dfd))) + goto fail; + return TRUE; + + fail: + xdirClose(pxdir); + return FALSE; +} + +/* -------------------------------------------------------------------- + * read content of a file (with a size limit) into piece of allocated + * memory and trim any trailing whitespace. + */ +static char* +readFileAt( + int rfd , + const char *path) +{ + struct stat sb; + char *ret = NULL, *tmp; + ssize_t rdlen; + int dfd; + + if (-1 == (dfd = openat(rfd, path, OModeF)) || -1 == fstat(dfd, &sb)) + goto fail; + if ((sb.st_size > 0x2000) || (NULL == (tmp = malloc(sb.st_size + 1)))) + goto fail; + if (1 > (rdlen = read(dfd, tmp, sb.st_size))) + goto fail; + while (rdlen > 0 && tmp[rdlen - 1] <= ' ') + --rdlen; + tmp[rdlen] = '\0'; + ret = tmp; + fail: + if (-1 != dfd) + close(dfd); + return ret; +} + +/* -------------------------------------------------------------------- + * Scan the "/dev" directory for a device with a given major and minor + * device id. Return the path if found. + */ +static char* +findDevByDevId( + dev_t rdev) +{ + struct stat sb; + struct dirent *dent; + XDIR xdir; + char *name = NULL; + + if (xdirOpenAt(&xdir, AT_FDCWD, "/dev")) { + while (!name && (dent = readdir(xdir.dir))) { + if (-1 == fstatat(xdir.dfd, dent->d_name, + &sb, AT_SYMLINK_NOFOLLOW)) + continue; + if (!S_ISCHR(sb.st_mode)) + continue; + if (sb.st_rdev == rdev) { + if (-1 == asprintf(&name, "/dev/%s", dent->d_name)) + name = NULL; + } + } + xdirClose(&xdir); + } + return name; +} + +/* -------------------------------------------------------------------- + * Get the mofor:minor device id for a character device file descriptor + */ +static BOOL +getCharDevId( + int fd , + dev_t *out, + struct stat *psb) +{ + BOOL rc = FALSE; + struct stat sb; + + if (NULL == psb) + psb = &sb; + if (-1 != fstat(fd, psb)) { + rc = S_ISCHR(psb->st_mode); + if (rc) + *out = psb->st_rdev; + else + errno = EINVAL; + } + return rc; +} + +/* -------------------------------------------------------------------- + * given the dir-fd of a pps instance dir in the linux sysfs tree, get + * the device IDs for the PPS device and the associated TTY. + */ +static BOOL +getPpsTuple( + int fdDir, + dev_t *pTty, + dev_t *pPps) +{ + BOOL rc = FALSE; + unsigned long dmaj, dmin; + struct stat sb; + char *bufp, *endp, *scan; + + if (NULL == (bufp = readFileAt(fdDir, "path"))) + goto fail; + if ((-1 == stat(bufp, &sb)) || !S_ISCHR(sb.st_mode)) + goto fail; + *pTty = sb.st_rdev; + free(bufp); + + if (NULL == (bufp = readFileAt(fdDir, "dev"))) + goto fail; + dmaj = strtoul((scan = bufp), &endp, 10); + if ((endp == scan) || (*endp != ':') || (dmaj >= 256)) + goto fail; + dmin = strtoul((scan = endp + 1), &endp, 10); + if ((endp == scan) || (*endp >= ' ') || (dmin >= 256)) + goto fail; + *pPps = makedev((unsigned int)dmaj, (unsigned int)dmin); + rc = TRUE; + + fail: + free(bufp); + return rc; +} + +/* -------------------------------------------------------------------- + * for a given (TTY) device id, lookup the corresponding PPS device id + * by processing the contents of the kernel sysfs tree. + * Returns false if no such PS device can be found; otherwise set the + * ouput parameter to the PPS dev id and return true... + */ +static BOOL +findPpsDevId( + dev_t ttyId , + dev_t *pPpsId) +{ + BOOL found = FALSE; + XDIR ClsDir; + + if (xdirOpenAt(&ClsDir, AT_FDCWD, "/sys/class/pps")) { + struct dirent *dent; + dev_t othId, ppsId; + + while (!found && (dent = readdir(ClsDir.dir))) { + int fdDevDir; + if (strncmp("pps", dent->d_name, 3)) + continue; + if (-1 == (fdDevDir = openat( + ClsDir.dfd, dent->d_name, OModeD))) + continue; + found = getPpsTuple(fdDevDir, &othId, &ppsId) + && (ttyId == othId); + close(fdDevDir); + } + if (found) + *pPpsId = ppsId; + xdirClose(&ClsDir); + } + return found; +} + +/* -------------------------------------------------------------------- + * Return the path to a PPS device related to tghe TT fd given. The + * function might even try to instantiate such a PPS device when + * running es effective root. Returns NULL if no PPS device can be + * established; otherwise it is a 'malloc()'ed area that should be + * 'free()'d after use. + */ +static char* +findMatchingPpsDev( + int fdtty) +{ + struct stat sb; + dev_t ttyId, ppsId; + int fdpps, ldisc = N_PPS; + char *dpath = NULL; + + /* Without the device identifier of the TTY, we're busted: */ + if (!getCharDevId(fdtty, &ttyId, &sb)) + goto done; + + /* If we find a matching PPS device ID, return the path to the + * device. It might not open, but it's the best we can get. + */ + if (findPpsDevId(ttyId, &ppsId)) { + dpath = findDevByDevId(ppsId); + goto done; + } + +# ifdef ENABLE_MAGICPPS + /* 'magic' PPS support -- try to instantiate missing PPS devices + * on-the-fly. Our mileage may vary -- running as root at that + * moment is vital for success. (We *can* create the PPS device + * as ordnary user, but we won't be able to open it!) + */ + + /* If we're root, try to push the PPS LDISC to the tty FD. If + * that does not work out, we're busted again: + */ + if ((0 != geteuid()) || (-1 == ioctl(fdtty, TIOCSETD, &ldisc))) + goto done; + msyslog(LOG_INFO, "auto-instantiated PPS device for device %u:%u", + major(ttyId), minor(ttyId)); + + /* We really should find a matching PPS device now. And since + * we're root (see above!), we should be able to open that device. + */ + if (findPpsDevId(ttyId, &ppsId)) + dpath = findDevByDevId(ppsId); + if (!dpath) + goto done; + + /* And since ince we're 'root', we might as well try to clone + * the ownership and access rights from the original TTY to the + * PPS device. If that does not work, we just have to live with + * what we've got so far... + */ + if (-1 == (fdpps = open(dpath, OModeF))) { + msyslog(LOG_ERR, "could not open auto-created '%s': %m", dpath); + goto done; + } + if (-1 == fchmod(fdpps, sb.st_mode)) { + msyslog(LOG_ERR, "could not chmod auto-created '%s': %m", dpath); + } + if (-1 == fchown(fdpps, sb.st_uid, sb.st_gid)) { + msyslog(LOG_ERR, "could not chown auto-created '%s': %m", dpath); + } + close(fdpps); +# else + (void)ldisc; +# endif + + done: + /* Whatever we go so far, that's it. */ + return dpath; +} + +/* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- */ +#endif /* linux PPS device matcher */ +/* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- */ + + +int +ppsdev_open( + int ttyfd , + const char *ppspath, + int omode , + int oflags ) +{ + int retfd = -1; + +# if defined(__unix__) && !defined(_WIN32) + if (-1 == retfd) { + if (ppspath && *ppspath) { + retfd = open(ppspath, omode, oflags); + msyslog(LOG_INFO, "ppsdev_open(%s) %s", + ppspath, (retfd != -1 ? "succeeded" : "failed")); + } + } +# endif + +# if defined(WITH_PPSDEV_MATCH) + if (-1 == retfd) { + char *xpath = findMatchingPpsDev(ttyfd); + if (xpath && *xpath) { + retfd = open(xpath, omode, oflags); + msyslog(LOG_INFO, "ppsdev_open(%s) %s", + xpath, (retfd != -1 ? "succeeded" : "failed")); + } + free(xpath); + } +# endif + + /* BSDs and probably SOLARIS can use the TTY fd for the PPS API, + * and so does Windows where the PPS API is implemented via an + * IOCTL. Likewise does the 'SoftPPS' implementation in Windows + * based on COM Events. So, if everything else fails, simply + * try the FD given for the TTY/COMport... + */ + if (-1 == retfd) + retfd = ttyfd; + + return retfd; +} + +/* --*-- that's all folks --*-- */ diff --git a/ntpd/refclock_nmea.c b/ntpd/refclock_nmea.c index 1d7997c6a..0b9e2ea7e 100644 --- a/ntpd/refclock_nmea.c +++ b/ntpd/refclock_nmea.c @@ -472,7 +472,7 @@ nmea_shutdown( # ifdef HAVE_PPSAPI if (up->ppsapi_lit) time_pps_destroy(up->atom.handle); - if (up->ppsapi_tried && up->ppsapi_fd != pp->io.fd) + if ((up->ppsapi_fd != -1) && (up->ppsapi_fd != pp->io.fd)) close(up->ppsapi_fd); # endif free(up); @@ -510,25 +510,48 @@ nmea_control( * PPS control * * If /dev/gpspps$UNIT can be opened that will be used for - * PPSAPI. Otherwise, the GPS serial device /dev/gps$UNIT - * already opened is used for PPSAPI as well. (This might not - * work, in which case the PPS API remains unavailable...) + * PPSAPI. On Linux, a PPS device mathing the TTY will be + * searched for and possibly created on the fly. Otherwise, the + * GPS serial device /dev/gps$UNIT already opened is used for + * PPSAPI as well. (This might not work, in which case the PPS + * API remains unavailable...) */ /* Light up the PPSAPI interface if not yet attempted. */ if ((CLK_FLAG1 & pp->sloppyclockflag) && !up->ppsapi_tried) { + int ppsfd; up->ppsapi_tried = TRUE; + /* get FD for the pps device; might be the tty itself! */ devlen = snprintf(device, sizeof(device), PPSDEV, unit); if (devlen < sizeof(device)) { - up->ppsapi_fd = open(device, PPSOPENMODE, - S_IRUSR | S_IWUSR); + ppsfd = ppsdev_open( + pp->io.fd, device, + PPSOPENMODE, (S_IRUSR|S_IWUSR)); } else { - up->ppsapi_fd = -1; + ppsfd = pp->io.fd; msyslog(LOG_ERR, "%s PPS device name too long", refnumtoa(&peer->srcadr)); } - if (-1 == up->ppsapi_fd) - up->ppsapi_fd = pp->io.fd; + /* Now do a dance to juggle it into place: */ + if (-1 == up->ppsapi_fd) { + /* no previous FD -- that one is easy. */ + up->ppsapi_fd = ppsfd; + } else if (ppsfd != pp->io.fd) { + /* new distinct pps FD -- take it! */ + if (up->ppsapi_fd != pp->io.fd) + close(up->ppsapi_fd); + up->ppsapi_fd = ppsfd; + } + /* If neither condition above is met, we have to keep + * the existing pps handle: It is either a device we + * could not open again since we dropped privs, or it is + * the tty handle because there was nothing else to open + * right from the beginning. + * + * note: the PPS I/O handle remains valid until + * - the clock is shut down + * - flag1 is set again after being cleared + */ if (refclock_ppsapi(up->ppsapi_fd, &up->atom)) { /* use the PPS API for our own purposes now. */ up->ppsapi_lit = refclock_params( @@ -540,9 +563,6 @@ nmea_control( "%s set PPSAPI params fails", refnumtoa(&peer->srcadr)); } - /* note: the PPS I/O handle remains valid until - * flag1 is cleared or the clock is shut down. - */ } else { msyslog(LOG_WARNING, "%s flag1 1 but PPSAPI fails", @@ -556,10 +576,7 @@ nmea_control( if (up->ppsapi_lit) time_pps_destroy(up->atom.handle); up->atom.handle = 0; - /* close/drop PPS fd */ - if (up->ppsapi_fd != pp->io.fd) - close(up->ppsapi_fd); - up->ppsapi_fd = -1; + /* do !!NOT!! close/drop PPS fd here! */ /* clear markers and peer items */ up->ppsapi_gate = FALSE;