/* $NetBSD: ntp_ppsdev.c,v 1.2 2024/08/18 20:47:17 christos Exp $ */
/*
* ntp_ppsdev.c - PPS-device support
*
* Written by Juergen Perlinger (
[email protected]) 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"
#ifdef REFCLOCK
#if defined(HAVE_UNISTD_H)
# include <unistd.h>
#endif
#if defined(HAVE_FCNTL_H)
# include <fcntl.h>
#endif
#include <stdlib.h>
/* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- */
#if defined(__linux__) && defined(HAVE_OPENAT) && defined(HAVE_FDOPENDIR)
#define WITH_PPSDEV_MATCH
/* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- */
#include <stdio.h>
#include <dirent.h>
#include <string.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <linux/tty.h>
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 a piece of allocated
* memory and trim any trailing whitespace.
*
* The issue here is that several files in the 'sysfs' tree claim a size
* of 4096 bytes when you 'stat' them -- but reading gives EOF after a
* few chars. (I *can* understand why the kernel takes this shortcut.
* it's just a bit unwieldy...)
*/
static char*
readFileAt(
int rfd ,
const char *path)
{
struct stat sb;
char *ret = NULL;
ssize_t rdlen;
int dfd;
if (-1 == (dfd = openat(rfd, path, OModeF)) || -1 == fstat(dfd, &sb))
goto fail;
if ((sb.st_size > 0x2000) || (NULL == (ret = malloc(sb.st_size + 1))))
goto fail;
if (1 > (rdlen = read(dfd, ret, sb.st_size)))
goto fail;
close(dfd);
while (rdlen > 0 && ret[rdlen - 1] <= ' ')
--rdlen;
ret[rdlen] = '\0';
return ret;
fail:
free(ret);
if (-1 != dfd)
close(dfd);
return NULL;
}
/* --------------------------------------------------------------------
* 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"))
goto done;
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);
done:
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;
/* 'path' contains the primary path to the associated TTY:
* we 'stat()' for the device id in 'st_rdev'.
*/
if (NULL == (bufp = readFileAt(fdDir, "path")))
goto done;
if ((-1 == stat(bufp, &sb)) || !S_ISCHR(sb.st_mode))
goto done;
*pTty = sb.st_rdev;
free(bufp);
/* 'dev' holds the device ID of the PPS device as 'major:minor'
* in text format. *sigh* couldn't that simply be the name of
* the PPS device itself, as in 'path' above??? But nooooo....
*/
if (NULL == (bufp = readFileAt(fdDir, "dev")))
goto done;
dmaj = strtoul((scan = bufp), &endp, 10);
if ((endp == scan) || (*endp != ':') || (dmaj >= 256))
goto done;
dmin = strtoul((scan = endp + 1), &endp, 10);
if ((endp == scan) || (*endp >= ' ') || (dmin >= 256))
goto done;
*pPps = makedev((unsigned int)dmaj, (unsigned int)dmin);
rc = TRUE;
done:
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 ClassDir;
struct dirent *dent;
dev_t othId, ppsId;
int fdDevDir;
if (!xdirOpenAt(&ClassDir, AT_FDCWD, "/sys/class/pps"))
goto done;
while (!found && (dent = readdir(ClassDir.dir))) {
/* If the entry is not a referring to a PPS device or
* if we can't open the directory for reading, skipt it:
*/
if (strncmp("pps", dent->d_name, 3))
continue;
fdDevDir = openat(ClassDir.dfd, dent->d_name, OModeD);
if (-1 == fdDevDir)
continue;
/* get the data and check if device ID for the TTY
* is what we're looking for:
*/
found = getPpsTuple(fdDevDir, &othId, &ppsId)
&& (ttyId == othId);
close(fdDevDir);
}
xdirClose(&ClassDir);
if (found)
*pPpsId = ppsId;
done:
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 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 */
/* -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- */
#include "ntp_clockdev.h"
int
ppsdev_reopen(
const sockaddr_u *srcadr,
int ttyfd , /* current tty FD, or -1 */
int ppsfd , /* current pps FD, or -1 */
const char *ppspath, /* path to pps device, or NULL */
int omode , /* open mode for pps device */
int oflags ) /* openn flags for pps device */
{
int retfd = -1;
const char *altpath;
/* avoid 'unused' warnings: we might not use all args, no
* thanks to conditional compiling:)
*/
(void)ppspath;
(void)omode;
(void)oflags;
if (NULL != (altpath = clockdev_lookup(srcadr, 1)))
ppspath = altpath;
# 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) && (-1 != ttyfd)) {
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 = ppsfd;
if (-1 == retfd)
retfd = ttyfd;
/* Close the old pps FD, but only if the new pps FD is neither
* the tty FD nor the existing pps FD!
*/
if ((retfd != ttyfd) && (retfd != ppsfd))
ppsdev_close(ttyfd, ppsfd);
return retfd;
}
void
ppsdev_close(
int ttyfd, /* current tty FD, or -1 */
int ppsfd) /* current pps FD, or -1 */
{
/* The pps fd might be the same as the tty fd. We close the pps
* channel only if it's valid and _NOT_ the tty itself:
*/
if ((-1 != ppsfd) && (ttyfd != ppsfd))
close(ppsfd);
}
/* --*-- that's all folks --*-- */
#else
NONEMPTY_TRANSLATION_UNIT
#endif /* !defined(REFCLOCK) */