Home | History | Annotate | Line # | Download | only in ntpd
      1 /*	$NetBSD: ntp_ppsdev.c,v 1.2 2024/08/18 20:47:17 christos Exp $	*/
      2 
      3 /*
      4  * ntp_ppsdev.c - PPS-device support
      5  *
      6  * Written by Juergen Perlinger (perlinger (at) ntp.org) for the NTP project.
      7  * The contents of 'html/copyright.html' apply.
      8  * ---------------------------------------------------------------------
      9  * Helper code to work around (or with) a Linux 'specialty': PPS devices
     10  * are created via attaching the PPS line discipline to a TTY.  This
     11  * creates new pps devices, and the PPS API is *not* available through
     12  * the original TTY fd.
     13  *
     14  * Findig the PPS device associated with a TTY is possible but needs
     15  * quite a bit of file system traversal & lookup in the 'sysfs' tree.
     16  *
     17  * The code below does the job for kernel versions 4 & 5, and will
     18  * probably work for older and newer kernels, too... and in any case, if
     19  * the device or symlink to the PPS device with the given name exists,
     20  * it will take precedence anyway.
     21  * ---------------------------------------------------------------------
     22  */
     23 #ifdef __linux__
     24 # define _GNU_SOURCE
     25 #endif
     26 
     27 #include "config.h"
     28 
     29 #include "ntpd.h"
     30 
     31 #ifdef REFCLOCK
     32 
     33 #if defined(HAVE_UNISTD_H)
     34 # include <unistd.h>
     35 #endif
     36 #if defined(HAVE_FCNTL_H)
     37 # include <fcntl.h>
     38 #endif
     39 
     40 #include <stdlib.h>
     41 
     42 /* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- */
     43 #if defined(__linux__) && defined(HAVE_OPENAT) && defined(HAVE_FDOPENDIR)
     44 #define WITH_PPSDEV_MATCH
     45 /* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- */
     46 
     47 #include <stdio.h>
     48 #include <dirent.h>
     49 #include <string.h>
     50 #include <errno.h>
     51 
     52 #include <sys/ioctl.h>
     53 #include <sys/types.h>
     54 #include <sys/stat.h>
     55 #include <sys/sysmacros.h>
     56 #include <linux/tty.h>
     57 
     58 typedef int BOOL;
     59 #ifndef TRUE
     60 # define TRUE 1
     61 #endif
     62 #ifndef FALSE
     63 # define FALSE 0
     64 #endif
     65 
     66 static const int OModeF = O_CLOEXEC|O_RDONLY|O_NOCTTY;
     67 static const int OModeD = O_CLOEXEC|O_RDONLY|O_DIRECTORY;
     68 
     69 /* ------------------------------------------------------------------ */
     70 /* extended directory stream
     71  */
     72 typedef struct {
     73 	int  dfd;	/* file descriptor for dir for 'openat()' */
     74 	DIR *dir;	/* directory stream for iteration         */
     75 } XDIR;
     76 
     77 static void
     78 xdirClose(
     79 	XDIR *pxdir)
     80 {
     81 	if (NULL != pxdir->dir)
     82 		closedir(pxdir->dir); /* closes the internal FD, too! */
     83 	else if (-1 != pxdir->dfd)
     84 		close(pxdir->dfd);    /* otherwise _we_ have to do it */
     85 	pxdir->dfd = -1;
     86 	pxdir->dir = NULL;
     87 }
     88 
     89 static BOOL
     90 xdirOpenAt(
     91 	XDIR       *pxdir,
     92 	int         fdo  ,
     93 	const char *path )
     94 {
     95 	/* Officially, the directory stream owns the file discriptor it
     96 	 * received via 'fdopendir()'.  But for the purpose of 'openat()'
     97 	 * it's ok to keep the value around -- even if we should do
     98 	 * _absolutely_nothing_ with it apart from using it as a path
     99 	 * reference!
    100 	 */
    101 	pxdir->dir = NULL;
    102 	if (-1 == (pxdir->dfd = openat(fdo, path, OModeD)))
    103 		goto fail;
    104 	if (NULL == (pxdir->dir = fdopendir(pxdir->dfd)))
    105 		goto fail;
    106 	return TRUE;
    107 
    108   fail:
    109 	xdirClose(pxdir);
    110 	return FALSE;
    111 }
    112 
    113 /* --------------------------------------------------------------------
    114  * read content of a file (with a size limit) into a piece of allocated
    115  * memory and trim any trailing whitespace.
    116  *
    117  * The issue here is that several files in the 'sysfs' tree claim a size
    118  * of 4096 bytes when you 'stat' them -- but reading gives EOF after a
    119  * few chars.  (I *can* understand why the kernel takes this shortcut.
    120  * it's just a bit unwieldy...)
    121  */
    122 static char*
    123 readFileAt(
    124 	int         rfd ,
    125 	const char *path)
    126 {
    127 	struct stat sb;
    128 	char *ret = NULL;
    129 	ssize_t rdlen;
    130 	int dfd;
    131 
    132 	if (-1 == (dfd = openat(rfd, path, OModeF)) || -1 == fstat(dfd, &sb))
    133 		goto fail;
    134 	if ((sb.st_size > 0x2000) || (NULL == (ret = malloc(sb.st_size + 1))))
    135 		goto fail;
    136 	if (1 > (rdlen = read(dfd, ret, sb.st_size)))
    137 		goto fail;
    138 	close(dfd);
    139 
    140 	while (rdlen > 0 && ret[rdlen - 1] <= ' ')
    141 		--rdlen;
    142 	ret[rdlen] = '\0';
    143 	return ret;
    144 
    145   fail:
    146 	free(ret);
    147 	if (-1 != dfd)
    148 		close(dfd);
    149 	return NULL;
    150 }
    151 
    152 /* --------------------------------------------------------------------
    153  * Scan the "/dev" directory for a device with a given major and minor
    154  * device id. Return the path if found.
    155  */
    156 static char*
    157 findDevByDevId(
    158 	dev_t rdev)
    159 {
    160 	struct stat    sb;
    161 	struct dirent *dent;
    162 	XDIR           xdir;
    163 	char          *name = NULL;
    164 
    165 	if (!xdirOpenAt(&xdir, AT_FDCWD, "/dev"))
    166 		goto done;
    167 
    168 	while (!name && (dent = readdir(xdir.dir))) {
    169 		if (-1 == fstatat(xdir.dfd, dent->d_name,
    170 				  &sb, AT_SYMLINK_NOFOLLOW))
    171 			continue;
    172 		if (!S_ISCHR(sb.st_mode))
    173 			continue;
    174 		if (sb.st_rdev == rdev) {
    175 			if (-1 == asprintf(&name, "/dev/%s", dent->d_name))
    176 				name = NULL;
    177 		}
    178 	}
    179 	xdirClose(&xdir);
    180 
    181   done:
    182 	return name;
    183 }
    184 
    185 /* --------------------------------------------------------------------
    186  * Get the mofor:minor device id for a character device file descriptor
    187  */
    188 static BOOL
    189 getCharDevId(
    190 	int          fd ,
    191 	dev_t       *out,
    192 	struct stat *psb)
    193 {
    194 	BOOL        rc = FALSE;
    195 	struct stat sb;
    196 
    197 	if (NULL == psb)
    198 		psb = &sb;
    199 	if (-1 != fstat(fd, psb)) {
    200 		rc = S_ISCHR(psb->st_mode);
    201 		if (rc)
    202 			*out = psb->st_rdev;
    203 		else
    204 			errno = EINVAL;
    205 	}
    206 	return rc;
    207 }
    208 
    209 /* --------------------------------------------------------------------
    210  * given the dir-fd of a pps instance dir in the linux sysfs tree, get
    211  * the device IDs for the PPS device and the associated TTY.
    212  */
    213 static BOOL
    214 getPpsTuple(
    215 	int   fdDir,
    216 	dev_t *pTty,
    217 	dev_t *pPps)
    218 {
    219 	BOOL          rc = FALSE;
    220 	unsigned long dmaj, dmin;
    221 	struct stat   sb;
    222 	char         *bufp, *endp, *scan;
    223 
    224 	/* 'path' contains the primary path to the associated TTY:
    225 	 * we 'stat()' for the device id in 'st_rdev'.
    226 	 */
    227 	if (NULL == (bufp = readFileAt(fdDir, "path")))
    228 		goto done;
    229 	if ((-1 == stat(bufp, &sb)) || !S_ISCHR(sb.st_mode))
    230 		goto done;
    231 	*pTty = sb.st_rdev;
    232 	free(bufp);
    233 
    234 	/* 'dev' holds the device ID of the PPS device as 'major:minor'
    235 	 * in text format.   *sigh* couldn't that simply be the name of
    236 	 * the PPS device itself, as in 'path' above??? But nooooo....
    237 	 */
    238 	if (NULL == (bufp = readFileAt(fdDir, "dev")))
    239 		goto done;
    240 	dmaj = strtoul((scan = bufp), &endp, 10);
    241 	if ((endp == scan) || (*endp != ':') || (dmaj >= 256))
    242 		goto done;
    243 	dmin = strtoul((scan = endp + 1), &endp, 10);
    244 	if ((endp == scan) || (*endp >= ' ') || (dmin >= 256))
    245 		goto done;
    246 	*pPps = makedev((unsigned int)dmaj, (unsigned int)dmin);
    247 	rc = TRUE;
    248 
    249   done:
    250 	free(bufp);
    251 	return rc;
    252 }
    253 
    254 /* --------------------------------------------------------------------
    255  * for a given (TTY) device id, lookup the corresponding PPS device id
    256  * by processing the contents of the kernel sysfs tree.
    257  * Returns false if no such PS device can be found; otherwise set the
    258  * ouput parameter to the PPS dev id and return true...
    259  */
    260 static BOOL
    261 findPpsDevId(
    262 	dev_t  ttyId ,
    263 	dev_t *pPpsId)
    264 {
    265 	BOOL           found = FALSE;
    266 	XDIR           ClassDir;
    267 	struct dirent *dent;
    268 	dev_t          othId, ppsId;
    269 	int            fdDevDir;
    270 
    271 	if (!xdirOpenAt(&ClassDir, AT_FDCWD, "/sys/class/pps"))
    272 		goto done;
    273 
    274 	while (!found && (dent = readdir(ClassDir.dir))) {
    275 
    276 		/* If the entry is not a referring to a PPS device or
    277 		 * if we can't open the directory for reading, skipt it:
    278 		 */
    279 		if (strncmp("pps", dent->d_name, 3))
    280 			continue;
    281 		fdDevDir = openat(ClassDir.dfd, dent->d_name, OModeD);
    282 		if (-1 == fdDevDir)
    283 			continue;
    284 
    285 		/* get the data and check if device ID for the TTY
    286 		 * is what we're looking for:
    287 		 */
    288 		found = getPpsTuple(fdDevDir, &othId, &ppsId)
    289 		    && (ttyId == othId);
    290 		close(fdDevDir);
    291 	}
    292 
    293 	xdirClose(&ClassDir);
    294 
    295 	if (found)
    296 		*pPpsId = ppsId;
    297   done:
    298 	return found;
    299 }
    300 
    301 /* --------------------------------------------------------------------
    302  * Return the path to a PPS device related to tghe TT fd given. The
    303  * function might even try to instantiate such a PPS device when
    304  * running es effective root.  Returns NULL if no PPS device can be
    305  * established; otherwise it is a 'malloc()'ed area that should be
    306  * 'free()'d after use.
    307  */
    308 static char*
    309 findMatchingPpsDev(
    310 	int fdtty)
    311 {
    312 	struct stat sb;
    313 	dev_t       ttyId, ppsId;
    314 	int         fdpps, ldisc = N_PPS;
    315 	char       *dpath = NULL;
    316 
    317 	/* Without the device identifier of the TTY, we're busted: */
    318 	if (!getCharDevId(fdtty, &ttyId, &sb))
    319 		goto done;
    320 
    321 	/* If we find a matching PPS device ID, return the path to the
    322 	 * device. It might not open, but it's the best we can get.
    323 	 */
    324 	if (findPpsDevId(ttyId, &ppsId)) {
    325 		dpath = findDevByDevId(ppsId);
    326 		goto done;
    327 	}
    328 
    329 #   ifdef ENABLE_MAGICPPS
    330 	/* 'magic' PPS support -- try to instantiate missing PPS devices
    331 	 * on-the-fly.  Our mileage may vary -- running as root at that
    332 	 * moment is vital for success.  (We *can* create the PPS device
    333 	 * as ordnary user, but we won't be able to open it!)
    334 	 */
    335 
    336 	/* If we're root, try to push the PPS LDISC to the tty FD. If
    337 	 * that does not work out, we're busted again:
    338 	 */
    339 	if ((0 != geteuid()) || (-1 == ioctl(fdtty, TIOCSETD, &ldisc)))
    340 		goto done;
    341 	msyslog(LOG_INFO, "auto-instantiated PPS device for device %u:%u",
    342 		major(ttyId), minor(ttyId));
    343 
    344 	/* We really should find a matching PPS device now. And since
    345 	 * we're root (see above!), we should be able to open that device.
    346 	 */
    347 	if (findPpsDevId(ttyId, &ppsId))
    348 		dpath = findDevByDevId(ppsId);
    349 	if (!dpath)
    350 		goto done;
    351 
    352 	/* And since we're 'root', we might as well try to clone the
    353 	 * ownership and access rights from the original TTY to the
    354 	 * PPS device.  If that does not work, we just have to live with
    355 	 * what we've got so far...
    356 	 */
    357 	if (-1 == (fdpps = open(dpath, OModeF))) {
    358 		msyslog(LOG_ERR, "could not open auto-created '%s': %m", dpath);
    359 		goto done;
    360 	}
    361 	if (-1 == fchmod(fdpps, sb.st_mode)) {
    362 		msyslog(LOG_ERR, "could not chmod auto-created '%s': %m", dpath);
    363 	}
    364 	if (-1 == fchown(fdpps, sb.st_uid, sb.st_gid)) {
    365 		msyslog(LOG_ERR, "could not chown auto-created '%s': %m", dpath);
    366 	}
    367 	close(fdpps);
    368 #   else
    369 	(void)ldisc;
    370 #   endif
    371 
    372   done:
    373 	/* Whatever we go so far, that's it. */
    374 	return dpath;
    375 }
    376 
    377 /* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- */
    378 #endif /* linux PPS device matcher */
    379 /* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- */
    380 
    381 #include "ntp_clockdev.h"
    382 
    383 int
    384 ppsdev_reopen(
    385 	const sockaddr_u *srcadr,
    386 	int         ttyfd  , /* current tty FD, or -1 */
    387 	int         ppsfd  , /* current pps FD, or -1 */
    388 	const char *ppspath, /* path to pps device, or NULL */
    389 	int         omode  , /* open mode for pps device */
    390 	int         oflags ) /* openn flags for pps device */
    391 {
    392 	int retfd = -1;
    393 	const char *altpath;
    394 
    395 	/* avoid 'unused' warnings: we might not use all args, no
    396 	 * thanks to conditional compiling:)
    397 	 */
    398 	(void)ppspath;
    399 	(void)omode;
    400 	(void)oflags;
    401 
    402 	if (NULL != (altpath = clockdev_lookup(srcadr, 1)))
    403 		ppspath = altpath;
    404 
    405 #   if defined(__unix__) && !defined(_WIN32)
    406 	if (-1 == retfd) {
    407 		if (ppspath && *ppspath) {
    408 			retfd = open(ppspath, omode, oflags);
    409 			msyslog(LOG_INFO, "ppsdev_open(%s) %s",
    410 				ppspath, (retfd != -1 ? "succeeded" : "failed"));
    411 		}
    412 	}
    413 #   endif
    414 
    415 #   if defined(WITH_PPSDEV_MATCH)
    416 	if ((-1 == retfd) && (-1 != ttyfd)) {
    417 		char *xpath = findMatchingPpsDev(ttyfd);
    418 		if (xpath && *xpath) {
    419 			retfd = open(xpath, omode, oflags);
    420 			msyslog(LOG_INFO, "ppsdev_open(%s) %s",
    421 				xpath, (retfd != -1 ? "succeeded" : "failed"));
    422 		}
    423 		free(xpath);
    424 	}
    425 #   endif
    426 
    427 	/* BSDs and probably SOLARIS can use the TTY fd for the PPS API,
    428 	 * and so does Windows where the PPS API is implemented via an
    429 	 * IOCTL.  Likewise does the 'SoftPPS' implementation in Windows
    430 	 * based on COM Events.  So, if everything else fails, simply
    431 	 * try the FD given for the TTY/COMport...
    432 	 */
    433 	if (-1 == retfd)
    434 		retfd = ppsfd;
    435 	if (-1 == retfd)
    436 		retfd = ttyfd;
    437 
    438 	/* Close the old pps FD, but only if the new pps FD is neither
    439 	 * the tty FD nor the existing pps FD!
    440 	 */
    441 	if ((retfd != ttyfd) && (retfd != ppsfd))
    442 		ppsdev_close(ttyfd, ppsfd);
    443 
    444 	return retfd;
    445 }
    446 
    447 void
    448 ppsdev_close(
    449 	int ttyfd, /* current tty FD, or -1 */
    450 	int ppsfd) /* current pps FD, or -1 */
    451 {
    452 	/* The pps fd might be the same as the tty fd.  We close the pps
    453 	 * channel only if it's valid and _NOT_ the tty itself:
    454 	 */
    455 	if ((-1 != ppsfd) && (ttyfd != ppsfd))
    456 		close(ppsfd);
    457 }
    458 /* --*-- that's all folks --*-- */
    459 #else
    460 NONEMPTY_TRANSLATION_UNIT
    461 #endif /* !defined(REFCLOCK) */
    462