diff --git a/.github/workflows/freebsd.yaml b/.github/workflows/freebsd.yaml new file mode 100644 index 000000000..e660a4407 --- /dev/null +++ b/.github/workflows/freebsd.yaml @@ -0,0 +1,47 @@ +name: FreeBSD + +permissions: {} + +on: + push: + branches: + - main + pull_request: + merge_group: + branches: + - main + +jobs: + freebsd: + name: FreeBSD unit tests + runs-on: ubuntu-latest + timeout-minutes: 45 + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Unit tests in a FreeBSD VM + uses: vmactions/freebsd-vm@77ed28d336d03fe19a3f4f7266c1d2c4714dd79d # v1.5.2 + with: + usesh: true + cache-after-prepare: true + # system::time::tests::get_process_start_time compares the process + # start time against the wall clock, so the guest clock must be sane. + sync-time: true + prepare: | + pkg install -y rust git-tiny + run: | + set -e + rustc --version + cargo --version + + # The unit tests must not run as root: src/su/context.rs says + # "this test is allowed to fail if run as root -- do not run unit + # tests as root", and the VM logs in as root by default. + pw useradd sudorstest -m + chown -R sudorstest . + + # We skip a couple of tests which fail when running as root. + su -m sudorstest -c "cd $PWD && env HOME=/home/sudorstest cargo test --workspace --all-targets --release -- --skip group_as_non_root --skip test_secure_open_cookie_file --skip test_traverse_secure_open_positive" diff --git a/src/system/time.rs b/src/system/time.rs index 46a281c61..05b221a12 100644 --- a/src/system/time.rs +++ b/src/system/time.rs @@ -118,6 +118,40 @@ impl ProcessCreateTime { } } + /// The current time on the same clock `Process::starting_time` reports. + /// + /// This deliberately does NOT reuse `SystemTime::now`, which always reads + /// `CLOCK_BOOTTIME`. On FreeBSD the process start time comes from + /// `kinfo_proc.ki_start`, which the kernel exports as absolute wall clock, + /// while `CLOCK_BOOTTIME` is an alias for the uptime clock there -- so the + /// two are not comparable. `SystemTime::now` must stay on the monotonic + /// clock (it guards the credential cache timeout), hence the split. + #[cfg(test)] + pub(super) fn now() -> std::io::Result { + let mut spec = MaybeUninit::::uninit(); + // SAFETY: valid pointer is passed to clock_gettime + crate::cutils::cerr(unsafe { + libc::clock_gettime( + if cfg!(target_os = "freebsd") { + libc::CLOCK_REALTIME + } else { + libc::CLOCK_BOOTTIME + }, + spec.as_mut_ptr(), + ) + })?; + // SAFETY: The `libc::clock_gettime` will correctly initialize `spec`, + // otherwise it will return early with the `?` operator. + let spec = unsafe { spec.assume_init() }; + + // the below conversion is not as useless as clippy thinks, on 32bit systems + #[allow(clippy::useless_conversion)] + Ok(ProcessCreateTime::new( + spec.tv_sec.into(), + spec.tv_nsec.into(), + )) + } + pub(super) fn encode(&self, target: &mut impl Write) -> std::io::Result<()> { let secs = self.secs.to_ne_bytes(); let nsecs = self.nsecs.to_ne_bytes(); @@ -178,10 +212,7 @@ mod tests { use crate::system::{Process, WithProcess}; let time = Process::starting_time(WithProcess::Current).unwrap(); - let now = { - let super::SystemTime { secs, nsecs } = super::SystemTime::now().unwrap(); - super::ProcessCreateTime { secs, nsecs } - }; + let now = super::ProcessCreateTime::now().unwrap(); assert!(time.secs > now.secs - 24 * 60 * 60); assert!(time < now);