xref: /netbsd-src/external/bsd/ntp/dist/ntpd/ntp_ppsdev.c (revision f8cf1a9151c7af1cb0bd8b09c13c66bca599c027)
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@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