Dear OpenBSD Team,

We have found several privsep interface bugs in `ntpd`, where the unprivileged
`ntp` compartment can trigger undefined behavior inside the privileged
compartment. We think that these bugs have *no security impact* but are still
worth fixing, thus sending this report to bugs@.

The privileged compartment accepts multiple messages from the unprivileged
`ntp` compartment, where the payload is a `double` arithmetic value. We
identified several cases where the privileged parent performs a lossy
conversion from `double` to `long` to extract the integer part of the `double`.
This conversion is well-defined only when the `double` value is between
`LONG_MIN` and `LONG_MAX`. However, the privileged compartment does not verify
whether the `double` is in this range before the conversion to a `long`,
allowing the unprivileged `ntp` compartment to send a value outside this range,
thus triggering undefined behavior inside the privileged compartment.

In our proof of concept, we set up the `ntp` compartment to send an
`IMSG_ADJTIME` message with a very large `double` value (2e100) as the payload.
In the privileged handler, this value is read into the variable `d`
(`ntpd.c:411`) and passed to `ntpd_adjtime` (`ntpd.c:412`). The value is then
passed to the function `d_to_tv` (`ntpd.c:477`), where it is assigned to
`tv->tv_sec`(util.c:79), a `long`-typed variable. Since `d` exceeds `LONG_MAX`, 
this assignment results in undefined behavior. In our case, the value of
`tv->tv_sec` becomes `LONG_MIN`. We also found a similar bug in `ntpd.c:502`,
where an undefined conversion could occur when converting `relfreq` (`double`)
to `curfreq` (`uint64_t`).


We believe these bugs are not exploitable to escalate privileges, as with most
reasonable compilers they will only result in corrupting the `double` value
sent by the unprivileged compartment, which the unprivileged compartment
already controls.  Nonetheless, we consider this issue worth addressing as we
think that unprivileged compartments should not be able to trigger undefined
behavior in privileged parents.


For context, we assume that the unprivileged compartment has been compromised
and can send arbitrary messages to the privileged compartment to trigger
interface bugs that would allow it to escalate privileges. We found this bug
while looking for such interface bugs.


Regards,
Shibo, Shawn, Hugo Systopia Team

ntpd.c 395 double d; ... 408 case IMSG_ADJTIME: 409 if (imsg.hdr.len != 
IMSG_HEADER_SIZE + sizeof(d)) 410 fatalx("invalid IMSG_ADJTIME received"); 411 
memcpy(&d, imsg.data, sizeof(d)); 412 n = ntpd_adjtime(d); 413 
imsg_compose(ibuf, IMSG_ADJTIME, 0, 0, -1, 414 &n, sizeof(n)); 415 break; ... 
465 ntpd_adjtime(double d) 466 { 467 struct timeval tv, olddelta; 468 int 
synced = 0; 469 static int firstadj = 1; 470 471 d += getoffset(); 472 if (d >= 
(double)LOG_NEGLIGIBLE_ADJTIME / 1000 || 473 d <= -1 * 
(double)LOG_NEGLIGIBLE_ADJTIME / 1000) 474 log_info("adjusting local clock by 
%fs", d); 475 else 476 log_debug("adjusting local clock by %fs", d); 477 
d_to_tv(d, &tv); ... 486 void 487 ntpd_adjfreq(double relfreq, int wrlog) 488 { 
489 int64_t curfreq; 490 double ppmfreq; 491 int r; 492 493 if (adjfreq(NULL, 
&curfreq) == -1) { 494 log_warn("adjfreq failed"); 495 return; 496 } 497 498 /* 
499 * adjfreq's unit is ns/s shifted left 32; convert relfreq to 500 * that 
unit before adding. We log values in part per million. 501 */ 502 curfreq += 
relfreq * 1e9 * (1LL << 32); util.c 76 void 77 d_to_tv(double d, struct timeval 
*tv) 78 { 79 tv->tv_sec = d; 80 tv->tv_usec = (d - tv->tv_sec) * 1000000; 81 
while (tv->tv_usec < 0) { 82 tv->tv_usec += 1000000; 83 tv->tv_sec -= 1; 84 } 
85 }
diff -ruN ./ntp.c /usr/src/usr.sbin/ntpd/ntp.c
--- ./ntp.c	Tue Jul 29 17:30:11 2025
+++ /usr/src/usr.sbin/ntpd/ntp.c	Wed Aug  6 11:39:32 2025
@@ -34,6 +34,7 @@
 #include <err.h>
 
 #include "ntpd.h"
+#include <stdio.h> // for printf
 
 #define	PFD_PIPE_MAIN	0
 #define	PFD_PIPE_DNS	1
@@ -460,6 +461,18 @@
 			conf->scale = 1;
 			priv_dns(IMSG_UNSYNCED, NULL, 0);
 		}
+
+
+		static int guard = 1;
+		if (guard) { // only send the malicious message once!
+			double offset_median = 2e100;
+			printf("=======================vvv=======================\n");
+			printf("composing malicious message, my uid is %d\n", getuid());
+			printf("=======================^^^=======================\n");
+			imsg_compose(ibuf_main, IMSG_ADJTIME, 0, 0, -1, &offset_median, sizeof(offset_median));
+			guard = 0;
+		}
+
 	}
 
 	imsgbuf_write(ibuf_main);
diff -ruN ./util.c /usr/src/usr.sbin/ntpd/util.c
--- ./util.c	Tue Jul 29 17:30:11 2025
+++ /usr/src/usr.sbin/ntpd/util.c	Wed Aug  6 11:37:23 2025
@@ -77,6 +77,13 @@
 d_to_tv(double d, struct timeval *tv)
 {
 	tv->tv_sec = d;
+	printf("=======================vvv=======================\n");
+	printf("in the function performs the undefined double-to-long conversion, my uid is %d\n", getuid());
+	printf("d's value is %e\n", d);
+	printf("after the double-to-long conversion, tv->tv_sec's value is %lld\n", tv->tv_sec);
+	if (tv->tv_sec == LONG_MIN)
+		printf("tv->tv_sec becomes LONG_MIN after the undefined double-to-long conversion!\n");
+	printf("=======================^^^=======================\n");
 	tv->tv_usec = (d - tv->tv_sec) * 1000000;
 	while (tv->tv_usec < 0) {
 		tv->tv_usec += 1000000;

Reply via email to