Fix uptime on macOS using sysctl kern.boottime fallback (#8908)

* fix(uucore): use sysctl kern.boottime on macOS as fallback for uptime

If utmpx BOOT_TIME is unavailable, derive boot time via sysctl CTL_KERN.KERN_BOOTTIME to reduce intermittent macOS failures (e.g., #3621).

Context (blame/history):

- 2774274cc2 ("uptime: Support files in uptime (#6400)"): added macOS utmpxname validation and non-fatal 'unknown uptime' fallback with tests (tests/by-util/test_uptime.rs).

- 920d29f703 ("uptime: add support for OpenBSD using utmp"): reorganized uptime.rs and solidified utmp/utmpx-driven paths.

* test: add comprehensive macOS tests for sysctl kern.boottime fallback

Add unit tests for sysctl boottime availability and get_uptime reliability on macOS, verifying the fallback mechanism works correctly when utmpx BOOT_TIME is unavailable.

Add integration tests to ensure uptime command consistently succeeds on macOS with various flags (default, --since) and produces properly formatted output.

Enhance documentation of the sysctl fallback code with detailed comments explaining why it exists, the issue it addresses (#3621), and comprehensive SAFETY comments for the unsafe sysctl call.

All tests are properly gated with #[cfg(target_os = "macos")] to ensure they only run on macOS and don't interfere with other platforms.

* refactor(uucore): replace unsafe sysctl with safe command-line approach for macOS boot time

- Remove unsafe libc::sysctl() system call entirely
- Replace with safe std::process::Command executing 'sysctl -n kern.boottime'
- Parse sysctl output format to extract boot time seconds
- Maintains same API and functionality while eliminating unsafe blocks
- Addresses reviewer feedback to completely remove unsafe code
This commit is contained in:
Cả thế giới là Rust
2025-12-26 07:11:37 +07:00
committed by GitHub
parent 6c5fae7a48
commit 3e71f638bc
2 changed files with 217 additions and 4 deletions
+140 -3
View File
@@ -3,7 +3,7 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore gettime BOOTTIME clockid boottime nusers loadavg getloadavg
// spell-checker:ignore gettime BOOTTIME clockid boottime nusers loadavg getloadavg timeval
//! Provides functions to get system uptime, number of users and load average.
@@ -41,6 +41,48 @@ pub fn get_formatted_time() -> String {
Local::now().time().format("%H:%M:%S").to_string()
}
/// Safely get macOS boot time using sysctl command
///
/// This function uses the sysctl command-line tool to retrieve the kernel
/// boot time on macOS, avoiding any unsafe code. It parses the output
/// of the sysctl command to extract the boot time.
///
/// # Returns
///
/// Returns Some(time_t) if successful, None if the call fails.
#[cfg(target_os = "macos")]
fn get_macos_boot_time_sysctl() -> Option<time_t> {
use std::process::Command;
// Execute sysctl command to get boot time
let output = Command::new("sysctl")
.arg("-n")
.arg("kern.boottime")
.output();
if let Ok(output) = output {
if output.status.success() {
// Parse output format: { sec = 1729338352, usec = 0 } Wed Oct 19 08:25:52 2025
// We need to extract the seconds value from the structured output
let stdout = String::from_utf8_lossy(&output.stdout);
// Extract the seconds from the output
// Look for "sec = " pattern
if let Some(sec_start) = stdout.find("sec = ") {
let sec_part = &stdout[sec_start + 6..];
if let Some(sec_end) = sec_part.find(',') {
let sec_str = &sec_part[..sec_end];
if let Ok(boot_time) = sec_str.trim().parse::<i64>() {
return Some(boot_time as time_t);
}
}
}
}
}
None
}
/// Get the system uptime
///
/// # Arguments
@@ -107,7 +149,8 @@ pub fn get_uptime(boot_time: Option<time_t>) -> UResult<i64> {
return Ok(uptime);
}
let boot_time = boot_time.or_else(|| {
// Try provided boot_time or derive from utmpx
let derived_boot_time = boot_time.or_else(|| {
let records = Utmpx::iter_all_records();
for line in records {
match line.record_type() {
@@ -123,7 +166,27 @@ pub fn get_uptime(boot_time: Option<time_t>) -> UResult<i64> {
None
});
if let Some(t) = boot_time {
// macOS-specific fallback: use sysctl kern.boottime when utmpx did not provide BOOT_TIME
//
// On macOS, the utmpx BOOT_TIME record can be unreliable or absent, causing intermittent
// test failures (see issue #3621: https://github.com/uutils/coreutils/issues/3621).
// The sysctl(CTL_KERN, KERN_BOOTTIME) approach is the canonical way to retrieve boot time
// on macOS and is always available, making uptime more reliable on this platform.
//
// This fallback only runs if utmpx failed to provide a boot time.
#[cfg(target_os = "macos")]
let derived_boot_time = {
let mut t = derived_boot_time;
if t.is_none() {
// Use a safe wrapper function to get boot time via sysctl
if let Some(boot_time) = get_macos_boot_time_sysctl() {
t = Some(boot_time);
}
}
t
};
if let Some(t) = derived_boot_time {
let now = Local::now().timestamp();
#[cfg(target_pointer_width = "64")]
let boottime: i64 = t;
@@ -386,4 +449,78 @@ mod tests {
assert_eq!("1 user", format_nusers(1));
assert_eq!("2 users", format_nusers(2));
}
/// Test that sysctl kern.boottime is accessible on macOS and returns valid boot time.
/// This ensures the fallback mechanism added for issue #3621 works correctly.
#[test]
#[cfg(target_os = "macos")]
fn test_macos_sysctl_boottime_available() {
// Test the safe wrapper function
let boot_time = get_macos_boot_time_sysctl();
// Verify the safe wrapper succeeded
assert!(
boot_time.is_some(),
"get_macos_boot_time_sysctl should succeed on macOS"
);
let boot_time = boot_time.unwrap();
// Verify boot time is valid (positive, reasonable value)
assert!(boot_time > 0, "Boot time should be positive");
// Boot time should be after 2000-01-01 (946684800 seconds since epoch)
assert!(boot_time > 946684800, "Boot time should be after year 2000");
// Boot time should be before current time
let now = chrono::Local::now().timestamp();
assert!(
(boot_time as i64) < now,
"Boot time should be before current time"
);
}
/// Test that get_uptime always succeeds on macOS due to sysctl fallback.
/// This addresses the intermittent failures reported in issue #3621.
#[test]
#[cfg(target_os = "macos")]
fn test_get_uptime_always_succeeds_on_macos() {
// Call get_uptime without providing boot_time, forcing the system
// to use utmpx or fall back to sysctl
let result = get_uptime(None);
assert!(
result.is_ok(),
"get_uptime should always succeed on macOS with sysctl fallback"
);
let uptime = result.unwrap();
assert!(uptime > 0, "Uptime should be positive");
// Reasonable upper bound: system hasn't been up for more than 365 days
// (This is just a sanity check)
assert!(
uptime < 365 * 86400,
"Uptime seems unreasonably high: {} seconds",
uptime
);
}
/// Test get_uptime consistency by calling it multiple times.
/// Verifies the sysctl fallback produces stable results.
#[test]
#[cfg(target_os = "macos")]
fn test_get_uptime_macos_consistency() {
let uptime1 = get_uptime(None).expect("First call should succeed");
let uptime2 = get_uptime(None).expect("Second call should succeed");
// Uptimes should be very close (within 1 second)
let diff = (uptime1 - uptime2).abs();
assert!(
diff <= 1,
"Consecutive uptime calls should be consistent, got {} and {}",
uptime1,
uptime2
);
}
}
+77 -1
View File
@@ -3,7 +3,7 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//
// spell-checker:ignore bincode serde utmp runlevel testusr testx
// spell-checker:ignore bincode serde utmp runlevel testusr testx boottime
#![allow(clippy::cast_possible_wrap, clippy::unreadable_literal)]
use uutests::at_and_ucmd;
@@ -269,3 +269,79 @@ fn test_uptime_since() {
new_ucmd!().arg("--since").succeeds().stdout_matches(&re);
}
/// Test uptime reliability on macOS with sysctl kern.boottime fallback.
/// This addresses intermittent failures from issue #3621 by ensuring
/// the command consistently succeeds when utmpx data is unavailable.
#[test]
#[cfg(target_os = "macos")]
fn test_uptime_macos_reliability() {
// Run uptime multiple times to ensure consistent success
// (Previously would fail intermittently when utmpx had no BOOT_TIME)
for i in 0..5 {
let result = new_ucmd!().succeeds();
// Verify standard output patterns
result
.stdout_contains("up")
.stdout_contains("load average:");
// Ensure no error about retrieving system uptime
let stderr = result.stderr_str();
assert!(
!stderr.contains("could not retrieve system uptime"),
"Iteration {i}: uptime should not fail on macOS (stderr: {stderr})"
);
}
}
/// Test uptime --since reliability on macOS.
/// Verifies the sysctl fallback works for the --since flag.
#[test]
#[cfg(target_os = "macos")]
fn test_uptime_since_macos() {
let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}").unwrap();
// Run multiple times to ensure consistency
for i in 0..3 {
let result = new_ucmd!().arg("--since").succeeds();
result.stdout_matches(&re);
// Ensure no error messages
let stderr = result.stderr_str();
assert!(
stderr.is_empty(),
"Iteration {i}: uptime --since should not produce stderr on macOS (stderr: {stderr})"
);
}
}
/// Test that uptime output format is consistent on macOS.
/// Ensures the sysctl fallback produces properly formatted output.
#[test]
#[cfg(target_os = "macos")]
fn test_uptime_macos_output_format() {
let result = new_ucmd!().succeeds();
let stdout = result.stdout_str();
// Verify time is present (format: HH:MM:SS)
let time_re = Regex::new(r"\d{2}:\d{2}:\d{2}").unwrap();
assert!(
time_re.is_match(stdout),
"Output should contain time in HH:MM:SS format: {stdout}"
);
// Verify uptime format (either "HH:MM" or "X days HH:MM")
assert!(
stdout.contains(" up "),
"Output should contain 'up': {stdout}"
);
// Verify load average is present
let load_re = Regex::new(r"load average: \d+\.\d+, \d+\.\d+, \d+\.\d+").unwrap();
assert!(
load_re.is_match(stdout),
"Output should contain load average: {stdout}"
);
}