From fb5eb9edc19ef9fe69d50383ef3a9df6d1a8e681 Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 12 Oct 2026 10:57:27 +0200 Subject: [PATCH] agents: read every MemoryMax= size systemd enforces `parse_bytes` knew only K/M/G/T, in either case. A size with the bare `B` unit (`1048576B`), which `validate_memory_max` stores and systemd enforces, became `infinity` in the agent's cap file: the cgroup was capped while the agent was told there was no cap. It now takes exactly the single-term forms systemd's `parse_size` does: no suffix, `B`, or K/M/G/T/P/E, case-sensitive. Lowercase units and `4GB`, which systemd refuses and so leaves the unit uncapped, read as `infinity` too. A test runs every suffix form the validator accepts through it and checks the stored value parses to the right byte count. Part of #5081 (slice S0-2). --- hive-c0re/src/agent_config/resource_limits.rs | 71 +++++++++++++++---- hive-c0re/src/lifecycle/host_config.rs | 4 ++ 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/hive-c0re/src/agent_config/resource_limits.rs b/hive-c0re/src/agent_config/resource_limits.rs index 4a173c89..fd68b78a 100644 --- a/hive-c0re/src/agent_config/resource_limits.rs +++ b/hive-c0re/src/agent_config/resource_limits.rs @@ -123,9 +123,12 @@ fn resolve(limits: &AgentLimits, hive_cpu_quota: &str, hive_memory_max: &str) -> } /// Parse a systemd `MemoryMax=`-style byte-size value (`"4G"`, `"512M"`, -/// a bare byte count, optionally with a decimal like `"1.5G"`) into a -/// raw byte count. systemd's `K`/`M`/`G`/`T` suffixes are IEC binary -/// (1024-based), not decimal — this matches. Pure integer arithmetic +/// `"1048576B"`, a bare byte count, optionally with a decimal like `"1.5G"`) +/// into a raw byte count, accepting exactly the single-term forms systemd's +/// `parse_size` does: a `K`/`M`/`G`/`T`/`P`/`E` suffix, IEC binary +/// (1024-based), a `B` suffix, or none. Suffixes are case-sensitive, and a +/// redundant `B` after a multiplier (`4GB`) is rejected, as systemd rejects +/// both and then leaves the unit uncapped. Pure integer arithmetic /// throughout (via `u128` headroom) rather than `f64`: a `MemoryMax=` /// value is always a small non-negative decimal (enforced by /// [`is_plain_number`] upstream in [`validate_memory_max`]), so floats @@ -137,14 +140,18 @@ pub fn parse_bytes(value: &str) -> Option { if value == "infinity" || value.ends_with('%') { return None; } - let (mantissa, exponent) = [('K', 1u32), ('M', 2), ('G', 3), ('T', 4)] - .into_iter() - .find_map(|(suffix, exp)| { - value - .strip_suffix([suffix, suffix.to_ascii_lowercase()]) - .map(|m| (m, exp)) - }) - .unwrap_or((value, 0)); + let (mantissa, exponent) = [ + ('E', 6u32), + ('P', 5), + ('T', 4), + ('G', 3), + ('M', 2), + ('K', 1), + ('B', 0), + ] + .into_iter() + .find_map(|(suffix, exp)| value.strip_suffix(suffix).map(|m| (m, exp))) + .unwrap_or((value, 0)); let scale = u128::from(1024u64.checked_pow(exponent)?); let (int_part, frac_part) = mantissa.split_once('.').unwrap_or((mantissa, "")); let whole: u128 = int_part.parse().ok()?; @@ -466,8 +473,14 @@ mod tests { parse_bytes("1.5G"), Some(1024 * 1024 * 1024 + 512 * 1024 * 1024) ); - // Lowercase suffixes accepted, matching `normalized_byte_size`. - assert_eq!(parse_bytes("4g"), Some(4 * 1024 * 1024 * 1024)); + assert_eq!(parse_bytes("1048576B"), Some(1_048_576)); + assert_eq!(parse_bytes("3P"), Some(3 << 50)); + assert_eq!(parse_bytes("2E"), Some(2 << 60)); + assert_eq!( + parse_bytes("16E"), + None, + "past u64, which systemd refuses too" + ); } #[test] @@ -478,8 +491,38 @@ mod tests { #[test] fn parse_bytes_rejects_garbage() { - for v in ["", "abc", "-8G", "8Gi"] { + // systemd's suffixes are case-sensitive and take no `B` after a + // multiplier, so it leaves a unit with any of these uncapped. + for v in [ + "", "abc", "-8G", "8Gi", "4g", "4k", "1024b", "4GB", "4MB", "B", ".5G", + ] { assert_eq!(parse_bytes(v), None, "{v} should not parse"); } } + + #[test] + fn every_size_the_validator_stores_has_a_byte_count() { + for (input, bytes) in [ + ("1048576", 1_048_576), + ("1048576B", 1_048_576), + ("1048576b", 1_048_576), + ("4096K", 4096 << 10), + ("4096kb", 4096 << 10), + ("512M", 512 << 20), + ("512mB", 512 << 20), + ("4G", 4 << 30), + ("4g", 4 << 30), + ("4GB", 4 << 30), + ("1.5G", 3 << 29), + ("2T", 2 << 40), + ("2tb", 2 << 40), + ] { + let stored = validate_memory_max(input).expect(input); + assert_eq!( + parse_bytes(&stored), + Some(bytes), + "{input} stored as {stored}" + ); + } + } } diff --git a/hive-c0re/src/lifecycle/host_config.rs b/hive-c0re/src/lifecycle/host_config.rs index 0552b1fd..008d20cf 100644 --- a/hive-c0re/src/lifecycle/host_config.rs +++ b/hive-c0re/src/lifecycle/host_config.rs @@ -947,6 +947,8 @@ mod tests { fn a_cap_is_read_the_way_systemd_reads_memory_max() { for (memory_max, cap) in [ ("4G", "4294967296"), + ("1048576B", "1048576"), + ("3P", "3377699720527872"), ("infinity", "infinity"), ("50%", "8192004096"), ("33.33%", "5460787200"), @@ -954,6 +956,8 @@ mod tests { ("100%", "16384012288"), // systemd ignores these and the unit runs uncapped. ("0", "infinity"), + ("4g", "infinity"), + ("4GB", "infinity"), ("0%", "infinity"), ("150%", "infinity"), ("12.345%", "infinity"),