diff --git a/README b/README index 4154cce..749a6b8 100644 --- a/README +++ b/README @@ -27,9 +27,9 @@ operating parameters whilst it is running. What will chrony run on? ======================== -The software is known to work on Linux, FreeBSD, NetBSD, macOS and -illumos. Closely related systems may work too. Any other system will -likely require a porting exercise. +The software is known to work on Linux, FreeBSD, NetBSD, OpenBSD, macOS, +and illumos. Closely related systems may work too. Any other system +will likely require a porting exercise. How do I set it up? =================== diff --git a/configure b/configure index 47c7eb2..1c25443 100755 --- a/configure +++ b/configure @@ -234,6 +234,7 @@ try_libcap=-1 try_clockctl=0 feat_scfilter=0 try_seccomp=-1 +try_pledge=0 priv_ops="" feat_ipv6=1 feat_phc=1 @@ -447,6 +448,18 @@ case $OPERATINGSYSTEM in add_def NETBSD echo "Configuring for $SYSTEM" ;; + OpenBSD) + EXTRA_OBJECTS="sys_generic.o sys_openbsd.o sys_posix.o" + try_setsched=1 + try_lockmem=1 + try_pledge=1 + add_def OPENBSD + if [ $feat_droproot = "1" ]; then + add_def FEAT_PRIVDROP + priv_ops="ADJUSTTIME ADJUSTFREQ SETTIME" + fi + echo "Configuring for $SYSTEM" + ;; Darwin) EXTRA_OBJECTS="sys_macosx.o" LIBS="$LIBS -lresolv" @@ -812,6 +825,12 @@ then EXTRA_OBJECTS="$EXTRA_OBJECTS sys_linux_scmp.o" fi +if [ $feat_scfilter = "1" ] && [ $try_pledge = "1" ] && \ + test_code 'pledge()' 'unistd.h' '' '' 'pledge("stdio", NULL);' +then + add_def FEAT_SCFILTER +fi + if [ "x$priv_ops" != "x" ]; then EXTRA_OBJECTS="$EXTRA_OBJECTS privops.o" add_def PRIVOPS_HELPER diff --git a/doc/chrony.conf.adoc b/doc/chrony.conf.adoc index 7e6445e..6b49749 100644 --- a/doc/chrony.conf.adoc +++ b/doc/chrony.conf.adoc @@ -1449,8 +1449,8 @@ for a high quality clock using a temperature compensated crystal oscillator. This directive specifies the maximum assumed drift (frequency error) of the system clock. It limits the frequency adjustment that *chronyd* is allowed to use to correct the measured drift. It is an additional limit to the maximum -adjustment that can be set by the system driver (100000 ppm on Linux, 500 ppm -on FreeBSD, NetBSD, and macOS 10.13+, 32500 ppm on illumos). +adjustment that can be set by the system driver (100000 ppm on Linux and +OpenBSD, 500 ppm on FreeBSD, NetBSD, and macOS 10.13+, 32500 ppm on illumos). + By default, the maximum assumed drift is 500000 ppm, i.e. the adjustment is limited by the system driver rather than this directive. @@ -1487,10 +1487,10 @@ is effective only on systems where *chronyd* is able to control the rate (i.e. all supported systems with the exception of macOS 12 or earlier). + For each system there is a maximum frequency offset of the clock that can be set -by the driver. On Linux it is 100000 ppm, on FreeBSD, NetBSD and macOS 10.13+ it -is 5000 ppm, and on illumos it is 32500 ppm. Also, due to a kernel limitation, -setting *maxslewrate* on FreeBSD, NetBSD, macOS 10.13+ to a value between 500 -ppm and 5000 ppm will effectively set it to 500 ppm. +by the driver. On Linux and OpenBSD it is 100000 ppm, on FreeBSD, NetBSD and +macOS 10.13+ it is 5000 ppm, on illumos it is 32500 ppm. Also, due to a kernel +limitation, setting *maxslewrate* on FreeBSD, NetBSD, macOS 10.13+ to a value +between 500 ppm and 5000 ppm will effectively set it to 500 ppm. + By default, the maximum slew rate is set to 83333.333 ppm (one twelfth). @@ -2988,7 +2988,7 @@ file when the <> command is issued by *chronyc*). [[lock_all]]*lock_all*:: The *lock_all* directive will lock the *chronyd* process into RAM so that it will never be paged out. This can result in lower and more consistent latency. -The directive is supported on Linux, FreeBSD, NetBSD, and illumos. +The directive is supported on Linux, FreeBSD, NetBSD, OpenBSD, and illumos. [[pidfile]]*pidfile* _file_:: Unless *chronyd* is started with the *-Q* option, it writes its process ID @@ -3039,11 +3039,11 @@ accepted NTP-over-PTP messages. Messages from other domains are ignored. The default is 123, the minimum is 0, and the maximum is 255. [[sched_priority]]*sched_priority* _priority_:: -On Linux, FreeBSD, NetBSD, and illumos, the *sched_priority* directive will -select the SCHED_FIFO real-time scheduler at the specified priority (which must -be between 0 and 100). On macOS, this option must have either a value of 0 (the -default) to disable the thread time constraint policy or 1 for the policy to be -enabled. +On Linux, FreeBSD, NetBSD, OpenBSD, and illumos, the *sched_priority* directive +will select the SCHED_FIFO real-time scheduler at the specified priority (which +must be between 0 and 100). On macOS, this option must have either a value of 0 +(the default) to disable the thread time constraint policy or 1 for the policy +to be enabled. + On systems other than macOS, this directive uses the *pthread_setschedparam()* system call to instruct the kernel to use the SCHED_FIFO first-in, first-out @@ -3065,9 +3065,9 @@ The *user* directive sets the name of the system user to which *chronyd* will switch after start in order to drop root privileges. + On Linux, *chronyd* needs to be compiled with support for the *libcap* library. -On macOS, FreeBSD, NetBSD and illumos *chronyd* forks into two processes. -The child process retains root privileges, but can only perform a very limited -range of privileged system calls on behalf of the parent. +On macOS, FreeBSD, NetBSD, OpenBSD, and illumos *chronyd* forks into two +processes. The child process retains root privileges, but can only perform a +very limited range of privileged system calls on behalf of the parent. + The compiled-in default value is _@DEFAULT_USER@_. diff --git a/doc/chronyd.adoc b/doc/chronyd.adoc index dfabd3e..be34c96 100644 --- a/doc/chronyd.adoc +++ b/doc/chronyd.adoc @@ -102,7 +102,7 @@ directive in the configuration file. This option is useful if you want to stop and restart *chronyd* briefly for any reason, e.g. to install a new version. However, it should be used only on systems where the kernel can maintain clock compensation whilst not under *chronyd*'s control (i.e. Linux, FreeBSD, NetBSD, -illumos, and macOS 10.13 or later). +OpenBSD, illumos, and macOS 10.13 or later). *-R*:: When this option is used, the <> @@ -143,9 +143,9 @@ after start in order to drop root privileges. It overrides the _@DEFAULT_USER@_. + On Linux, *chronyd* needs to be compiled with support for the *libcap* library. -On macOS, FreeBSD, NetBSD, and illumos *chronyd* forks into two processes. -The child process retains root privileges, but can only perform a very limited -range of privileged system calls on behalf of the parent. +On macOS, FreeBSD, NetBSD, OpenBSD, and illumos *chronyd* forks into two +processes. The child process retains root privileges, but can only perform a +very limited range of privileged system calls on behalf of the parent. *-U*:: This option disables a check for root privileges to allow *chronyd* to be @@ -159,21 +159,22 @@ specific directives. *-F* _level_:: This option configures system call filters loaded by *chronyd* processes if it -was compiled with support for the Linux secure computing (seccomp) facility. -Three levels are defined: 0, 1, 2. The filters are disabled at level 0. At -levels 1 and 2, *chronyd* will be killed if it makes a system call which is -blocked by the filters. The level can be specified as a negative number to -trigger the SIGSYS signal instead of SIGKILL, which can be useful for -debugging. The default value is 0. +was compiled with support for the Linux secure computing (seccomp) facility or +OpenBSD pledge. For Linux three levels are defined: 0, 1, 2, for OpenBSD two +levels: 0, 1. The filters are disabled at level 0. On Linux at levels 1 and 2 +and on OpenBSD at level 1, *chronyd* will be killed if it makes a system call +which is blocked by the filters. On Linux the level can be specified as a +negative number to trigger the SIGSYS signal instead of SIGKILL, which can be +useful for debugging. The default value is 0. + -At level 1, the filters allow only selected system calls that are normally -expected to be made by *chronyd*. Other system calls are blocked. This level is -recommended only if it is known to work on the version of the system where -*chrony* is installed. The filters need to allow also system calls made by -libraries that *chronyd* is using (e.g. libc), but different versions or -implementations of the libraries might make different system calls. If the -filters are missing a system call, *chronyd* could be killed even in normal -operation. +On Linux at level 1, the filters allow only selected system calls that are +normally expected to be made by *chronyd*. Other system calls are blocked. +This level is recommended only if it is known to work on the version of the +system where *chrony* is installed. The filters need to allow also system +calls made by libraries that *chronyd* is using (e.g. libc), but different +versions or implementations of the libraries might make different system calls. +If the filters are missing a system call, *chronyd* could be killed even in +normal operation. + At level 2, the filters block only a small number of specific system calls (e.g. fork and exec). This approach should avoid false positives, but the @@ -183,15 +184,15 @@ limited. The filters cannot be enabled with the *mailonchange* directive. *-P* _priority_:: -On Linux, FreeBSD, NetBSD, and illumos this option will select the SCHED_FIFO -real-time scheduler at the specified priority (which must be between 0 and -100). On macOS, this option must have either a value of 0 to disable the thread -time constraint policy or 1 for the policy to be enabled. Other systems do not -support this option. The default value is 0. +On Linux, FreeBSD, NetBSD, OpenBSD, and illumos this option will select the +SCHED_FIFO real-time scheduler at the specified priority (which must be +between 0 and 100). On macOS, this option must have either a value of 0 to +disable the thread time constraint policy or 1 for the policy to be enabled. +Other systems do not support this option. The default value is 0. *-m*:: This option will lock *chronyd* into RAM so that it will never be paged out. -This mode is only supported on Linux, FreeBSD, NetBSD, and illumos. +This mode is only supported on Linux, FreeBSD, NetBSD, OpenBSD, and illumos. *-x*:: This option disables the control of the system clock. *chronyd* will not try to diff --git a/sys.c b/sys.c index 1a1a432..138f8fc 100644 --- a/sys.c +++ b/sys.c @@ -42,6 +42,9 @@ #elif defined(NETBSD) || defined(FREEBSD) #include "sys_netbsd.h" #include "sys_posix.h" +#elif defined(OPENBSD) +#include "sys_openbsd.h" +#include "sys_posix.h" #elif defined(MACOSX) #include "sys_macosx.h" #endif @@ -66,6 +69,8 @@ SYS_Initialise(int clock_control) SYS_Solaris_Initialise(); #elif defined(NETBSD) || defined(FREEBSD) SYS_NetBSD_Initialise(); +#elif defined(OPENBSD) + SYS_OpenBSD_Initialise(); #elif defined(MACOSX) SYS_MacOSX_Initialise(); #else @@ -88,6 +93,8 @@ SYS_Finalise(void) SYS_Solaris_Finalise(); #elif defined(NETBSD) || defined(FREEBSD) SYS_NetBSD_Finalise(); +#elif defined(OPENBSD) + SYS_OpenBSD_Finalise(); #elif defined(MACOSX) SYS_MacOSX_Finalise(); #else @@ -105,6 +112,8 @@ void SYS_DropRoot(uid_t uid, gid_t gid, SYS_ProcessContext context) SYS_Solaris_DropRoot(uid, gid, context); #elif (defined(NETBSD) || defined(FREEBSD)) && defined(FEAT_PRIVDROP) SYS_NetBSD_DropRoot(uid, gid, context, !null_driver); +#elif defined(OPENBSD) && defined(FEAT_PRIVDROP) + SYS_OpenBSD_DropRoot(uid, gid, context, !null_driver); #elif defined(MACOSX) && defined(FEAT_PRIVDROP) SYS_MacOSX_DropRoot(uid, gid, context); #else @@ -118,6 +127,8 @@ void SYS_EnableSystemCallFilter(int level, SYS_ProcessContext context) { #if defined(LINUX) && defined(FEAT_SCFILTER) SYS_Linux_EnableSystemCallFilter(level, context); +#elif defined(OPENBSD) && defined(FEAT_SCFILTER) + SYS_OpenBSD_EnableSystemCallFilter(level, context); #else LOG_FATAL("system call filter not supported"); #endif diff --git a/sys_openbsd.c b/sys_openbsd.c new file mode 100644 index 0000000..40705e9 --- /dev/null +++ b/sys_openbsd.c @@ -0,0 +1,281 @@ +/* + chronyd/chronyc - Programs for keeping computer clocks accurate. + + ********************************************************************** + * Copyright (C) Richard P. Curnow 1997-2001 + * Copyright (C) J. Hannken-Illjes 2001 + * Copyright (C) Miroslav Lichvar 2015 + * Copyright (C) Shaun Ren 2021 + * Copyright (C) Thomas Kupper 2026 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of version 2 of the GNU General Public License as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + ********************************************************************** + + ======================================================================= + + Driver file for the OpenBSD operating system. + */ + +#include "config.h" + +#include "sysincl.h" + +#include + +#include "sys_generic.h" +#include "sys_openbsd.h" +#include "conf.h" +#include "local.h" +#include "logging.h" +#include "privops.h" +#include "sched.h" +#include "util.h" + +/* The OpenBSD kernel supports a maximum value of 500000 ppm. + To avoid extending the range that would need to be tested, use + the same maximum as on Linux. + + Maximum frequency offset (in ppm) */ +#define MAX_FREQ 100000.0 + +/* RTC synchronisation - once an hour */ + +static struct timespec last_rtc_sync; +#define RTC_SYNC_INTERVAL (60 * 60.0) + +/* ================================================== */ + +static double +read_frequency(void) +{ + int64_t freq; + + if (PRV_AdjustFreq(NULL, &freq)) + LOG_FATAL("adjfreq() failed"); + + return (double)-freq / (1000LL << 32); +} + +/* ================================================== */ + +static double +set_frequency(double freq_ppm) +{ + int64_t freq; + + freq = -freq_ppm * (1000LL << 32); + if (PRV_AdjustFreq(&freq, NULL)) + LOG_FATAL("adjfreq() failed"); + + return read_frequency(); +} + +/* ================================================== */ + +static void +synchronise_rtc(void) +{ + struct timespec ts, new_ts; + double err; + + LCL_ReadRawTime(&ts); + + if (PRV_SetTime(CLOCK_REALTIME, &ts) < 0) { + DEBUG_LOG("clock_settime() failed"); + return; + } + + LCL_ReadRawTime(&new_ts); + err = UTI_DiffTimespecsToDouble(&new_ts, &ts); + + lcl_InvokeDispersionNotifyHandlers(fabs(err)); +} + +/* ================================================== */ + +static void +set_sync_status(int synchronised, double est_error, double max_error) +{ + double rtc_sync_elapsed; + struct timespec now; + + if (!synchronised || !CNF_GetRtcSync()) + return; + + SCH_GetLastEventTime(NULL, NULL, &now); + rtc_sync_elapsed = UTI_DiffTimespecsToDouble(&now, &last_rtc_sync); + if (fabs(rtc_sync_elapsed) >= RTC_SYNC_INTERVAL) { + synchronise_rtc(); + last_rtc_sync = now; + DEBUG_LOG("rtc synchronised"); + } +} + + +/* ================================================== */ + +static struct clockinfo +get_clockinfo(void) +{ + struct clockinfo cinfo; + size_t cinfo_len; + int mib[2]; + + cinfo_len = sizeof (cinfo); + mib[0] = CTL_KERN; + mib[1] = KERN_CLOCKRATE; + + if (sysctl(mib, 2, &cinfo, &cinfo_len, NULL, 0) < 0) + LOG_FATAL("sysctl() failed"); + + return cinfo; +} + +/* ================================================== */ + +static void +reset_adjtime_offset(void) +{ + struct timeval delta; + + memset(&delta, 0, sizeof (delta)); + + if (PRV_AdjustTime(&delta, NULL)) + LOG_FATAL("adjtime() failed"); +} + +/* ================================================== */ + +/* PRV_SetTime() uses clock_settime() to set the system time. + clock_setsetime() on OpenBSD is not pledged but + settimeofday() is. Override clock_settime() here for + OpenBSD and call settimeofday() from it. */ + +int +clock_settime(clockid_t clock, const struct timespec *now) +{ + struct timeval tv; + + if (clock != CLOCK_REALTIME) + return -1; + + UTI_TimespecToTimeval(now, &tv); + + return settimeofday(&tv, NULL); +} + +/* ================================================== */ + +void +SYS_OpenBSD_Initialise(void) +{ + struct clockinfo cinfo; + + cinfo = get_clockinfo(); + reset_adjtime_offset(); + + LCL_ReadRawTime(&last_rtc_sync); + + SYS_Generic_CompleteFreqDriver(MAX_FREQ, 1.0 / cinfo.hz, + read_frequency, set_frequency, NULL, + 0.0, 0.0, + NULL, NULL, + NULL, set_sync_status); +} + +/* ================================================== */ + +void +SYS_OpenBSD_Finalise(void) +{ + SYS_Generic_Finalise(); +} + +/* ================================================== */ + +#ifdef FEAT_PRIVDROP +void +SYS_OpenBSD_DropRoot(uid_t uid, gid_t gid, SYS_ProcessContext context, int clock_control) +{ + if (context == SYS_MAIN_PROCESS) + PRV_StartHelper(); + + UTI_DropRoot(uid, gid); +} +#endif + +/* ================================================== */ + +#ifdef FEAT_SCFILTER +void +SYS_OpenBSD_EnableSystemCallFilter(int level, SYS_ProcessContext context) +{ + /* If level == 0, SYS_EnableSystemCallFilter() is not called. Therefore + only a value of 1 is valid here. */ + if (level != 1 && context == SYS_MAIN_PROCESS) + /* Only log/fatal once in the main process, the child processes will be + terminated too as a result */ + LOG_FATAL("Unsupported filter level"); + + if (context == SYS_MAIN_PROCESS) { + /* stdio => allow libc stdio calls + {r,w,c}path => allow read/write/change config, drift file, etc + inet => allow connections to/from internet + unix => allow handling unix sockets + dns => allow DNS resolution + sendfd => allow send fd to nts_ke helper thread. In + NKS_Initialize() open_socket() -> accept_connection() + settime => allow set time if system call filter is enabled and user is root */ + const char **certs, **keys; + + if (CNF_GetNtsServerCertAndKeyFiles(&certs, &keys) > 0 && + CNF_GetNtsServerProcesses() > 0) { + /* NTS-KE helper(s) will be forked, the 'sendfd' promise is necessary */ + if (geteuid() == 0) { + /* Running as root, in addition settime pledge promise needed */ + if (pledge("stdio rpath wpath cpath inet unix dns sendfd settime", NULL) < 0) + LOG_FATAL("pledge() failed"); + } else { + if (pledge("stdio rpath wpath cpath inet unix dns sendfd", NULL) < 0) + LOG_FATAL("pledge() failed"); + } + } else { + /* No NTS-KE helper(s) will be forked, no need to set 'sendfd' promise */ + if (geteuid() == 0) { + /* Running as root, in addition settime pledge promise needed */ + if (pledge("stdio rpath wpath cpath inet unix dns settime", NULL) < 0) + LOG_FATAL("pledge() failed"); + } else { + if (pledge("stdio rpath wpath cpath inet unix dns", NULL) < 0) + LOG_FATAL("pledge() failed"); + } + } + } else if (context == SYS_PRIVOPS_HELPER) { + /* stdio => allow libc stdio calls + settime => allow set/adjust time */ + if (pledge("stdio settime", NULL) < 0) + LOG_FATAL("pledge() failed"); + } else if (context == SYS_NTSKE_HELPER) { + /* stdio => allow libc stdio calls + recvfd => allow receiving fd from main process. In run_helper() + -> handle_helper_request() */ + if (pledge("stdio recvfd", NULL) < 0) + LOG_FATAL("pledge() failed"); + } + + LOG(context == SYS_MAIN_PROCESS ? LOGS_INFO : LOGS_DEBUG, "Loaded pledge filter"); +} + +#endif diff --git a/sys_openbsd.h b/sys_openbsd.h new file mode 100644 index 0000000..9151966 --- /dev/null +++ b/sys_openbsd.h @@ -0,0 +1,41 @@ +/* + chronyd/chronyc - Programs for keeping computer clocks accurate. + + ********************************************************************** + * Copyright (C) Shaun Ren 2021 + * Copyright (C) Thomas Kupper 2026 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of version 2 of the GNU General Public License as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + ********************************************************************** + + ======================================================================= + + Header file for OpenBSD driver + */ + +#ifndef GOT_SYS_OPENBSD_H +#define GOT_SYS_OPENBSD_H + +#include "sys.h" + +void SYS_OpenBSD_Initialise(void); + +void SYS_OpenBSD_Finalise(void); + +void SYS_OpenBSD_DropRoot(uid_t uid, gid_t gid, SYS_ProcessContext context, int clock_control); + +void SYS_OpenBSD_EnableSystemCallFilter(int level, SYS_ProcessContext context); + +#endif diff --git a/test/system/099-scfilter b/test/system/099-scfilter index f34863d..04da06e 100755 --- a/test/system/099-scfilter +++ b/test/system/099-scfilter @@ -8,7 +8,21 @@ check_chronyd_features SCFILTER || test_skip "SCFILTER support disabled" test_start "system call filter in non-destructive tests" -for level in 1 2 -1 -2; do +os_name=$(uname -s) +case "$os_name" in + OpenBSD) + supported_levels=1 + ;; + Linux) + supported_levels="1 2 -1 -2" + ;; + *) + test_message 1 1 "unsupported OS $os_name" + test_fail + ;; +esac + +for level in $supported_levels; do test_message 1 1 "level $level:" for test in 0[0-8][0-9]-*[^_]; do test_message 2 0 "$test" diff --git a/test/system/199-scfilter b/test/system/199-scfilter index dccdac0..9499502 100755 --- a/test/system/199-scfilter +++ b/test/system/199-scfilter @@ -8,7 +8,21 @@ check_chronyd_features SCFILTER || test_skip "SCFILTER support disabled" test_start "system call filter in destructive tests" -for level in 1 2 -1 -2; do +os_name="$(uname -s)" +case $os_name in + OpenBSD) + supported_levels=1 + ;; + Linux) + supported_levels="1 2 -1 -2" + ;; + *) + test_message 1 1 "unsupported OS $os_name" + test_fail + ;; +esac + +for level in $supported_levels; do test_message 1 1 "level $level:" for test in 1[0-8][0-9]-*[^_]; do test_message 2 0 "$test"