From 802137757e5a9b07b0289322b4c4c8d67d92600e Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Mon, 18 May 2026 09:17:53 -0500 Subject: [PATCH 1/2] fix(date): force UTC formatting under virtual clock modes --- crates/bashkit/src/builtins/date.rs | 21 ++++++++++++--------- crates/bashkit/tests/threat_model_tests.rs | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/crates/bashkit/src/builtins/date.rs b/crates/bashkit/src/builtins/date.rs index 59e8686a..c9c34851 100644 --- a/crates/bashkit/src/builtins/date.rs +++ b/crates/bashkit/src/builtins/date.rs @@ -328,8 +328,8 @@ fn expand_nanoseconds(format: &str, nanos: u32) -> String { } /// Format an RFC 2822 date string from a UTC datetime. -fn format_rfc2822(dt: &DateTime, utc: bool) -> String { - if utc { +fn format_rfc2822(dt: &DateTime, force_utc: bool) -> String { + if force_utc { dt.format("%a, %d %b %Y %H:%M:%S +0000").to_string() } else { let local_dt: DateTime = (*dt).into(); @@ -338,10 +338,10 @@ fn format_rfc2822(dt: &DateTime, utc: bool) -> String { } /// Format an ISO 8601 date string. -fn format_iso8601(dt: &DateTime, utc: bool, precision: &str) -> String { +fn format_iso8601(dt: &DateTime, force_utc: bool, precision: &str) -> String { match precision { "hours" => { - if utc { + if force_utc { dt.format("%Y-%m-%dT%H+00:00").to_string() } else { let local_dt: DateTime = (*dt).into(); @@ -349,7 +349,7 @@ fn format_iso8601(dt: &DateTime, utc: bool, precision: &str) -> String { } } "minutes" => { - if utc { + if force_utc { dt.format("%Y-%m-%dT%H:%M+00:00").to_string() } else { let local_dt: DateTime = (*dt).into(); @@ -357,7 +357,7 @@ fn format_iso8601(dt: &DateTime, utc: bool, precision: &str) -> String { } } "seconds" | "s" => { - if utc { + if force_utc { dt.format("%Y-%m-%dT%H:%M:%S+00:00").to_string() } else { let local_dt: DateTime = (*dt).into(); @@ -460,15 +460,18 @@ impl Builtin for Date { dt_utc = now; }; + let virtualized_clock = self.fixed_epoch.is_some() || self.offset_seconds != 0; + let force_utc = utc || epoch_input || virtualized_clock; + // Handle -R (RFC 2822) output if rfc2822 { - let output = format_rfc2822(&dt_utc, utc); + let output = format_rfc2822(&dt_utc, force_utc); return Ok(ExecResult::ok(format!("{}\n", output))); } // Handle -I (ISO 8601) output if let Some(ref precision) = iso8601 { - let output = format_iso8601(&dt_utc, utc, precision); + let output = format_iso8601(&dt_utc, force_utc, precision); return Ok(ExecResult::ok(format!("{}\n", output))); } @@ -501,7 +504,7 @@ impl Builtin for Date { // Format the date, handling potential errors gracefully. let mut output = String::new(); - let format_result = if utc || epoch_input { + let format_result = if force_utc { write!(output, "{}", dt_utc.format(&format)) } else { let local_dt: DateTime = dt_utc.into(); diff --git a/crates/bashkit/tests/threat_model_tests.rs b/crates/bashkit/tests/threat_model_tests.rs index 36164d9c..c0b04ea5 100644 --- a/crates/bashkit/tests/threat_model_tests.rs +++ b/crates/bashkit/tests/threat_model_tests.rs @@ -3810,6 +3810,24 @@ mod tm_inf_018_date { ); } + + /// TM-INF-018: virtual clock modes must not leak host timezone via custom formats. + #[tokio::test] + async fn fixed_epoch_forces_utc_for_timezone_formats() { + let mut bash = Bash::builder().fixed_epoch(1_700_000_000).build(); + let r = bash.exec("date +%z").await.unwrap(); + assert_eq!(r.exit_code, 0); + assert_eq!(r.stdout.trim(), "+0000"); + } + + /// TM-INF-018: epoch_offset mode must not leak host timezone via RFC 2822 output. + #[tokio::test] + async fn epoch_offset_forces_utc_for_rfc2822() { + let mut bash = Bash::builder().epoch_offset(86_400).build(); + let r = bash.exec("date -R").await.unwrap(); + assert_eq!(r.exit_code, 0); + assert!(r.stdout.trim_end().ends_with(" +0000")); + } /// TM-INF-018: `fixed_epoch` and `epoch_offset` are mutually /// exclusive — last builder call wins. fixed_epoch followed by /// epoch_offset should disable fixed_epoch. From 4f18b49006b85ed913169365c5d1dae016ff76f8 Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Tue, 19 May 2026 09:10:43 +0000 Subject: [PATCH 2/2] fix(date): apply rustfmt to TM-INF-018 tests Remove extra blank line introduced before the new TM-INF-018 test function so `cargo fmt --check` passes in CI. --- crates/bashkit/tests/threat_model_tests.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bashkit/tests/threat_model_tests.rs b/crates/bashkit/tests/threat_model_tests.rs index c0b04ea5..a74abd4e 100644 --- a/crates/bashkit/tests/threat_model_tests.rs +++ b/crates/bashkit/tests/threat_model_tests.rs @@ -3810,7 +3810,6 @@ mod tm_inf_018_date { ); } - /// TM-INF-018: virtual clock modes must not leak host timezone via custom formats. #[tokio::test] async fn fixed_epoch_forces_utc_for_timezone_formats() {