โฌข DragonFlyBSD Kernel Audit
โ† dashboard
DF-0022

PPS_IOC_KCBIND missing privilege check allows unprivileged kernel-PLL binding (NTP confusion)

Field Value
ID DF-0022
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N
CWE CWE-862 Missing Authorization
File sys/kern/kern_clock.c
Lines 1680-1694
Area kern
Confidence certain
Discovered 2026-06-29
Reported pending

Summary

The PPS_IOC_KCBIND handler in pps_ioctl() honors an unprivileged request to bind the kernel hardpps() consumer (pps->kcmode = kapi->edge) without any caps_priv_check_self(). The code carries a /* XXX Only root should be able to do this */ comment acknowledging the omission. The pps(4) cdev is created mode 0644, so any local user can open /dev/pps0 and issue the ioctl. On a PPS_SYNC kernel, each subsequent PPS pulse on that source drives hardpps(), poisoning the global pps_freq/ pps_jitter/pps_tf[]/pps_valid/STA_PPSSIGNAL state used by ntp_update_second(). A confused-deputy result follows: a privileged ntpd that enables STA_PPSFREQ/STA_PPSTIME will discipline the system clock against attacker-influenced values (bounded by MAXFREQ/MAXPHASE).

Root cause

sys/kern/kern_clock.c:1680-1694:

case PPS_IOC_KCBIND:
#ifdef PPS_SYNC
    kapi = (struct pps_kcbind_args *)data;
    /* XXX Only root should be able to do this */
    if (kapi->tsformat && kapi->tsformat != PPS_TSFMT_TSPEC)
        return (EINVAL);
    if (kapi->kernel_consumer != PPS_KC_HARDPPS)
        return (EINVAL);
    if (kapi->edge & ~pps->ppscap)
        return (EINVAL);
    pps->kcmode = kapi->edge;          /* :1690  no privilege check */
    return (0);
#else
    return (EOPNOTSUPP);
#endif

The callers (sys/dev/misc/pps/pps.c:183, sys/dev/serial/sio/sio.c, sys/bus/u4b/serial/usb_serial.c) forward the ioctl verbatim with no upstream privilege check. pps(4) is created make_dev(..., UID_ROOT, GID_WHEEL, 0644, ...) (sys/dev/misc/pps/pps.c:103-104). So any local user can open /dev/pps0 and bind hardpps().

Threat model & preconditions

  • Attacker position: any local unprivileged user who can open a PPS-capable device node that is world-openable (the parallel-port /dev/pps0 is 0644 by default).
  • Privileges gained or impact: confused-deputy against NTP. The attacker cannot directly step the clock, but can (a) poison the pps_* quality counters (pps_jitcnt/pps_errcnt/pps_stbcnt/pps_calcnt) reported back to ntpd via ntp_adjtime(), and (b) if a privileged ntpd runs with PPS discipline (STA_PPSFREQ and/or STA_PPSTIME), steer pps_freq/ time_offset under the MAXFREQ/MAXPHASE clamps โ€” bounded but arbitrary phase/frequency drift and the ability to spoof STA_PPSSIGNAL.
  • Required config or capabilities: kernel built with options PPS_SYNC (time-service/LINT64-derived kernels); a PPS event source the user can drive (e.g. parallel-port pin 10 via ppbus); realized clock impact additionally needs a privileged PPS-disciplined ntpd.
  • Reachability: ioctl(fd, PPS_IOC_KCBIND, ...) on an openable PPS device.

Proof of concept

PoC source: findings/poc/DF-0022/kcbind.c

Opens /dev/pps0 and issues PPS_IOC_KCBIND to demonstrate the privilege bypass (the ioctl succeeds for an unprivileged user).

Build & run (unprivileged)

cc -o kcbind findings/poc/DF-0022/kcbind.c
./kcbind /dev/pps0

Expected output (bug present)

[+] KCBIND succeeded (privilege bypass) on /dev/pps0 as uid=1000

(Full clock-steering needs the additional preconditions above.) On a fixed kernel the ioctl returns EPERM.

Impact

Low. A missing privilege check acknowledged in-tree by a XXX comment, with a bounded confused-deputy effect against NTP (not direct clock control). The fix is one check.

Gate PPS_IOC_KCBIND behind the same privilege used for setting the time (SYSCAP_NOSETTIME), resolving the long-standing XXX:

--- a/sys/kern/kern_clock.c
+++ b/sys/kern/kern_clock.c
@@ -1680,6 +1680,11 @@
    case PPS_IOC_KCBIND:
 #ifdef PPS_SYNC
+       /*
+        * Binding the kernel PPS consumer (hardpps) influences the
+        * system clock discipline, so require the set-time privilege.
+        */
+       if (caps_priv_check_self(SYSCAP_NOSETTIME))
+           return (EPERM);
        kapi = (struct pps_kcbind_args *)data;
-       /* XXX Only root should be able to do this */
        if (kapi->tsformat && kapi->tsformat != PPS_TSFMT_TSPEC)

References

Timeline

  • 2026-06-29 Discovered during automated file-by-file audit of sys/kern/kern_clock.c.
  • pending Reported to DragonFlyBSD security contact.