Skip to content

Commit 3e71f63

Browse files
authored
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): - 2774274 ("uptime: Support files in uptime (#6400)"): added macOS utmpxname validation and non-fatal 'unknown uptime' fallback with tests (tests/by-util/test_uptime.rs). - 920d29f ("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
1 parent 6c5fae7 commit 3e71f63

File tree

2 files changed

+217
-4
lines changed

2 files changed

+217
-4
lines changed

src/uucore/src/lib/features/uptime.rs

Lines changed: 140 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55

6-
// spell-checker:ignore gettime BOOTTIME clockid boottime nusers loadavg getloadavg
6+
// spell-checker:ignore gettime BOOTTIME clockid boottime nusers loadavg getloadavg timeval
77

88
//! Provides functions to get system uptime, number of users and load average.
99
@@ -41,6 +41,48 @@ pub fn get_formatted_time() -> String {
4141
Local::now().time().format("%H:%M:%S").to_string()
4242
}
4343

44+
/// Safely get macOS boot time using sysctl command
45+
///
46+
/// This function uses the sysctl command-line tool to retrieve the kernel
47+
/// boot time on macOS, avoiding any unsafe code. It parses the output
48+
/// of the sysctl command to extract the boot time.
49+
///
50+
/// # Returns
51+
///
52+
/// Returns Some(time_t) if successful, None if the call fails.
53+
#[cfg(target_os = "macos")]
54+
fn get_macos_boot_time_sysctl() -> Option<time_t> {
55+
use std::process::Command;
56+
57+
// Execute sysctl command to get boot time
58+
let output = Command::new("sysctl")
59+
.arg("-n")
60+
.arg("kern.boottime")
61+
.output();
62+
63+
if let Ok(output) = output {
64+
if output.status.success() {
65+
// Parse output format: { sec = 1729338352, usec = 0 } Wed Oct 19 08:25:52 2025
66+
// We need to extract the seconds value from the structured output
67+
let stdout = String::from_utf8_lossy(&output.stdout);
68+
69+
// Extract the seconds from the output
70+
// Look for "sec = " pattern
71+
if let Some(sec_start) = stdout.find("sec = ") {
72+
let sec_part = &stdout[sec_start + 6..];
73+
if let Some(sec_end) = sec_part.find(',') {
74+
let sec_str = &sec_part[..sec_end];
75+
if let Ok(boot_time) = sec_str.trim().parse::<i64>() {
76+
return Some(boot_time as time_t);
77+
}
78+
}
79+
}
80+
}
81+
}
82+
83+
None
84+
}
85+
4486
/// Get the system uptime
4587
///
4688
/// # Arguments
@@ -107,7 +149,8 @@ pub fn get_uptime(boot_time: Option<time_t>) -> UResult<i64> {
107149
return Ok(uptime);
108150
}
109151

110-
let boot_time = boot_time.or_else(|| {
152+
// Try provided boot_time or derive from utmpx
153+
let derived_boot_time = boot_time.or_else(|| {
111154
let records = Utmpx::iter_all_records();
112155
for line in records {
113156
match line.record_type() {
@@ -123,7 +166,27 @@ pub fn get_uptime(boot_time: Option<time_t>) -> UResult<i64> {
123166
None
124167
});
125168

126-
if let Some(t) = boot_time {
169+
// macOS-specific fallback: use sysctl kern.boottime when utmpx did not provide BOOT_TIME
170+
//
171+
// On macOS, the utmpx BOOT_TIME record can be unreliable or absent, causing intermittent
172+
// test failures (see issue #3621: https://github.com/uutils/coreutils/issues/3621).
173+
// The sysctl(CTL_KERN, KERN_BOOTTIME) approach is the canonical way to retrieve boot time
174+
// on macOS and is always available, making uptime more reliable on this platform.
175+
//
176+
// This fallback only runs if utmpx failed to provide a boot time.
177+
#[cfg(target_os = "macos")]
178+
let derived_boot_time = {
179+
let mut t = derived_boot_time;
180+
if t.is_none() {
181+
// Use a safe wrapper function to get boot time via sysctl
182+
if let Some(boot_time) = get_macos_boot_time_sysctl() {
183+
t = Some(boot_time);
184+
}
185+
}
186+
t
187+
};
188+
189+
if let Some(t) = derived_boot_time {
127190
let now = Local::now().timestamp();
128191
#[cfg(target_pointer_width = "64")]
129192
let boottime: i64 = t;
@@ -386,4 +449,78 @@ mod tests {
386449
assert_eq!("1 user", format_nusers(1));
387450
assert_eq!("2 users", format_nusers(2));
388451
}
452+
453+
/// Test that sysctl kern.boottime is accessible on macOS and returns valid boot time.
454+
/// This ensures the fallback mechanism added for issue #3621 works correctly.
455+
#[test]
456+
#[cfg(target_os = "macos")]
457+
fn test_macos_sysctl_boottime_available() {
458+
// Test the safe wrapper function
459+
let boot_time = get_macos_boot_time_sysctl();
460+
461+
// Verify the safe wrapper succeeded
462+
assert!(
463+
boot_time.is_some(),
464+
"get_macos_boot_time_sysctl should succeed on macOS"
465+
);
466+
467+
let boot_time = boot_time.unwrap();
468+
469+
// Verify boot time is valid (positive, reasonable value)
470+
assert!(boot_time > 0, "Boot time should be positive");
471+
472+
// Boot time should be after 2000-01-01 (946684800 seconds since epoch)
473+
assert!(boot_time > 946684800, "Boot time should be after year 2000");
474+
475+
// Boot time should be before current time
476+
let now = chrono::Local::now().timestamp();
477+
assert!(
478+
(boot_time as i64) < now,
479+
"Boot time should be before current time"
480+
);
481+
}
482+
483+
/// Test that get_uptime always succeeds on macOS due to sysctl fallback.
484+
/// This addresses the intermittent failures reported in issue #3621.
485+
#[test]
486+
#[cfg(target_os = "macos")]
487+
fn test_get_uptime_always_succeeds_on_macos() {
488+
// Call get_uptime without providing boot_time, forcing the system
489+
// to use utmpx or fall back to sysctl
490+
let result = get_uptime(None);
491+
492+
assert!(
493+
result.is_ok(),
494+
"get_uptime should always succeed on macOS with sysctl fallback"
495+
);
496+
497+
let uptime = result.unwrap();
498+
assert!(uptime > 0, "Uptime should be positive");
499+
500+
// Reasonable upper bound: system hasn't been up for more than 365 days
501+
// (This is just a sanity check)
502+
assert!(
503+
uptime < 365 * 86400,
504+
"Uptime seems unreasonably high: {} seconds",
505+
uptime
506+
);
507+
}
508+
509+
/// Test get_uptime consistency by calling it multiple times.
510+
/// Verifies the sysctl fallback produces stable results.
511+
#[test]
512+
#[cfg(target_os = "macos")]
513+
fn test_get_uptime_macos_consistency() {
514+
let uptime1 = get_uptime(None).expect("First call should succeed");
515+
let uptime2 = get_uptime(None).expect("Second call should succeed");
516+
517+
// Uptimes should be very close (within 1 second)
518+
let diff = (uptime1 - uptime2).abs();
519+
assert!(
520+
diff <= 1,
521+
"Consecutive uptime calls should be consistent, got {} and {}",
522+
uptime1,
523+
uptime2
524+
);
525+
}
389526
}

tests/by-util/test_uptime.rs

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55
//
6-
// spell-checker:ignore bincode serde utmp runlevel testusr testx
6+
// spell-checker:ignore bincode serde utmp runlevel testusr testx boottime
77
#![allow(clippy::cast_possible_wrap, clippy::unreadable_literal)]
88

99
use uutests::at_and_ucmd;
@@ -269,3 +269,79 @@ fn test_uptime_since() {
269269

270270
new_ucmd!().arg("--since").succeeds().stdout_matches(&re);
271271
}
272+
273+
/// Test uptime reliability on macOS with sysctl kern.boottime fallback.
274+
/// This addresses intermittent failures from issue #3621 by ensuring
275+
/// the command consistently succeeds when utmpx data is unavailable.
276+
#[test]
277+
#[cfg(target_os = "macos")]
278+
fn test_uptime_macos_reliability() {
279+
// Run uptime multiple times to ensure consistent success
280+
// (Previously would fail intermittently when utmpx had no BOOT_TIME)
281+
for i in 0..5 {
282+
let result = new_ucmd!().succeeds();
283+
284+
// Verify standard output patterns
285+
result
286+
.stdout_contains("up")
287+
.stdout_contains("load average:");
288+
289+
// Ensure no error about retrieving system uptime
290+
let stderr = result.stderr_str();
291+
assert!(
292+
!stderr.contains("could not retrieve system uptime"),
293+
"Iteration {i}: uptime should not fail on macOS (stderr: {stderr})"
294+
);
295+
}
296+
}
297+
298+
/// Test uptime --since reliability on macOS.
299+
/// Verifies the sysctl fallback works for the --since flag.
300+
#[test]
301+
#[cfg(target_os = "macos")]
302+
fn test_uptime_since_macos() {
303+
let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}").unwrap();
304+
305+
// Run multiple times to ensure consistency
306+
for i in 0..3 {
307+
let result = new_ucmd!().arg("--since").succeeds();
308+
309+
result.stdout_matches(&re);
310+
311+
// Ensure no error messages
312+
let stderr = result.stderr_str();
313+
assert!(
314+
stderr.is_empty(),
315+
"Iteration {i}: uptime --since should not produce stderr on macOS (stderr: {stderr})"
316+
);
317+
}
318+
}
319+
320+
/// Test that uptime output format is consistent on macOS.
321+
/// Ensures the sysctl fallback produces properly formatted output.
322+
#[test]
323+
#[cfg(target_os = "macos")]
324+
fn test_uptime_macos_output_format() {
325+
let result = new_ucmd!().succeeds();
326+
let stdout = result.stdout_str();
327+
328+
// Verify time is present (format: HH:MM:SS)
329+
let time_re = Regex::new(r"\d{2}:\d{2}:\d{2}").unwrap();
330+
assert!(
331+
time_re.is_match(stdout),
332+
"Output should contain time in HH:MM:SS format: {stdout}"
333+
);
334+
335+
// Verify uptime format (either "HH:MM" or "X days HH:MM")
336+
assert!(
337+
stdout.contains(" up "),
338+
"Output should contain 'up': {stdout}"
339+
);
340+
341+
// Verify load average is present
342+
let load_re = Regex::new(r"load average: \d+\.\d+, \d+\.\d+, \d+\.\d+").unwrap();
343+
assert!(
344+
load_re.is_match(stdout),
345+
"Output should contain load average: {stdout}"
346+
);
347+
}

0 commit comments

Comments
 (0)