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