From df84ca05f35ca024a77592855be4aed9fc3e1a51 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Wed, 14 Oct 2026 21:09:35 +0200 Subject: [PATCH 1/5] Plan v0.43.0 in rivet, where the epic said it was pointing at the epic, so `rivet release status v0.43.0` reported one requirement for a release that holds five. Each is now a requirement with clauses that a test can fail: REQ-COMPOSEWINDOW-001 a composition's window is capped at its includes' (#254, #226) REQ-ENVQUOTE-001 varve env quotes the store root it asks you to eval (#46) REQ-NOPINNOTOOL-001 no pin in scope: which/run/shims refuse as status does (#198) REQ-MUTANTDEBT-001 declared mutation debt is measured in full (#181) maintainer's decisions and stay as such. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu --- artifacts/requirements.yaml | 49 +++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/artifacts/requirements.yaml b/artifacts/requirements.yaml index d9ce6e2..e8ad35b 100644 --- a/artifacts/requirements.yaml +++ b/artifacts/requirements.yaml @@ -5013,3 +5013,52 @@ artifacts: created-by: ai model: claude-opus-5-5 timestamp: 2026-10-14T20:07:55Z + + - id: REQ-COMPOSEWINDOW-001 + type: requirement + title: A composition never promises support past the layers it composes, unless it says why + status: approved + release: v0.43.0 + description: "Measured on varve#254: covalent 2026.10.0 signed support-until 2026-11-02 while the wasm-layers layer it includes is supported until 2026-10-24, nine days less. The composition's window is derived from its own channel alone, so it signs a promise about bytes whose own realm has stopped vouching for them. varve#226 is the same defect stated generally. Clauses: (1) The support horizon of a layer that includes other layers shall be capped at the earliest support-until among its includes, read from each include's own signed line-status and verified against that include's realm root. (2) An include whose line-status is missing, unverifiable or states no window shall refuse the horizon rather than be skipped, because skipping it is how the cap becomes vacuous. (3) Exceeding the cap shall require an explicit override carrying a reason, so a longer promise is a decision on the record and not a default. (4) The cap shall be applied by the command the deposit already calls, so every layers repository gets it by taking the next varve and changing nothing else." + links: + - type: satisfies + target: REQ-SUPPORTUNTIL-001 + - type: satisfies + target: REQ-COMPOSE-001 + provenance: + created-by: ai + model: claude-opus-5-5 + timestamp: 2026-10-14T19:09:24Z + + - id: REQ-ENVQUOTE-001 + type: requirement + title: varve env emits the store root quoted, so the line a user is told to eval cannot execute it + status: approved + release: v0.43.0 + description: "varve#46: env_script_sh interpolates the store root inside sh double quotes, where $ and backticks stay live; VARVE_ROOT='/tmp/vr$(id)x' runs id when the user follows 'eval \"$(varve env)\"'. The same injection class v0.19.0 removed from shim dispatch, surviving where the user is told to eval. Clauses: (1) Every shell varve env supports shall receive the path in that shell's literal quoting (sh/bash/zsh single quotes with '\\'' escaping; fish and PowerShell by their own rules). (2) A root containing each shell's metacharacters shall round-trip as the literal path, shown by running the emitted script in that shell." + provenance: + created-by: ai + model: claude-opus-5-5 + timestamp: 2026-10-14T19:09:24Z + + - id: REQ-NOPINNOTOOL-001 + type: requirement + title: With no pin in scope, which, run and the shims refuse as status does + status: approved + release: v0.43.0 + description: "varve#198: in a directory with no varve.toml, varve status refused (no trust root) while varve which ordeal resolved an old ordeal from the legacy pre-realm store ~/.varve/core with no verification, and the shims on PATH did the same. Two code paths answering one question differently. Clauses: (1) With no pin in scope, which, run and shim dispatch shall refuse with the same reason status gives. (2) No resolution shall read the legacy pre-realm store. (3) varve shall say the legacy store exists and that nothing in it verifies, without deleting it." + provenance: + created-by: ai + model: claude-opus-5-5 + timestamp: 2026-10-14T19:09:24Z + + - id: REQ-MUTANTDEBT-001 + type: requirement + title: Declared mutation debt is measured in full, not cut off at a time cap + status: approved + release: v0.43.0 + description: "varve#181: the nightly advisory mutation run is cancelled at its 90-minute cap every night after ~32% of 2,675 mutants, so 11 of the 22 not-yet files are never reached. The instrument that makes declared debt counted rather than hidden counts half of it. Clauses: (1) Every not-yet file in mutation-scope.toml shall be tested by some scheduled run that completes. (2) A run that does not complete shall be reported as incomplete, never as a measurement." + provenance: + created-by: ai + model: claude-opus-5-5 + timestamp: 2026-10-14T19:09:24Z From 003143da70451a591c8216b3f139fd20d9fb96a6 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Wed, 14 Oct 2026 21:22:13 +0200 Subject: [PATCH 2/5] A composition never promises support past the layers it composes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit covalent 2026.10.0 signed support until 2026-11-02 over a wasm-layers layer whose own realm stops vouching for it on 2026-10-24 (#254, #226). The window came from the composition's channel alone. `varve support-horizon --manifest layer.toml` now reads every [[include]] from its own realm: manifest by digest, verified against that realm's root, then the line-status install would choose (baseline or later correction, whichever verifies and is newer — extracted as current_line_status so both readers share one rule). The horizon is the earliest of the policy and every include. An include that states no window refuses rather than being skipped. --past-includes "" promises longer, on the record. Measured against the live registries with covalent-layers' real layer.toml: "support capped at 2026-10-24 by included layer 2026.09.0 (realm 'pulseengine-wasm'); the rolling policy alone would say 2026-11-02" — the nine days of #254, exactly. REQ-COMPOSEWINDOW-001 clause 4 is corrected: a layers repository adopts this with one argument, not with nothing; covalent's deposit needs it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu --- artifacts/requirements.yaml | 2 +- crates/varve-core/src/linestatus.rs | 29 +- crates/varve-core/src/support.rs | 483 ++++++++++++++++++++++- crates/varve/docs/cmd-support-horizon.md | 41 +- crates/varve/src/main.rs | 131 +++++- crates/varve/tests/cli.rs | 64 +++ 6 files changed, 725 insertions(+), 25 deletions(-) diff --git a/artifacts/requirements.yaml b/artifacts/requirements.yaml index e8ad35b..d7ec3a3 100644 --- a/artifacts/requirements.yaml +++ b/artifacts/requirements.yaml @@ -5019,7 +5019,7 @@ artifacts: title: A composition never promises support past the layers it composes, unless it says why status: approved release: v0.43.0 - description: "Measured on varve#254: covalent 2026.10.0 signed support-until 2026-11-02 while the wasm-layers layer it includes is supported until 2026-10-24, nine days less. The composition's window is derived from its own channel alone, so it signs a promise about bytes whose own realm has stopped vouching for them. varve#226 is the same defect stated generally. Clauses: (1) The support horizon of a layer that includes other layers shall be capped at the earliest support-until among its includes, read from each include's own signed line-status and verified against that include's realm root. (2) An include whose line-status is missing, unverifiable or states no window shall refuse the horizon rather than be skipped, because skipping it is how the cap becomes vacuous. (3) Exceeding the cap shall require an explicit override carrying a reason, so a longer promise is a decision on the record and not a default. (4) The cap shall be applied by the command the deposit already calls, so every layers repository gets it by taking the next varve and changing nothing else." + description: Measured on varve#25(4) The cap shall be applied by the command the deposit already calls (varve support-horizon), given the layer's manifest; a layers repository adopts it with one argument and the realm definitions it already downloads, and no second command decides the window. links: - type: satisfies target: REQ-SUPPORTUNTIL-001 diff --git a/crates/varve-core/src/linestatus.rs b/crates/varve-core/src/linestatus.rs index 21db3ea..a57966c 100644 --- a/crates/varve-core/src/linestatus.rs +++ b/crates/varve-core/src/linestatus.rs @@ -785,6 +785,30 @@ pub fn cache_baseline_from_source( root_pk: &[u8], store_root: &Path, ) -> Result, LineStatusError> { + match current_line_status(source, layer, line, root_pk)? { + Some((envelope, doc)) => { + StatusCache::at_root(store_root).update(line, &envelope, &doc)?; + Ok(Some(doc.counter)) + } + None => Ok(None), + } +} + +/// The line-status that currently speaks for `layer`: the baseline deposited +/// beside it or the document later published under the line's own tag, +/// whichever VERIFIES and carries the higher counter. `Ok(None)` when the +/// source carries neither; `Err` when something was present but bad. +/// +/// One rule for every reader. The install cache and a composition reading its +/// includes' windows (REQ-COMPOSEWINDOW-001) must agree about which document +/// is current, or a correction that shortened a window would bind one and +/// not the other. +pub fn current_line_status( + source: &dyn crate::source::LayerSource, + layer: &crate::source::LayerRef, + line: &Line, + root_pk: &[u8], +) -> Result, LineStatus)>, LineStatusError> { let baseline = source .fetch_line_status(layer) .map_err(|e| LineStatusError::Payload(format!("fetching baseline line-status: {e}")))?; @@ -830,10 +854,7 @@ pub fn cache_baseline_from_source( } match best { - Some((counter, envelope, doc)) => { - StatusCache::at_root(store_root).update(line, &envelope, &doc)?; - Ok(Some(counter)) - } + Some((_, envelope, doc)) => Ok(Some((envelope, doc))), // Nothing usable. If something was PRESENT but bad, that is a finding // and is raised; if both were simply absent, the source carries none. None => match first_error { diff --git a/crates/varve-core/src/support.rs b/crates/varve-core/src/support.rs index d1485ae..1c1e380 100644 --- a/crates/varve-core/src/support.rs +++ b/crates/varve-core/src/support.rs @@ -67,7 +67,18 @@ impl Policy { #[derive(Debug, Clone, PartialEq, Eq)] pub enum SupportError { UnknownChannel(String), - BadDate { field: &'static str, value: String }, + BadDate { + field: &'static str, + value: String, + }, + /// An include's own signed line-status states no window, so nothing + /// bounds what the composition may promise about it. + IncludeStatesNoWindow { + layer: String, + realm: String, + }, + /// `--past-includes` was given with nothing in it. + OverrideWithoutReason, } impl fmt::Display for SupportError { @@ -85,6 +96,18 @@ impl fmt::Display for SupportError { field has never been validated, so an unparseable value would \ sign cleanly and then be unusable by everything that reads it." ), + SupportError::IncludeStatesNoWindow { layer, realm } => write!( + f, + "included layer {layer} (realm '{realm}') states no support window, so \ + nothing bounds how long this composition may promise to support it. \ + Refusing rather than skipping it: the include nobody vouched for is \ + the one that would have set the cap." + ), + SupportError::OverrideWithoutReason => write!( + f, + "promising support past an included layer's window needs a reason on \ + the record; an empty one is not a decision" + ), } } } @@ -130,6 +153,163 @@ fn days_in_month(y: i64, m: i64) -> i64 { } } +/// One included layer's stated window, read from that layer's OWN signed +/// line-status and verified against ITS realm's root by the caller. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IncludeWindow { + pub layer: String, + pub realm: String, + /// `None` when the include's line-status states no window. + pub support_until: Option, +} + +/// What set a composition's horizon. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Bound { + /// The layer's own channel policy; no include ends sooner. + Own, + /// This include ends sooner, and the composition promises no more. + Include { layer: String, realm: String }, + /// The composition promises past `earliest`, deliberately, for `reason`. + PastIncludes { earliest: String, reason: String }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Capped { + pub until: String, + pub bound_by: Bound, +} + +/// Cap a composition's horizon at the earliest window among its includes +/// (REQ-COMPOSEWINDOW-001). +/// +/// A composition's window was derived from its own channel alone, so covalent +/// 2026.10.0 signed support until 2026-11-02 over a wasm layer whose own realm +/// stops vouching for it on 2026-10-24 (varve#254). The composition cannot +/// support bytes longer than the realm that publishes them does. +/// +/// Capping, not refusing, is the default: an auto-deposit should produce the +/// true window without a person. Promising longer is possible and takes a +/// `past_includes` reason, which the caller prints beside the signed date. +pub fn cap_by_includes( + own: &str, + includes: &[IncludeWindow], + past_includes: Option<&str>, +) -> Result { + let own_days = epoch_days(own).ok_or_else(|| SupportError::BadDate { + field: "support-until", + value: own.to_string(), + })?; + let mut earliest: Option<(i64, &IncludeWindow, &str)> = None; + for inc in includes { + let until = + inc.support_until + .as_deref() + .ok_or_else(|| SupportError::IncludeStatesNoWindow { + layer: inc.layer.clone(), + realm: inc.realm.clone(), + })?; + let days = epoch_days(until).ok_or_else(|| SupportError::BadDate { + field: "include support-until", + value: until.to_string(), + })?; + if earliest.is_none_or(|(e, _, _)| days < e) { + earliest = Some((days, inc, until)); + } + } + let own_stands = Capped { + until: own.to_string(), + bound_by: Bound::Own, + }; + let Some((_, inc, until)) = earliest.filter(|(d, _, _)| *d < own_days) else { + return Ok(own_stands); + }; + match past_includes { + Some(reason) if reason.trim().is_empty() => Err(SupportError::OverrideWithoutReason), + Some(reason) => Ok(Capped { + until: own.to_string(), + bound_by: Bound::PastIncludes { + earliest: until.to_string(), + reason: reason.trim().to_string(), + }, + }), + None => Ok(Capped { + until: until.to_string(), + bound_by: Bound::Include { + layer: inc.layer.clone(), + realm: inc.realm.clone(), + }, + }), + } +} + +/// Why one include's window could not be read. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IncludeWindowError { + /// The include, by digest — all that is known before its manifest verifies. + pub include: String, + pub realm: String, + pub reason: String, +} + +impl fmt::Display for IncludeWindowError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "cannot read the support window of included layer {} (realm '{}'): {}", + self.include, self.realm, self.reason + ) + } +} + +impl std::error::Error for IncludeWindowError {} + +/// Read one include's stated window from the realm that vouches for it. +/// +/// `source`, `verifier` and `root_pk` are THAT realm's — never the composing +/// realm's. The manifest is verified first, so the layer id the window is +/// reported against is one the realm signed; the line-status is then chosen by +/// [`crate::linestatus::current_line_status`], the same rule install uses, so +/// a correction that shortened the window binds here too. +/// +/// No status at all yields `support_until: None`, which `cap_by_includes` +/// refuses. A status that is present and bad is an error. +pub fn include_window( + source: &dyn crate::source::LayerSource, + verifier: &dyn crate::install::ManifestVerifier, + root_pk: &[u8], + digest: &str, + realm: &str, +) -> Result { + let fail = |reason: String| IncludeWindowError { + include: digest.to_string(), + realm: realm.to_string(), + reason, + }; + let layer_ref = crate::source::LayerRef::Digest(digest.to_string()); + let fetched = source + .fetch_manifest(&layer_ref) + .map_err(|e| fail(format!("fetching its manifest: {e}")))?; + let payload = verifier + .verify(&fetched) + .map_err(|e| fail(format!("its manifest does not verify: {}", e.0)))?; + if crate::store::manifest_digest(&payload) != digest { + return Err(fail( + "the manifest served for this digest hashes to something else".to_string(), + )); + } + let manifest = crate::manifest::LayerManifest::parse(&payload) + .map_err(|e| fail(format!("its manifest does not parse: {e}")))?; + let status = + crate::linestatus::current_line_status(source, &layer_ref, manifest.layer.line(), root_pk) + .map_err(|e| fail(format!("layer {}: its line-status: {e}", manifest.layer)))?; + Ok(IncludeWindow { + layer: manifest.layer.to_string(), + realm: realm.to_string(), + support_until: status.and_then(|(_, doc)| doc.report_for(&manifest.layer).support_until), + }) +} + /// Where a layer stands against its stated horizon. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Standing { @@ -358,4 +538,305 @@ mod tests { ); assert!(far.contains("(382 days)"), "{far}"); } + + fn inc(layer: &str, until: Option<&str>) -> IncludeWindow { + IncludeWindow { + layer: layer.to_string(), + realm: "wasm".to_string(), + support_until: until.map(str::to_string), + } + } + + /// The measured case from varve#254: covalent's own channel says + /// 2026-11-02, the wasm layer it includes says 2026-10-24. + // rivet: verifies REQ-COMPOSEWINDOW-001 + #[test] + fn a_composition_is_capped_at_its_earliest_include() { + let got = cap_by_includes( + "2026-11-02", + &[ + inc("2026.10.0", Some("2026-10-24")), + inc("2026.10.3", Some("2026-10-30")), + ], + None, + ) + .unwrap(); + assert_eq!(got.until, "2026-10-24"); + assert_eq!( + got.bound_by, + Bound::Include { + layer: "2026.10.0".into(), + realm: "wasm".into() + } + ); + } + + /// The earliest include binds wherever it is listed, and on a tie the + /// first listed is named, so the message is the same on every run. + // rivet: verifies REQ-COMPOSEWINDOW-001 + #[test] + fn the_earliest_include_binds_wherever_it_is_listed() { + let got = cap_by_includes( + "2026-11-02", + &[ + inc("late", Some("2026-10-30")), + inc("early", Some("2026-10-24")), + inc("tied", Some("2026-10-24")), + ], + None, + ) + .unwrap(); + assert_eq!(got.until, "2026-10-24"); + assert_eq!( + got.bound_by, + Bound::Include { + layer: "early".into(), + realm: "wasm".into() + } + ); + } + + /// An include supported for longer than the composition does not lengthen + /// it, and a layer with no includes keeps its own window. + // rivet: verifies REQ-COMPOSEWINDOW-001 + #[test] + fn the_own_window_stands_when_it_is_already_the_shortest() { + let got = cap_by_includes("2026-11-02", &[inc("x", Some("2027-01-01"))], None).unwrap(); + assert_eq!( + (got.until.as_str(), &got.bound_by), + ("2026-11-02", &Bound::Own) + ); + let alone = cap_by_includes("2026-11-02", &[], None).unwrap(); + assert_eq!( + (alone.until.as_str(), &alone.bound_by), + ("2026-11-02", &Bound::Own) + ); + } + + /// The same day is not an overshoot: "through that day" on both sides. + // rivet: verifies REQ-COMPOSEWINDOW-001 + #[test] + fn an_include_ending_the_same_day_is_not_a_cap() { + let got = cap_by_includes("2026-11-02", &[inc("x", Some("2026-11-02"))], None).unwrap(); + assert_eq!(got.bound_by, Bound::Own); + } + + /// Clause 2: an include that states no window is not skipped. Skipping it + /// is how the cap goes vacuous — the one include nobody vouched for is + /// exactly the one that would have bound it. + // rivet: verifies REQ-COMPOSEWINDOW-001 + #[test] + fn an_include_that_states_no_window_refuses_rather_than_being_skipped() { + let err = cap_by_includes( + "2026-11-02", + &[inc("a", Some("2026-12-01")), inc("b", None)], + None, + ) + .unwrap_err(); + assert_eq!( + err, + SupportError::IncludeStatesNoWindow { + layer: "b".into(), + realm: "wasm".into() + } + ); + assert!(err.to_string().contains("b"), "{err}"); + } + + /// A malformed include date refuses rather than comparing as text. + // rivet: verifies REQ-COMPOSEWINDOW-001 + #[test] + fn an_unparseable_include_window_refuses() { + let err = cap_by_includes("2026-11-02", &[inc("a", Some("soon"))], None).unwrap_err(); + assert!(matches!(err, SupportError::BadDate { .. }), "{err:?}"); + } + + /// Clause 3: a longer promise is possible, and it is a decision with a + /// reason on the record — the reason is carried out so the caller can + /// print it beside the signed date. + // rivet: verifies REQ-COMPOSEWINDOW-001 + #[test] + fn exceeding_the_cap_takes_a_stated_reason() { + let got = cap_by_includes( + "2026-11-02", + &[inc("2026.10.0", Some("2026-10-24"))], + Some("wasm-layers 2026.11.0 is staged and carries the same tools"), + ) + .unwrap(); + assert_eq!(got.until, "2026-11-02"); + assert_eq!( + got.bound_by, + Bound::PastIncludes { + earliest: "2026-10-24".into(), + reason: "wasm-layers 2026.11.0 is staged and carries the same tools".into() + } + ); + } + + /// An override that says nothing is not a decision. + // rivet: verifies REQ-COMPOSEWINDOW-001 + #[test] + fn an_override_without_a_reason_is_refused() { + let err = + cap_by_includes("2026-11-02", &[inc("a", Some("2026-10-24"))], Some(" ")).unwrap_err(); + assert_eq!(err, SupportError::OverrideWithoutReason); + assert!(!err.to_string().is_empty()); + } + + /// An override that was not needed changes nothing: it must not be + /// recorded as though a longer promise had been made. + // rivet: verifies REQ-COMPOSEWINDOW-001 + #[test] + fn an_unneeded_override_is_not_recorded() { + let got = cap_by_includes( + "2026-11-02", + &[inc("a", Some("2027-01-01"))], + Some("because"), + ) + .unwrap(); + assert_eq!(got.bound_by, Bound::Own); + } + + mod from_source { + use super::super::*; + use crate::install::{ManifestVerifier, VerifyError}; + use crate::linestatus::LineStatus; + use crate::manifest::fixtures::manifest_with_tools; + use crate::source::MemorySource; + use crate::store::manifest_digest; + use crate::verify::generate_root_keypair; + + struct AcceptAll; + impl ManifestVerifier for AcceptAll { + fn verify(&self, fetched: &[u8]) -> Result, VerifyError> { + Ok(fetched.to_vec()) + } + } + struct RejectAll; + impl ManifestVerifier for RejectAll { + fn verify(&self, _: &[u8]) -> Result, VerifyError> { + Err(VerifyError("untrusted signature (test)".into())) + } + } + + fn wasm_layer() -> Vec { + manifest_with_tools("2026.10.0", "rolling", 0, "2026-09-24T00:00:00Z", &[]) + } + + fn status(counter: u64, until: Option<&str>) -> LineStatus { + LineStatus { + min_counter: None, + line: "2026.10".into(), + counter, + issued_at: "2026-09-24T00:00:00Z".into(), + support_until: until.map(str::to_string), + yanked: Default::default(), + known_problems: Vec::new(), + } + } + + /// The include's window is read from ITS realm's signed status, and + /// names the layer the digest turned out to be. + // rivet: verifies REQ-COMPOSEWINDOW-001 + #[test] + fn an_include_window_comes_from_its_own_signed_status() { + let (sk, pk) = generate_root_keypair(); + let bytes = wasm_layer(); + let source = MemorySource::new().with_manifest(&bytes).with_line_status( + status(1, Some("2026-10-24")) + .sign(&sk, "k") + .unwrap() + .as_bytes(), + ); + let w = + include_window(&source, &AcceptAll, &pk, &manifest_digest(&bytes), "wasm").unwrap(); + assert_eq!( + w, + IncludeWindow { + layer: "2026.10.0".into(), + realm: "wasm".into(), + support_until: Some("2026-10-24".into()), + } + ); + } + + /// A later correction that SHORTENS the window binds the cap — the + /// same document install would cache. + // rivet: verifies REQ-COMPOSEWINDOW-001 + #[test] + fn a_published_correction_overtakes_the_baseline_window() { + let (sk, pk) = generate_root_keypair(); + let bytes = wasm_layer(); + let source = MemorySource::new() + .with_manifest(&bytes) + .with_line_status( + status(1, Some("2026-10-24")) + .sign(&sk, "k") + .unwrap() + .as_bytes(), + ) + .with_published_line_status( + status(5, Some("2026-10-10")) + .sign(&sk, "k") + .unwrap() + .as_bytes(), + ); + let w = + include_window(&source, &AcceptAll, &pk, &manifest_digest(&bytes), "wasm").unwrap(); + assert_eq!(w.support_until.as_deref(), Some("2026-10-10")); + } + + /// A status signed by any other key is not that realm speaking. + // rivet: verifies REQ-COMPOSEWINDOW-001 + #[test] + fn a_status_not_signed_by_the_includes_realm_is_refused() { + let (sk_other, _) = generate_root_keypair(); + let (_, pk) = generate_root_keypair(); + let bytes = wasm_layer(); + let source = MemorySource::new().with_manifest(&bytes).with_line_status( + status(1, Some("2027-12-31")) + .sign(&sk_other, "k") + .unwrap() + .as_bytes(), + ); + let err = include_window(&source, &AcceptAll, &pk, &manifest_digest(&bytes), "wasm") + .unwrap_err(); + assert!( + err.to_string().contains("2026.10.0") || err.to_string().contains("sha256:"), + "{err}" + ); + } + + /// No status at all is a window nobody stated — carried as `None`, so + /// `cap_by_includes` refuses it (clause 2) rather than skipping it. + // rivet: verifies REQ-COMPOSEWINDOW-001 + #[test] + fn an_include_with_no_status_states_no_window() { + let (_, pk) = generate_root_keypair(); + let bytes = wasm_layer(); + let source = MemorySource::new().with_manifest(&bytes); + let w = + include_window(&source, &AcceptAll, &pk, &manifest_digest(&bytes), "wasm").unwrap(); + assert_eq!(w.support_until, None); + assert!(cap_by_includes("2026-11-02", &[w], None).is_err()); + } + + /// The include's manifest must verify under its realm before anything + /// it says about itself is believed. + // rivet: verifies REQ-COMPOSEWINDOW-001 + #[test] + fn an_include_whose_manifest_does_not_verify_is_refused() { + let (sk, pk) = generate_root_keypair(); + let bytes = wasm_layer(); + let source = MemorySource::new().with_manifest(&bytes).with_line_status( + status(1, Some("2026-10-24")) + .sign(&sk, "k") + .unwrap() + .as_bytes(), + ); + assert!( + include_window(&source, &RejectAll, &pk, &manifest_digest(&bytes), "wasm").is_err() + ); + } + } } diff --git a/crates/varve/docs/cmd-support-horizon.md b/crates/varve/docs/cmd-support-horizon.md index b040503..185d120 100644 --- a/crates/varve/docs/cmd-support-horizon.md +++ b/crates/varve/docs/cmd-support-horizon.md @@ -8,7 +8,17 @@ varve support-horizon --channel rolling --issued-at 2026-09-03T00:00:00Z # 2027-03-03 ``` -`--json` emits `{"channel", "issued_at", "support_until"}` for a pipeline. +`--json` emits `{"channel", "issued_at", "support_until", "policy_until", +"bound_by"}` for a pipeline. + +A layer that composes others passes its `layer.toml`: + +```sh +varve support-horizon --channel rolling --issued-at 2026-10-02T10:00:00Z \ + --manifest layer.toml +# support capped at 2026-10-24 by included layer 2026.09.0 (realm 'pulseengine-wasm'); ... (stderr) +# 2026-10-24 (stdout) +``` ## Why this is derived and not typed @@ -46,10 +56,37 @@ is pointed at, and the one whose contents are deliberately frozen. An unknown channel is refused rather than given a default. A horizon nobody decided is a promise nobody made, and it would be signed with the realm's root. +## A composition cannot outlast what it composes + +covalent 2026.10.0 signed support until **2026-11-02** while the wasm-layers +layer it includes was supported only until **2026-10-24** (varve#254). The +composition promised nine days of support for bytes whose own realm had stopped +vouching for them, and signed that promise with its root. + +With `--manifest`, every `[[include]]` is read from **its own realm**: the +include's manifest is fetched by digest and verified against that realm's +root, then its current line-status (the baseline, or a later published +correction, whichever verifies and is newer) supplies its window. The horizon +is the earliest of the channel's policy and every include's window. + +- **Capping is the default**, so an automatic deposit states the true window + without a person. +- **An include that states no window refuses.** Skipping it would make the cap + vacuous: the include nobody vouched for is exactly the one that would have + bound it. +- **`--past-includes ""` promises longer anyway.** The reason is + printed beside the date, so a longer promise is a decision on the record. + An empty reason is refused. + +The realms the includes name must be defined in a `varve-realms.toml` found +from the working directory, exactly as for `install`. + ## What it does not do + It does not sample a clock beyond the `--issued-at` you give it, and it does -not write anything. The caller puts the result in the status document, and +not write anything. Without `--manifest` it makes no network request; with +one, it reads only the includes' manifests and line-statuses. The caller puts the result in the status document, and `varve sign-status` refuses a document without one. ## Reading it back diff --git a/crates/varve/src/main.rs b/crates/varve/src/main.rs index 067f051..c6b0962 100644 --- a/crates/varve/src/main.rs +++ b/crates/varve/src/main.rs @@ -205,6 +205,16 @@ enum Cmd { /// The layer's issued-at, RFC 3339. #[arg(long = "issued-at")] issued_at: String, + /// The layer's `layer.toml`. When it composes other layers, the + /// horizon is capped at the earliest window among them, read from each + /// include's own realm (REQ-COMPOSEWINDOW-001). + #[arg(long)] + manifest: Option, + /// Promise support past an include's window anyway, for this reason. + /// The reason is printed beside the date so the decision is on the + /// record. + #[arg(long = "past-includes", value_name = "REASON")] + past_includes: Option, #[arg(long)] json: bool, }, @@ -871,24 +881,16 @@ fn run() -> anyhow::Result { Cmd::SupportHorizon { channel, issued_at, + manifest, + past_includes, json, - } => { - let until = varve_core::support::horizon(&issued_at, &channel)?; - if json { - println!( - "{}", - serde_json::json!({ - "command": "support-horizon", - "channel": channel, - "issued_at": issued_at, - "support_until": until, - }) - ); - } else { - println!("{until}"); - } - Ok(()) - } + } => support_horizon( + &channel, + &issued_at, + manifest.as_deref(), + past_includes.as_deref(), + json, + ), Cmd::Keygen { out, public } => keygen(&out, public.as_deref()), Cmd::Pubkey { key } => pubkey(&key), Cmd::LayerSpec { manifest, json } => layer_spec(&manifest, json), @@ -3382,6 +3384,101 @@ fn today_rfc3339() -> String { format!("{y:04}-{m:02}-{d:02}T00:00:00Z") } +/// `varve support-horizon` — the channel's window, capped at the earliest +/// window among the layers this one composes (REQ-SUPPORTUNTIL-001, +/// REQ-COMPOSEWINDOW-001). +/// +/// The date alone goes to stdout, as it always has, so a workflow's +/// `SUPPORT_UNTIL="$(varve support-horizon ...)"` keeps working; what bound +/// it goes to stderr, where a deposit log keeps it. +fn support_horizon( + channel: &str, + issued_at: &str, + manifest: Option<&std::path::Path>, + past_includes: Option<&str>, + json: bool, +) -> anyhow::Result<()> { + use varve_core::support::{self, Bound}; + let own = support::horizon(issued_at, channel)?; + let includes = match manifest { + Some(path) => { + let text = std::fs::read_to_string(path) + .with_context(|| format!("cannot read {}", path.display()))?; + varve_core::layerspec::parse_layer_manifest(&text) + .with_context(|| format!("{} does not parse", path.display()))? + .includes + } + None => Vec::new(), + }; + let cwd = std::env::current_dir().context("cannot determine working directory")?; + let mut windows = Vec::new(); + for inc in &includes { + let Some(realm_name) = inc.realm.as_deref() else { + bail!( + "include {} names no realm, so no root vouches for its support window", + inc.digest + ); + }; + let realm = varve_core::resolve_realm(&cwd, realm_name).with_context(|| { + format!( + "include {} names realm '{realm_name}', which is not defined here", + inc.digest + ) + })?; + let source = varve_core::RegistrySource::parse(&realm.registry)?; + let verifier = varve_core::PinnedKeyVerifier::from_public_key_bytes(&realm.trust_root) + .map_err(|e| anyhow::anyhow!(e.to_string()))?; + windows.push(support::include_window( + &source, + &verifier, + &realm.trust_root, + &inc.digest, + realm_name, + )?); + } + let capped = support::cap_by_includes(&own, &windows, past_includes)?; + let note = match &capped.bound_by { + Bound::Own => None, + Bound::Include { layer, realm } => Some(format!( + "support capped at {} by included layer {layer} (realm '{realm}'); the {channel} \ + policy alone would say {own}", + capped.until + )), + Bound::PastIncludes { earliest, reason } => Some(format!( + "support stated past an included layer's window ({earliest}) to {}, because: {reason}", + capped.until + )), + }; + if json { + let bound_by = match &capped.bound_by { + Bound::Own => serde_json::json!({ "kind": "own" }), + Bound::Include { layer, realm } => { + serde_json::json!({ "kind": "include", "layer": layer, "realm": realm }) + } + Bound::PastIncludes { earliest, reason } => { + serde_json::json!({ "kind": "past-includes", "earliest": earliest, "reason": reason }) + } + }; + println!( + "{}", + serde_json::json!({ + "command": "support-horizon", + "channel": channel, + "issued_at": issued_at, + "support_until": capped.until, + "policy_until": own, + "bound_by": bound_by, + }) + ); + } else { + if let Some(note) = ¬e { + eprintln!("{note}"); + } + println!("{}", capped.until); + } + Ok(()) +} + /// Fetch one layer a composition names, from the realm that vouches for it. /// /// The realm decides everything: its registry is where the bytes come from, diff --git a/crates/varve/tests/cli.rs b/crates/varve/tests/cli.rs index 43ba801..b12ffe2 100644 --- a/crates/varve/tests/cli.rs +++ b/crates/varve/tests/cli.rs @@ -7052,3 +7052,67 @@ fn the_timing_report_goes_to_stderr_and_not_to_stdout() { to skip:\n{body}" ); } + +const LAYER_TOML_NO_INCLUDES: &str = r#" +[varve] +version = "v0.41.0" + +[realm] +name = "pulseengine" +channel = "rolling" +registry = "oci://ghcr.io/pulseengine/layers" + +[[tool]] +name = "rivet" +version = "v0.34.0" +"#; + +/// A layer that composes nothing keeps its channel's window, and stdout stays +/// the bare date a workflow captures with `$(...)`. +// rivet: verifies REQ-COMPOSEWINDOW-001 +#[test] +fn support_horizon_with_a_manifest_that_composes_nothing_keeps_the_policy_date() { + let fx = fixture(None, &[]); + std::fs::write(fx.project.join("layer.toml"), LAYER_TOML_NO_INCLUDES).unwrap(); + varve(&fx) + .args([ + "support-horizon", + "--channel", + "rolling", + "--issued-at", + "2026-10-02T10:00:00Z", + "--manifest", + "layer.toml", + ]) + .assert() + .success() + .stdout("2026-11-02\n") + .stderr(""); +} + +/// An include whose realm is not defined here cannot be read, and the horizon +/// is refused rather than computed without it (clause 2). +// rivet: verifies REQ-COMPOSEWINDOW-001 +#[test] +fn support_horizon_refuses_an_include_from_an_undefined_realm() { + let fx = fixture(None, &[]); + let text = format!( + "{LAYER_TOML_NO_INCLUDES}\n[[include]]\ndigest = \"sha256:{}\"\nrealm = \"nowhere\"\nlayer = \"2026.09.0\"\n", + "a".repeat(64) + ); + std::fs::write(fx.project.join("layer.toml"), text).unwrap(); + varve(&fx) + .args([ + "support-horizon", + "--channel", + "rolling", + "--issued-at", + "2026-10-02T10:00:00Z", + "--manifest", + "layer.toml", + ]) + .assert() + .failure() + .stdout("") + .stderr(predicate::str::contains("nowhere")); +} From 0182e5af20dbae984f79ed4ea2f558c5deab71df Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Wed, 14 Oct 2026 21:26:04 +0200 Subject: [PATCH 3/5] A correction the cap cannot fetch refuses, rather than reading as none Found by a background security review of the previous commit. The cap read an include's window through current_line_status, which treats a failed fetch of the line's published correction as "no correction". A correction only ever narrows the baseline, so a dropped connection or a withholding registry made the composition sign the longer, superseded window: fail-open on exactly the value being capped. Install keeps that reading on purpose (a best-effort floor, documented), so the choice is now explicit: Published::BestEffort for the cache, Published::Required for a reader about to sign. The new test serves a valid baseline and fails the correction fetch; it failed before this. Residual, stated: a registry that answers 404 for the correction tag is indistinguishable from one that never had it. Binding that needs the realm's signed index to name the status counter; not this change. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu --- crates/varve-core/src/linestatus.rs | 33 +++++++++++++++--- crates/varve-core/src/support.rs | 53 +++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/crates/varve-core/src/linestatus.rs b/crates/varve-core/src/linestatus.rs index a57966c..7bf8780 100644 --- a/crates/varve-core/src/linestatus.rs +++ b/crates/varve-core/src/linestatus.rs @@ -785,7 +785,7 @@ pub fn cache_baseline_from_source( root_pk: &[u8], store_root: &Path, ) -> Result, LineStatusError> { - match current_line_status(source, layer, line, root_pk)? { + match current_line_status(source, layer, line, root_pk, Published::BestEffort)? { Some((envelope, doc)) => { StatusCache::at_root(store_root).update(line, &envelope, &doc)?; Ok(Some(doc.counter)) @@ -794,11 +794,26 @@ pub fn cache_baseline_from_source( } } +/// What a failed fetch of the line's published correction means. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Published { + /// Read as absence: the reader acts on the best document it can verify. + BestEffort, + /// Refuse: the reader will sign on the answer. + Required, +} + /// The line-status that currently speaks for `layer`: the baseline deposited /// beside it or the document later published under the line's own tag, /// whichever VERIFIES and carries the higher counter. `Ok(None)` when the /// source carries neither; `Err` when something was present but bad. /// +/// `published` says what a FAILED fetch of the correction means. Install +/// reads it as absence ([`Published::BestEffort`]) and documents the residual. +/// A reader about to SIGN something on the strength of the answer cannot: +/// a correction only ever narrows what the baseline said, so "could not ask" +/// read as "nothing newer" fails open ([`Published::Required`]). +/// /// One rule for every reader. The install cache and a composition reading its /// includes' windows (REQ-COMPOSEWINDOW-001) must agree about which document /// is current, or a correction that shortened a window would bind one and @@ -808,6 +823,7 @@ pub fn current_line_status( layer: &crate::source::LayerRef, line: &Line, root_pk: &[u8], + published: Published, ) -> Result, LineStatus)>, LineStatusError> { let baseline = source .fetch_line_status(layer) @@ -817,9 +833,18 @@ pub fn current_line_status( // A source that cannot serve one must not be able to make that look like a // failure, so a transport error here is treated as absence — the baseline // path decides whether the consumer ends up with nothing. - let published = source - .fetch_published_line_status(&line.to_string()) - .unwrap_or(None); + let published = match ( + source.fetch_published_line_status(&line.to_string()), + published, + ) { + (Ok(doc), _) => doc, + (Err(_), Published::BestEffort) => None, + (Err(e), Published::Required) => { + return Err(LineStatusError::Payload(format!( + "cannot tell whether a correction was published for line {line}: {e}" + ))); + } + }; // VERIFY FIRST, RANK SECOND. Both documents are untrusted bytes from the // party this evidence exists to constrain. Choosing by counter before diff --git a/crates/varve-core/src/support.rs b/crates/varve-core/src/support.rs index 1c1e380..2adff5a 100644 --- a/crates/varve-core/src/support.rs +++ b/crates/varve-core/src/support.rs @@ -300,9 +300,14 @@ pub fn include_window( } let manifest = crate::manifest::LayerManifest::parse(&payload) .map_err(|e| fail(format!("its manifest does not parse: {e}")))?; - let status = - crate::linestatus::current_line_status(source, &layer_ref, manifest.layer.line(), root_pk) - .map_err(|e| fail(format!("layer {}: its line-status: {e}", manifest.layer)))?; + let status = crate::linestatus::current_line_status( + source, + &layer_ref, + manifest.layer.line(), + root_pk, + crate::linestatus::Published::Required, + ) + .map_err(|e| fail(format!("layer {}: its line-status: {e}", manifest.layer)))?; Ok(IncludeWindow { layer: manifest.layer.to_string(), realm: realm.to_string(), @@ -807,6 +812,48 @@ mod tests { ); } + /// A source that cannot SAY whether a correction was published must + /// not be read as "none was". A correction can only shorten what the + /// baseline promised, so treating a failed fetch as absence fails open: + /// the composition would sign the longer, superseded window. Install + /// tolerates that (best-effort floor); a signed promise must not. + // rivet: verifies REQ-COMPOSEWINDOW-001 + #[test] + fn a_correction_that_cannot_be_fetched_refuses_rather_than_reading_as_none() { + use crate::source::{LayerRef, LayerSource, SourceError}; + struct CorrectionUnreachable(MemorySource); + impl LayerSource for CorrectionUnreachable { + fn fetch_manifest(&self, l: &LayerRef) -> Result, SourceError> { + self.0.fetch_manifest(l) + } + fn fetch_blob(&self, d: &str) -> Result, SourceError> { + self.0.fetch_blob(d) + } + fn fetch_line_status(&self, l: &LayerRef) -> Result>, SourceError> { + self.0.fetch_line_status(l) + } + fn fetch_published_line_status( + &self, + _: &str, + ) -> Result>, SourceError> { + Err(SourceError::Transport("connection reset".into())) + } + } + let (sk, pk) = generate_root_keypair(); + let bytes = wasm_layer(); + let source = CorrectionUnreachable( + MemorySource::new().with_manifest(&bytes).with_line_status( + status(1, Some("2026-10-24")) + .sign(&sk, "k") + .unwrap() + .as_bytes(), + ), + ); + let err = include_window(&source, &AcceptAll, &pk, &manifest_digest(&bytes), "wasm") + .unwrap_err(); + assert!(err.reason.contains("connection reset"), "{err}"); + } + /// No status at all is a window nobody stated — carried as `None`, so /// `cap_by_includes` refuses it (clause 2) rather than skipping it. // rivet: verifies REQ-COMPOSEWINDOW-001 From 94f0d48650c3067dcd5b7187131d170905cb087b Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Thu, 15 Oct 2026 23:58:35 +0200 Subject: [PATCH 4/5] One prefix replacement with no index arithmetic, so a mutant cannot exhaust the runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The varve-core (export) mutation shard died twice on this PR with "the runner has received a shutdown signal" right after the same mutant: sdkexport.rs's `i = hit + built.len()` mutated to `hit * built.len()`, which never advances when the hit is at 0 and appends on every pass — so it did not merely hang, it grew a buffer until the VM ran out of memory. On main it happened to reach its 120 s timeout first. The three copies of that loop (text path, per-string binary path, SYSDIRS table) are now one replace_all that advances with split_at: no operator for the mutant to turn. Local cargo-mutants on it and its callers: 65 tested, 0 missed, 0 timeouts. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu --- crates/varve-core/src/sdkexport.rs | 58 +++++++++++++++--------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/crates/varve-core/src/sdkexport.rs b/crates/varve-core/src/sdkexport.rs index 339e505..04de213 100644 --- a/crates/varve-core/src/sdkexport.rs +++ b/crates/varve-core/src/sdkexport.rs @@ -216,6 +216,31 @@ fn find_sub(haystack: &[u8], needle: &[u8], from: usize) -> Option { (from..=haystack.len() - needle.len()).find(|&i| &haystack[i..i + needle.len()] == needle) } +/// Every occurrence of `from` in `hay` replaced by `to`, and how many there +/// were. ONE copy, used by the text path, the per-string binary path and the +/// SYSDIRS table. +/// +/// Written without index arithmetic on purpose. The three loops this replaced +/// advanced with `i = hit + from.len()`, and cargo-mutants' `+` → `*` turned +/// that into a loop that never advances when the hit is at 0 — while +/// appending on every pass. It did not just hang: it grew a buffer until the +/// CI runner ran out of memory and was shut down, twice, taking the whole +/// mutation shard with it. `split_at` leaves no operator for that mutant. +fn replace_all(hay: &[u8], from: &[u8], to: &[u8]) -> (Vec, usize) { + let mut out = Vec::with_capacity(hay.len()); + let mut rest = hay; + let mut count = 0; + while let Some(at) = find_sub(rest, from, 0) { + let (head, tail) = rest.split_at(at); + out.extend_from_slice(head); + out.extend_from_slice(to); + rest = tail.get(from.len()..).unwrap_or_default(); + count += 1; + } + out.extend_from_slice(rest); + (out, count) +} + /// What relocating one member's bytes produced. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Relocation { @@ -253,16 +278,7 @@ pub fn relocate_bytes( if !is_binary(bytes) { // The `sed -i` half: no field to overflow, so the text may change length. - let mut out = Vec::with_capacity(bytes.len()); - let mut i = 0; - let mut substitutions = 0; - while let Some(hit) = find_sub(bytes, built, i) { - out.extend_from_slice(&bytes[i..hit]); - out.extend_from_slice(dest); - i = hit + built.len(); - substitutions += 1; - } - out.extend_from_slice(&bytes[i..]); + let (out, substitutions) = replace_all(bytes, built, dest); return Ok(Relocation { bytes: out, fields: 0, @@ -317,15 +333,7 @@ pub fn relocate_bytes( let capacity = pad_end - start; // Replace every occurrence of the prefix WITHIN this one string. - let old = out[start..end].to_vec(); - let mut new = Vec::with_capacity(old.len()); - let mut i = 0; - while let Some(h) = find_sub(&old, built, i) { - new.extend_from_slice(&old[i..h]); - new.extend_from_slice(dest); - i = h + built.len(); - } - new.extend_from_slice(&old[i..]); + let (new, _) = replace_all(&out[start..end], built, dest); // `if len(new_dl_path) >= p_filesz: ERROR` — transcribed, not // paraphrased. The destination check has already made this @@ -447,17 +455,7 @@ fn relocate_sysdirs( let paths: Vec> = table .split(|b| *b == 0) .take_while(|p| !p.is_empty()) - .map(|p| { - let mut new = Vec::with_capacity(p.len()); - let mut i = 0; - while let Some(h) = find_sub(p, built, i) { - new.extend_from_slice(&p[i..h]); - new.extend_from_slice(dest); - i = h + built.len(); - } - new.extend_from_slice(&p[i..]); - new - }) + .map(|p| replace_all(p, built, dest).0) .collect(); let width = if class64 { 8 } else { 4 }; let slots = len_size / width; From fe7025917aa701b94df14a1d13622a20acf24027 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Fri, 16 Oct 2026 06:18:50 +0200 Subject: [PATCH 5/5] Every relocation loop consumes input or stops, whatever find_sub answers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit's replace_all still let the mutant `find_sub -> Some(0)` spin forever on an empty rest, appending each pass — the runner ran out of memory and was shut down right after the baseline (export shard, attempt on fde9b03). My local check had filtered mutants to replace_all and relocate_*, so find_sub's own mutants never ran against it. replace_all now advances only past a match lying wholly inside the remaining input, and relocate_bytes' outer loop stops on a hit behind its cursor instead of re-patching the same field until the test times out. Local cargo-mutants over the whole of sdkexport.rs: the two find_sub mutants that hung are now caught; on find_sub/relocate_bytes/replace_all, 46 tested, 44 caught, 2 unviable, 0 timeouts. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019TNtfRjLNhEz82G2ggeeNu --- crates/varve-core/src/sdkexport.rs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/crates/varve-core/src/sdkexport.rs b/crates/varve-core/src/sdkexport.rs index 04de213..caa856c 100644 --- a/crates/varve-core/src/sdkexport.rs +++ b/crates/varve-core/src/sdkexport.rs @@ -225,16 +225,25 @@ fn find_sub(haystack: &[u8], needle: &[u8], from: usize) -> Option { /// that into a loop that never advances when the hit is at 0 — while /// appending on every pass. It did not just hang: it grew a buffer until the /// CI runner ran out of memory and was shut down, twice, taking the whole -/// mutation shard with it. `split_at` leaves no operator for that mutant. +/// mutation shard with it. Slicing with `get` leaves no operator for that +/// mutant, and the loop below cannot run without consuming input. fn replace_all(hay: &[u8], from: &[u8], to: &[u8]) -> (Vec, usize) { let mut out = Vec::with_capacity(hay.len()); let mut rest = hay; let mut count = 0; + // Every pass must CONSUME input or stop. Advancing only past a match + // that lies wholly inside `rest` makes termination independent of what + // `find_sub` answers: a mutant `find_sub` that always says `Some(0)` + // once spun this loop forever on an empty `rest`, appending each time, + // until the CI runner ran out of memory — the failure it was written to + // end, reached by a different mutant (2026-10-16). while let Some(at) = find_sub(rest, from, 0) { - let (head, tail) = rest.split_at(at); - out.extend_from_slice(head); + let Some(tail) = rest.get(at..).and_then(|t| t.get(from.len()..)) else { + break; + }; + out.extend_from_slice(&rest[..at]); out.extend_from_slice(to); - rest = tail.get(from.len()..).unwrap_or_default(); + rest = tail; count += 1; } out.extend_from_slice(rest); @@ -294,6 +303,12 @@ pub fn relocate_bytes( let mut fields = sysdirs.as_ref().map_or(0, |(_, n)| *n); let mut cursor = 0; while let Some(hit) = find_sub(&out, built, cursor) { + // A hit behind the cursor is a search that did not advance; stop + // rather than re-patch the same field forever (a mutant `find_sub` + // returning a fixed position hung here until the test timed out). + if hit < cursor { + break; + } if let Some((range, _)) = &sysdirs && range.contains(&hit) {