diff --git a/CHANGELOG.md b/CHANGELOG.md index 047c9e72c..c926cb8e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Real loopback TCP integration proof plus deterministic timeout, refusal, retry, peer-inspection, peer-mismatch, canonicalization, IPv6 metadata, and single-use replay tests. - Real loopback rustls integration covering trusted DNS SAN, Common-Name fallback rejection, wrong-name and untrusted-root rejection, fixed-time expiry and not-yet-valid failures, exact IPv4 and IPv6 SANs, TLS 1.2/TLS 1.3, required and optional ALPN, and transport-origin binding. - Cumulative interactive-first RAM, VRAM, batch, local-model, admission, pause, and compositor-pressure mitigation plans, including active-consumer reduction at exact hard limits. -- Bounded browser-task runtime telemetry that validates platform-supplied RSS, semantic-observation bytes, governed-action latency, and total task duration; can conservatively feed RSS into the resource governor; and can sample one explicitly supplied Linux process ID from `/proc//status` with strict `VmRSS` syntax, unit, duplicate, overflow, and read-failure handling (Linux Kernel Documentation, n.d.). The sampler performs no Chromium process discovery, child-process aggregation, cgroup accounting, GPU/heap measurement, or cross-platform sampling. +- Bounded browser-task runtime telemetry that validates platform-supplied RSS, semantic-observation bytes, governed-action latency, and total task duration; conservatively feeds supplied RSS into the resource governor; samples one explicitly supplied Linux process ID from `/proc//status` with strict `VmRSS` syntax, unit, duplicate, overflow, and read-failure handling; and aggregates or samples an explicit process set of at most 256 unique nonzero PIDs with checked RSS addition and fail-closed all-member sampling (Linux Kernel Documentation, n.d.). Identity-bound RSS sampling verifies the Linux PID plus `/proc//stat` start time before reading process status and re-verifies it afterward, so stale identities cannot authorize inspection of a reused PID and cross-file PID reuse invalidates the measurement. These adapters perform no Chromium process discovery, child-process/cgroup attribution, GPU/heap measurement, or cross-platform sampling. - Universally value-redacted network evidence with explicit path, metadata, and provenance bounds; ambiguous path rejection; validated source URLs; lowercase SHA-256 identifiers; and verification state. - Rust 1.97.1 build contract, strict Clippy and rustdoc gates, and exact production function, line, region, and branch coverage enforcement. - Hourly bounded OpenCode product-development workflow using `NVIDIA_NIM_API_KEY`, an unprivileged disposable workspace, loopback-only model broker, independently verified patches, and publication through a dedicated `OPENCODE_PR_TOKEN` that cannot review or merge. @@ -94,4 +94,4 @@ All notable changes to OriginWeave are documented in this file. The format follo Linux Kernel Documentation. (n.d.). *The /proc filesystem*. Retrieved August 15, 2026, from https://www.kernel.org/doc/html/latest/filesystems/proc.html -[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD \ No newline at end of file +[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD diff --git a/crates/originweave-resource/src/lib.rs b/crates/originweave-resource/src/lib.rs index 2908cddd8..22a2f5468 100644 --- a/crates/originweave-resource/src/lib.rs +++ b/crates/originweave-resource/src/lib.rs @@ -11,6 +11,9 @@ const MEBIBYTE_BYTES: u64 = 1_048_576; +/// Maximum number of explicitly attributed browser processes in one RSS sample set. +pub const MAX_BROWSER_PROCESS_SET_SIZE: usize = 256; + const fn bytes_to_mebibytes_ceil(bytes: u64) -> u64 { let whole_mebibytes = bytes / MEBIBYTE_BYTES; if bytes.is_multiple_of(MEBIBYTE_BYTES) { @@ -402,6 +405,20 @@ pub enum BrowserTaskTelemetryError { pub enum BrowserRssSampleError { /// Process identifiers are one-based and zero was supplied. InvalidProcessId, + /// No browser process was supplied to a process-set measurement. + EmptyProcessSet, + /// A process identifier appeared more than once and would be double-counted. + DuplicateProcessId, + /// The caller supplied more process identifiers than the bounded set permits. + ProcessSetTooLarge, + /// Summing bounded process resident-set sizes would overflow `u64`. + ProcessSetRssOverflow, + /// The Linux process stat file could not be read at the identity boundary. + ProcessStatUnavailable, + /// The Linux process stat record was malformed or lacked its start-time field. + InvalidProcessStat, + /// The PID no longer refers to the kernel process instance bound by the identity. + ProcessIdentityChanged, /// The process status file could not be read at the sampling boundary. ProcessStatusUnavailable, /// The Linux process status did not contain a resident-set-size field. @@ -418,6 +435,204 @@ pub enum BrowserRssSampleError { UnsupportedPlatform, } +fn validate_browser_process_set_size(process_count: usize) -> Result<(), BrowserRssSampleError> { + if process_count == 0 { + return Err(BrowserRssSampleError::EmptyProcessSet); + } + if process_count > MAX_BROWSER_PROCESS_SET_SIZE { + return Err(BrowserRssSampleError::ProcessSetTooLarge); + } + Ok(()) +} + +fn validate_browser_process_ids(process_ids: &[u32]) -> Result<(), BrowserRssSampleError> { + for (index, process_id) in process_ids.iter().copied().enumerate() { + if process_id == 0 { + return Err(BrowserRssSampleError::InvalidProcessId); + } + if process_ids[..index].contains(&process_id) { + return Err(BrowserRssSampleError::DuplicateProcessId); + } + } + Ok(()) +} + +/// A Linux PID bound to the kernel start-time tick recorded in `/proc//stat`. +/// +/// A PID by itself is reusable and therefore insufficient as long-lived browser +/// process authority. This value pairs the caller-attributed PID with Linux +/// field 22 (`starttime`) so RSS sampling can reject a PID that has been reused +/// for a different process instance. It does not prove Chromium/task ownership. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LinuxProcessIdentity { + process_id: u32, + start_time_ticks: u64, +} + +impl LinuxProcessIdentity { + /// Construct one process identity from a nonzero PID and kernel start-time tick. + pub const fn new( + process_id: u32, + start_time_ticks: u64, + ) -> Result { + if process_id == 0 { + return Err(BrowserRssSampleError::InvalidProcessId); + } + Ok(Self { + process_id, + start_time_ticks, + }) + } + + /// Return the Linux process identifier bound by this identity. + #[must_use] + pub const fn process_id(self) -> u32 { + self.process_id + } + + /// Return Linux `/proc//stat` field 22 in clock ticks since boot. + #[must_use] + pub const fn start_time_ticks(self) -> u64 { + self.start_time_ticks + } +} + +fn parse_linux_proc_stat_identity(stat: &str) -> Result<(u32, u64), BrowserRssSampleError> { + let Some(open_comm_index) = stat.find(" (") else { + return Err(BrowserRssSampleError::InvalidProcessStat); + }; + let Some(close_comm_index) = stat.rfind(") ") else { + return Err(BrowserRssSampleError::InvalidProcessStat); + }; + if close_comm_index <= open_comm_index + 1 { + return Err(BrowserRssSampleError::InvalidProcessStat); + } + + let process_id_text = &stat[..open_comm_index]; + if process_id_text.is_empty() || !process_id_text.bytes().all(|byte| byte.is_ascii_digit()) { + return Err(BrowserRssSampleError::InvalidProcessStat); + } + let process_id = process_id_text + .parse::() + .map_err(|_error| BrowserRssSampleError::InvalidProcessStat)?; + if process_id == 0 { + return Err(BrowserRssSampleError::InvalidProcessStat); + } + + let mut fields_after_comm = stat[close_comm_index + 2..].split_whitespace(); + let Some(_state) = fields_after_comm.next() else { + return Err(BrowserRssSampleError::InvalidProcessStat); + }; + let Some(start_time_text) = fields_after_comm.nth(18) else { + return Err(BrowserRssSampleError::InvalidProcessStat); + }; + if !start_time_text.bytes().all(|byte| byte.is_ascii_digit()) { + return Err(BrowserRssSampleError::InvalidProcessStat); + } + let start_time_ticks = start_time_text + .parse::() + .map_err(|_error| BrowserRssSampleError::InvalidProcessStat)?; + Ok((process_id, start_time_ticks)) +} + +/// Parse Linux `/proc//stat` field 22 (`starttime`) in kernel clock ticks. +/// +/// The command name is parenthesized and may itself contain spaces or closing +/// parentheses, so this parser anchors on the final `") "` delimiter instead +/// of splitting the complete record on whitespace. Malformed or truncated +/// records fail closed rather than producing a reusable PID-only identity. +pub fn parse_linux_proc_stat_start_time_ticks(stat: &str) -> Result { + parse_linux_proc_stat_identity(stat).map(|(_process_id, start_time_ticks)| start_time_ticks) +} + +/// Parse one Linux stat record and bind it to the caller-requested process identifier. +/// +/// The stat record must be structurally valid and its embedded PID must equal +/// `process_id`; otherwise this boundary fails closed. It performs no process +/// discovery and does not prove Chromium/task ownership. +pub fn parse_linux_process_identity( + process_id: u32, + stat: &str, +) -> Result { + if process_id == 0 { + return Err(BrowserRssSampleError::InvalidProcessId); + } + let (observed_process_id, start_time_ticks) = parse_linux_proc_stat_identity(stat)?; + if observed_process_id != process_id { + return Err(BrowserRssSampleError::ProcessIdentityChanged); + } + Ok(LinuxProcessIdentity { + process_id, + start_time_ticks, + }) +} + +/// Verify that one Linux stat record still represents the supplied process identity. +/// +/// Both PID and kernel start time must match. A syntactically valid record for a +/// different process instance returns [`BrowserRssSampleError::ProcessIdentityChanged`]. +pub fn verify_linux_process_identity( + identity: LinuxProcessIdentity, + stat: &str, +) -> Result<(), BrowserRssSampleError> { + let (process_id, start_time_ticks) = parse_linux_proc_stat_identity(stat)?; + if process_id != identity.process_id || start_time_ticks != identity.start_time_ticks { + return Err(BrowserRssSampleError::ProcessIdentityChanged); + } + Ok(()) +} + +/// Read the current Linux kernel identity for one explicit process identifier. +/// +/// This function reads only `/proc//stat`; it does not discover processes +/// or establish Chromium/task ownership. Non-Linux platforms fail closed. +pub fn read_linux_process_identity( + process_id: u32, +) -> Result { + if process_id == 0 { + return Err(BrowserRssSampleError::InvalidProcessId); + } + + #[cfg(target_os = "linux")] + { + let stat = std::fs::read_to_string(format!("/proc/{process_id}/stat")) + .map_err(|_error| BrowserRssSampleError::ProcessStatUnavailable)?; + parse_linux_process_identity(process_id, &stat) + } + + #[cfg(not(target_os = "linux"))] + { + let _ = process_id; + Err(BrowserRssSampleError::UnsupportedPlatform) + } +} + +/// Aggregate exact caller-supplied process RSS samples without double-counting. +/// +/// The input is deliberately an explicit bounded process set rather than a +/// discovered browser tree. Process identifiers must be nonzero and unique, +/// and byte totals use checked addition so an overflow cannot be misreported as +/// a smaller task. This function does not prove that any process belongs to +/// Chromium or to the same Agent Task. +pub fn aggregate_browser_process_rss_samples( + samples: &[(u32, u64)], +) -> Result { + validate_browser_process_set_size(samples.len())?; + let process_ids: Vec = samples + .iter() + .map(|(process_id, _rss_bytes)| *process_id) + .collect(); + validate_browser_process_ids(&process_ids)?; + + let mut total_rss_bytes = 0_u64; + for (_process_id, rss_bytes) in samples { + total_rss_bytes = total_rss_bytes + .checked_add(*rss_bytes) + .ok_or(BrowserRssSampleError::ProcessSetRssOverflow)?; + } + Ok(total_rss_bytes) +} + /// Parse Linux `/proc//status` and return the exact `VmRSS` value in bytes. /// /// Linux reports `VmRSS` in `kB`, where the kernel ABI uses 1024-byte units. @@ -487,3 +702,81 @@ pub fn sample_linux_process_rss_bytes(process_id: u32) -> Result Result<(), BrowserRssSampleError> { + let current_identity = read_linux_process_identity(identity.process_id)?; + if current_identity != identity { + return Err(BrowserRssSampleError::ProcessIdentityChanged); + } + Ok(()) +} + +/// Sample one Linux process RSS only while its PID names the bound process instance. +/// +/// The sampler checks `/proc//stat` before reading `/proc//status` so +/// a stale caller identity cannot authorize inspection of a reused PID. It then +/// checks the kernel identity again after the RSS read; any disappearance or PID +/// reuse during the cross-file sample invalidates the measurement. The function +/// never turns this operating-system identity into Chromium/task ownership authority. +pub fn sample_linux_process_identity_rss_bytes( + identity: LinuxProcessIdentity, +) -> Result { + ensure_linux_process_identity_current(identity)?; + sample_linux_process_rss_bytes(identity.process_id) + .and_then(|rss_bytes| ensure_linux_process_identity_current(identity).map(|()| rss_bytes)) +} + +/// Sample the aggregate RSS of one explicit bounded Linux process-identity set. +/// +/// Every PID must be nonzero and unique. Each member is sampled through the +/// PID-plus-start-time identity check before aggregation, so one stale/reused +/// PID fails the complete measurement rather than contributing ambiguous RSS. +pub fn sample_linux_process_identity_set_rss_bytes( + identities: &[LinuxProcessIdentity], +) -> Result { + validate_browser_process_set_size(identities.len())?; + let process_ids: Vec = identities + .iter() + .map(|identity| identity.process_id) + .collect(); + validate_browser_process_ids(&process_ids)?; + + let mut samples = Vec::with_capacity(identities.len()); + for identity in identities { + let rss_bytes = sample_linux_process_identity_rss_bytes(*identity)?; + samples.push((identity.process_id, rss_bytes)); + } + aggregate_browser_process_rss_samples(&samples) +} + +/// Sample the aggregate RSS of one explicit bounded Linux process set. +/// +/// The caller owns process discovery and attribution. This function validates +/// the complete supplied set before any operating-system read, samples every +/// exact member, fails closed if any member cannot be sampled, and returns no +/// partial total. It does not walk child processes or cgroups and does not prove +/// that the supplied identifiers belong to Chromium or to the same Agent Task. +pub fn sample_linux_process_set_rss_bytes( + process_ids: &[u32], +) -> Result { + validate_browser_process_set_size(process_ids.len())?; + validate_browser_process_ids(process_ids)?; + + #[cfg(target_os = "linux")] + { + let mut samples = Vec::with_capacity(process_ids.len()); + for process_id in process_ids { + let rss_bytes = sample_linux_process_rss_bytes(*process_id)?; + samples.push((*process_id, rss_bytes)); + } + aggregate_browser_process_rss_samples(&samples) + } + + #[cfg(not(target_os = "linux"))] + { + let _ = process_ids; + Err(BrowserRssSampleError::UnsupportedPlatform) + } +} diff --git a/crates/originweave-resource/tests/browser_process_identity_rss.rs b/crates/originweave-resource/tests/browser_process_identity_rss.rs new file mode 100644 index 000000000..f97a559cd --- /dev/null +++ b/crates/originweave-resource/tests/browser_process_identity_rss.rs @@ -0,0 +1,190 @@ +use originweave_resource::{ + BrowserRssSampleError, LinuxProcessIdentity, parse_linux_proc_stat_start_time_ticks, + parse_linux_process_identity, read_linux_process_identity, + sample_linux_process_identity_rss_bytes, sample_linux_process_identity_set_rss_bytes, + verify_linux_process_identity, +}; + +fn proc_stat_with_start_time(start_time_ticks: u64) -> String { + format!( + "42 (chrome worker)) S 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 {start_time_ticks} 999" + ) +} + +fn proc_stat_for_pid(process_id: u32, start_time_ticks: u64) -> String { + proc_stat_with_start_time(start_time_ticks).replacen("42 ", &format!("{process_id} "), 1) +} + +#[test] +fn proc_stat_parser_extracts_start_time_after_complex_comm() { + assert_eq!( + parse_linux_proc_stat_start_time_ticks(&proc_stat_with_start_time(987_654)), + Ok(987_654) + ); +} + +#[test] +fn proc_stat_parser_rejects_missing_or_malformed_start_time() { + assert_eq!( + parse_linux_proc_stat_start_time_ticks("42 chrome S 1 2 3"), + Err(BrowserRssSampleError::InvalidProcessStat) + ); + assert_eq!( + parse_linux_proc_stat_start_time_ticks( + &proc_stat_with_start_time(0).replace(" 0 999", " nope 999") + ), + Err(BrowserRssSampleError::InvalidProcessStat) + ); +} + +#[test] +fn proc_stat_parser_rejects_hostile_structure_and_numeric_fields() { + let invalid_cases = [ + "42 (unterminated S 1 2 3".to_owned(), + "42) (misordered S 1 2 3".to_owned(), + " (chrome) S 1 2 3".to_owned(), + "x (chrome) S 1 2 3".to_owned(), + proc_stat_with_start_time(1).replacen("42 ", "4294967296 ", 1), + proc_stat_with_start_time(1).replacen("42 ", "0 ", 1), + "42 (chrome) ".to_owned(), + "42 (chrome) S 1 2 3".to_owned(), + proc_stat_with_start_time(1).replace(" 1 999", " 18446744073709551616 999"), + ]; + + for stat in invalid_cases { + assert_eq!( + parse_linux_proc_stat_start_time_ticks(&stat), + Err(BrowserRssSampleError::InvalidProcessStat), + "unexpectedly accepted stat record: {stat:?}" + ); + } +} + +#[test] +fn process_identity_parser_binds_expected_pid_and_fails_closed() -> Result<(), BrowserRssSampleError> +{ + let expected = LinuxProcessIdentity::new(42, 987_654)?; + assert_eq!( + parse_linux_process_identity(42, &proc_stat_with_start_time(987_654)), + Ok(expected) + ); + assert_eq!( + parse_linux_process_identity(0, &proc_stat_with_start_time(987_654)), + Err(BrowserRssSampleError::InvalidProcessId) + ); + assert_eq!( + parse_linux_process_identity(42, &proc_stat_for_pid(43, 987_654)), + Err(BrowserRssSampleError::ProcessIdentityChanged) + ); + assert_eq!( + parse_linux_process_identity(42, "malformed"), + Err(BrowserRssSampleError::InvalidProcessStat) + ); + Ok(()) +} + +#[test] +fn process_identity_binds_pid_to_kernel_start_time() -> Result<(), BrowserRssSampleError> { + let identity = LinuxProcessIdentity::new(42, 987_654)?; + assert_eq!(identity.process_id(), 42); + assert_eq!(identity.start_time_ticks(), 987_654); + assert_eq!( + verify_linux_process_identity(identity, &proc_stat_with_start_time(987_654)), + Ok(()) + ); + assert_eq!( + verify_linux_process_identity(identity, &proc_stat_with_start_time(987_655)), + Err(BrowserRssSampleError::ProcessIdentityChanged) + ); + assert_eq!( + verify_linux_process_identity(identity, &proc_stat_for_pid(43, 987_654)), + Err(BrowserRssSampleError::ProcessIdentityChanged) + ); + assert_eq!( + verify_linux_process_identity(identity, "malformed"), + Err(BrowserRssSampleError::InvalidProcessStat) + ); + Ok(()) +} + +#[test] +fn process_identity_rejects_zero_pid() { + assert_eq!( + LinuxProcessIdentity::new(0, 1), + Err(BrowserRssSampleError::InvalidProcessId) + ); + assert_eq!( + read_linux_process_identity(0), + Err(BrowserRssSampleError::InvalidProcessId) + ); +} + +#[test] +fn linux_identity_sampler_rejects_pid_reuse_and_samples_current_process() -> Result<(), String> { + #[cfg(target_os = "linux")] + { + let identity = read_linux_process_identity(std::process::id()) + .map_err(|error| format!("read current process identity: {error:?}"))?; + let rss_bytes = sample_linux_process_identity_rss_bytes(identity) + .map_err(|error| format!("sample current process identity: {error:?}"))?; + assert!(rss_bytes > 0); + assert_eq!(rss_bytes % 1_024, 0); + + let identity_set_rss_bytes = sample_linux_process_identity_set_rss_bytes(&[identity]) + .map_err(|error| format!("sample current identity set: {error:?}"))?; + assert!(identity_set_rss_bytes > 0); + assert_eq!(identity_set_rss_bytes % 1_024, 0); + + let stale = LinuxProcessIdentity::new( + identity.process_id(), + identity.start_time_ticks().wrapping_add(1), + ) + .map_err(|error| format!("construct stale identity: {error:?}"))?; + assert_eq!( + sample_linux_process_identity_rss_bytes(stale), + Err(BrowserRssSampleError::ProcessIdentityChanged) + ); + assert_eq!( + sample_linux_process_identity_set_rss_bytes(&[stale]), + Err(BrowserRssSampleError::ProcessIdentityChanged) + ); + assert_eq!( + sample_linux_process_identity_set_rss_bytes(&[]), + Err(BrowserRssSampleError::EmptyProcessSet) + ); + assert_eq!( + sample_linux_process_identity_set_rss_bytes(&[identity, identity]), + Err(BrowserRssSampleError::DuplicateProcessId) + ); + + let absent = LinuxProcessIdentity::new(u32::MAX, 1) + .map_err(|error| format!("construct absent process identity: {error:?}"))?; + assert_eq!( + read_linux_process_identity(u32::MAX), + Err(BrowserRssSampleError::ProcessStatUnavailable) + ); + assert_eq!( + sample_linux_process_identity_rss_bytes(absent), + Err(BrowserRssSampleError::ProcessStatUnavailable) + ); + } + + #[cfg(not(target_os = "linux"))] + { + let identity = LinuxProcessIdentity::new(std::process::id(), 1) + .map_err(|error| format!("construct process identity: {error:?}"))?; + assert_eq!( + read_linux_process_identity(std::process::id()), + Err(BrowserRssSampleError::UnsupportedPlatform) + ); + assert_eq!( + sample_linux_process_identity_rss_bytes(identity), + Err(BrowserRssSampleError::UnsupportedPlatform) + ); + assert_eq!( + sample_linux_process_identity_set_rss_bytes(&[identity]), + Err(BrowserRssSampleError::UnsupportedPlatform) + ); + } + Ok(()) +} diff --git a/crates/originweave-resource/tests/browser_process_set_rss.rs b/crates/originweave-resource/tests/browser_process_set_rss.rs new file mode 100644 index 000000000..e073ee344 --- /dev/null +++ b/crates/originweave-resource/tests/browser_process_set_rss.rs @@ -0,0 +1,84 @@ +use originweave_resource::{ + BrowserRssSampleError, MAX_BROWSER_PROCESS_SET_SIZE, aggregate_browser_process_rss_samples, + sample_linux_process_set_rss_bytes, +}; + +#[test] +fn process_set_aggregation_counts_each_process_once() { + assert_eq!( + aggregate_browser_process_rss_samples(&[(11, 1_024), (12, 2_048), (13, 4_096)]), + Ok(7_168) + ); +} + +#[test] +fn process_set_aggregation_rejects_ambiguous_or_unbounded_membership() { + assert_eq!( + aggregate_browser_process_rss_samples(&[]), + Err(BrowserRssSampleError::EmptyProcessSet) + ); + assert_eq!( + aggregate_browser_process_rss_samples(&[(0, 1)]), + Err(BrowserRssSampleError::InvalidProcessId) + ); + assert_eq!( + aggregate_browser_process_rss_samples(&[(42, 1), (42, 2)]), + Err(BrowserRssSampleError::DuplicateProcessId) + ); + + let oversized: Vec<(u32, u64)> = (1..=(MAX_BROWSER_PROCESS_SET_SIZE as u32 + 1)) + .map(|process_id| (process_id, 1)) + .collect(); + assert_eq!( + aggregate_browser_process_rss_samples(&oversized), + Err(BrowserRssSampleError::ProcessSetTooLarge) + ); +} + +#[test] +fn process_set_aggregation_rejects_overflow_instead_of_undercounting() { + assert_eq!( + aggregate_browser_process_rss_samples(&[(1, u64::MAX), (2, 1)]), + Err(BrowserRssSampleError::ProcessSetRssOverflow) + ); +} + +#[test] +fn linux_process_set_sampler_measures_the_current_process() { + #[cfg(target_os = "linux")] + { + let sampled = sample_linux_process_set_rss_bytes(&[std::process::id()]); + assert!( + matches!(&sampled, Ok(rss_bytes) if *rss_bytes > 0 && *rss_bytes % 1_024 == 0), + "the current Linux test process must produce a positive kernel-kB RSS sample: {sampled:?}" + ); + } + + #[cfg(not(target_os = "linux"))] + assert_eq!( + sample_linux_process_set_rss_bytes(&[std::process::id()]), + Err(BrowserRssSampleError::UnsupportedPlatform) + ); +} + +#[test] +fn linux_process_set_sampler_fails_closed_for_invalid_or_partial_sets() { + assert_eq!( + sample_linux_process_set_rss_bytes(&[]), + Err(BrowserRssSampleError::EmptyProcessSet) + ); + assert_eq!( + sample_linux_process_set_rss_bytes(&[1, 1]), + Err(BrowserRssSampleError::DuplicateProcessId) + ); + assert_eq!( + sample_linux_process_set_rss_bytes(&[0]), + Err(BrowserRssSampleError::InvalidProcessId) + ); + + #[cfg(target_os = "linux")] + assert_eq!( + sample_linux_process_set_rss_bytes(&[std::process::id(), u32::MAX]), + Err(BrowserRssSampleError::ProcessStatusUnavailable) + ); +}