Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ after its public API and format compatibility policies are established.

## [Unreleased]

- Update rustix to 1.1.5 across the library, repository tools, process-spawn boundary and fuzz lockfile, retaining the existing filesystem and process contracts (#102).

- Isolate durable locator subprocess laws from golden-store writer handoffs so their child launches cannot inherit another law's live lock descriptions (#178).

- Update the repository-only YAML parser to yaml-rust2 0.13.0 and refresh its dependency admission, retaining existing workflow and Dependabot refusal expectations (#103).

- Update the development-only Markdown validation graph and exact admission policy to markdownlint-cli2 0.23.3, addressing the js-yaml, smol-toml and markdown-it advisories while documenting the separate remaining braces advisory (#106).
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ repository-tasks = []
blake3 = { version = "=1.8.5", default-features = false, features = ["pure", "std"] }
cap-fs-ext = { version = "=4.0.2", default-features = false, features = ["std"] }
cap-std = { version = "=4.0.2", default-features = false }
rustix = { version = "=1.1.4", default-features = false, features = ["fs", "std"] }
rustix = { version = "=1.1.5", default-features = false, features = ["fs", "std"] }

[dev-dependencies]
allocation-counter = { version = "=0.8.1", default-features = false }
Expand Down
17 changes: 11 additions & 6 deletions docs/dependencies/cap-std-and-cap-fs-ext-4.0.2.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Dependency Admission: cap-std, cap-fs-ext 4.0.2, and rustix 1.1.4
# Dependency Admission: cap-std, cap-fs-ext 4.0.2, and rustix 1.1.5

- Status: Accepted for repository-task and segment-store filesystem boundaries
- Date: 2026-07-26
- Rustix admission updated: 2026-10-03 (#102)
- Owner: Keep repository verification
- Upstream:
[bytecodealliance/cap-std](https://github.com/bytecodealliance/cap-std)
Expand All @@ -10,7 +11,7 @@

Keep admits the exactly pinned `cap-std` 4.0.2 and `cap-fs-ext` 4.0.2 packages
for the library's segment-store filesystem adapter and behind the `xtask`
crate's `repository-tasks` feature. The library also admits Rustix 1.1.4 for
crate's `repository-tasks` feature. The library also admits Rustix 1.1.5 for
safe Linux filesystem-profile inspection and no-symlink root opening; `xtask`
uses the same exact version behind `repository-tasks`.

Expand Down Expand Up @@ -91,7 +92,7 @@ The locked non-Windows graph introduced for this boundary is:
- `linux-raw-sys` 0.12.1;
- `maybe-owned` 0.3.4;
- `once_cell` 1.21.4;
- `rustix` 1.1.4; and
- `rustix` 1.1.5; and
- `rustix-linux-procfs` 0.1.1.

Windows resolution additionally retains the locked `windows-sys`,
Expand All @@ -108,9 +109,7 @@ license through repository policy. Rustix declares `Apache-2.0 OR MIT`.
The locked `winx` 0.36.4 transitive package declares only
`Apache-2.0 WITH LLVM-exception`, so `deny.toml` admits that exact package and
license combination rather than broadening the global license allowlist.
Their manifests declare no Rust-version floor. Compatibility is therefore
established only by Keep's pinned stable, MSRV, debug, release, Clippy,
dependency-policy, and advisory lanes.
The cap-std and cap-fs-ext manifests declare no Rust-version floor. Rustix 1.1.5 declares Rust 1.65, below Keep's pinned Rust 1.96.0; its previous 1.1.4 release declared 1.63. The version floor is compatibility metadata, not execution evidence. Keep's debug, release, Clippy, dependency-policy and advisory lanes remain required for the updated graph.

The admitted packages and their platform dependencies may contain unsafe code
around operating-system calls and handles. Keep-owned code invokes only their
Expand Down Expand Up @@ -140,3 +139,9 @@ whole-process-group cleanup tests on every supported platform.
Reopen this admission if either direct version, selected feature, resolved
graph, license, supported platform, handle-retention invariant, or
repository-task-only boundary changes.

## Rustix 1.1.5 update

The root, repository-task and isolated process-spawn manifests select the same exact Rustix version. The independently locked fuzz workspace also selects 1.1.5. The upgrade does not change selected features, Keep source or existing test expectations; filesystem admission, writer/reader locks, typed failures and bounded subprocess cleanup retain their existing contracts.

Current validation must exercise the admitted Linux filesystem and process boundaries with the new graph. Historical recovery, crash and benchmark receipts remain evidence for their recorded builds, not new measurements of this dependency release. Finite conformance and regression runs do not establish universal equivalence or validation of every upstream platform.
29 changes: 29 additions & 0 deletions docs/testing-evidence/durable-locator-isolation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Durable locator process isolation

Change kind: test-isolation bug fix for #178. Owner: `@flyingrobots`. The product oracles remain exact durable bytes, catalogued layout identity, typed refusal and original I/O sources; no expected product outcome changes. This record distinguishes observed runtime failure, a controlled kernel experiment, and the scheduling boundary introduced by the correction.

## Observed RED

Main `7a21faebdbed38c386db14873966d433754c6eb4` failed the release golden worldline law `suite::durable_layout_laws::supplied_range_layouts_cannot_replace_the_catalogued_target_binding` with `WriterLock { source: Busy }` in [run 37164344603](https://github.com/flyingrobots/keep/actions/runs/37164344603). The [failure excerpt](durable-locator-isolation/hosted-red.txt) retains the original diagnostics and timestamps with line-end whitespace normalized. Earlier green runs are not substituted for this observed RED.

The failed law calls the real store fixture before asserting layout refusal. That fixture relinquishes and reacquires writer authority between catalog publication, migration and retention publication. Two sibling locator laws launched child processes from the same test executable. A child can inherit another thread's open flock descriptions until exec, extending their lifetime past the parent guard's drop. The log does not identify which handoff refused or prove that this schedule caused the hosted failure.

## Controlled mechanism and limits

On parent `3165890e9291cfb5fe10e81a9d7cd151f3e59464`, a separate diagnostic crate opened production Keep authority, held a child before exec with pipe handshakes, dropped the parent authority and observed exact public `WriterLockAcquireError::Busy`. After releasing and reaping the child, public acquisition succeeded. The [receipt](durable-locator-isolation/controlled-inheritance.txt) and [diagnostic source](durable-locator-isolation/probe-source.txt) preserve the experiment. No sleep or probabilistic workload determines that schedule.

The diagnostic's isolated unsafe pre-exec hook performs only raw pipe reads and writes; it is not linked into Keep, added to the test suite, or used as a merge gate. It demonstrates an actual inherited-descriptor lifetime, not the exact unobserved CI trace. A separate strace run delaying syscall completion passed the original suite; it found no failing schedule and supplies no proof of absence. Keep production source and the failing test's assertions are identical between the observed RED revision and this parent.

## Correction and retained contracts

The two existing locator laws now run in `tests/durable_locator.rs`, a separate integration-test executable. Its parent creates no store or writer authority; each admitted child runs exactly one locator law serially and creates its own stores. The golden worldline executable no longer launches those children. Parallel execution of the two executables does not share their descriptor tables, so a locator child cannot inherit the golden executable's store locks.

The locator laws still check that a relative handle keeps its original store after a working-directory change and that a deleted working directory preserves its exact NotFound cause. The golden layout-binding law retains its exact LayoutMissing checks and unchanged output sentinel. No law is deleted, ignored, retried or globally serialized. The locator module and its exact child selectors remain byte-identical to the original. Shared fixture imports allow unused partial-record constructors and explicit sandbox removal only in the new locator executable; other filesystem laws still exercise those operations.

Keep's authority handoffs, flock semantics, public errors, APIs, formats and recovery protocols do not change. This correction removes the demonstrated interference mechanism from this suite without asserting that every possible cause of Busy has been eliminated. Any new failure remains a defect to diagnose, not a retry instruction.

## Validation profile

These unchanged laws remain medium tests using owned Linux ext4 scratch and real filesystem/process operations. The locator child retains its existing 20-second watchdog; that is an execution ceiling, not a latency promise. Focused validation runs both executables in debug and release inside copied Docker source, followed by the required stable-candidate chain and independent exact-head review. Commands and terminal results are recorded on the corrective PR; no pending execution is described as passing here.

The original ordinary-Cargo resource enforcement gaps remain disclosed in the [enforcement profile](../testing/enforcement.md). Moving existing assertions does not establish new per-test memory limits, egress isolation, suite latency measurements or exhaustive scheduler exploration. #106's unrelated version-update waiver does not cover this change. No new assertion calibration or artificial fixture-count test is substituted for the existing runtime failure and retained product laws. Retire the separation if subprocess creation is removed or another verified process-isolation boundary makes inherited lock interference impossible.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Locking 50 packages to latest Rust 1.96.0 compatible versions
Adding rustix v1.1.4 (available: v1.1.5)
Compiling rustix v1.1.4
Compiling linux-raw-sys v0.12.1
Compiling io-lifetimes v2.0.4
Compiling io-lifetimes v3.0.1
Compiling bitflags v2.13.1
Compiling io-extras v0.19.0
Compiling once_cell v1.21.4
Compiling find-msvc-tools v0.1.9
Compiling cap-primitives v4.0.2
Compiling shlex v2.0.1
Compiling ipnet v2.12.0
Compiling maybe-owned v0.3.4
Compiling cap-std v4.0.2
Compiling ambient-authority v0.0.2
Compiling cap-fs-ext v4.0.2
Compiling constant_time_eq v0.4.2
Compiling cfg-if v1.0.4
Compiling arrayvec v0.7.8
Compiling arrayref v0.3.9
Compiling cc v1.3.0
Compiling blake3 v1.8.5
Compiling rustix-linux-procfs v0.1.1
Compiling fs-set-times v0.20.3
Compiling keep v0.0.0 (/build/keep178-parent-source)
Compiling keep-inherited-lock-probe v0.0.0 (/build/keep178-inheritance-probe)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.14s
controlled pre-exec child: parent drop => Busy; child exec/reap => acquired
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Rust quality gates Test release profile 2026-10-04T00:24:47.3413241Z ---- suite::durable_layout_laws::supplied_range_layouts_cannot_replace_the_catalogued_target_binding stdout ----
Rust quality gates Test release profile 2026-10-04T00:24:47.3414320Z Error: WriterLock { source: Busy }
Rust quality gates Test release profile 2026-10-04T00:24:47.3414537Z
Rust quality gates Test release profile 2026-10-04T00:24:47.3414541Z
Rust quality gates Test release profile 2026-10-04T00:24:47.3414631Z failures:
Rust quality gates Test release profile 2026-10-04T00:24:47.3415069Z suite::durable_layout_laws::supplied_range_layouts_cannot_replace_the_catalogued_target_binding
Rust quality gates Test release profile 2026-10-04T00:24:47.3415356Z
Rust quality gates Test release profile 2026-10-04T00:24:47.3415561Z test result: FAILED. 51 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.67s
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
Diagnostic Cargo.toml (replace the Keep path with an isolated parent checkout):

[package]
name = "keep-inherited-lock-probe"
version = "0.0.0"
edition = "2024"
[dependencies]
keep = { path = "/build/keep178-parent-source" }
rustix = { version = "=1.1.4", default-features = false, features = ["pipe", "std"] }

Diagnostic src/main.rs:

//! Isolated diagnostic crate; never linked into Keep or used as a CI gate.
//! A pipe handshake controls the inherited-descriptor lifetime across pre-exec.
//! The hook performs only async-signal-safe read/write syscalls, without allocation.
use std::{error::Error, io, os::unix::process::CommandExt, path::Path, process::Command};
use keep::{FilesystemPlatformAdmission, FilesystemWriterLock, WriterLockAcquireError};
use rustix::pipe::{PipeFlags, pipe_with};

fn main() -> Result<(), Box<dyn Error>> {
let root_arg = std::env::args().nth(1).ok_or("scratch root argument missing")?;
let root = Path::new(&root_arg);
std::fs::create_dir(root)?;
let authority = FilesystemPlatformAdmission::initialize(root)?;
let (ready_read, ready_write) = pipe_with(PipeFlags::CLOEXEC)?;
let (release_read, release_write) = pipe_with(PipeFlags::CLOEXEC)?;
let mut command = Command::new("/bin/true");
// SAFETY: the child hook touches only its captured owned descriptors, uses
// async-signal-safe raw read/write, and does not allocate, lock or unwind.
unsafe {
command.pre_exec(move || {
if rustix::io::write(&ready_write, &[1])? != 1 {
return Err(io::Error::from(io::ErrorKind::WriteZero));
}
let mut byte = [0];
if rustix::io::read(&release_read, &mut byte)? != 1 {
return Err(io::Error::from(io::ErrorKind::UnexpectedEof));
}
Ok(())
});
}
let child = std::thread::spawn(move || command.status());
let mut ready = [0];
assert_eq!(rustix::io::read(&ready_read, &mut ready)?, 1);
drop(authority);
let during = FilesystemWriterLock::try_acquire(root);
// Release before asserting so even a failed diagnostic reaps its child.
assert_eq!(rustix::io::write(&release_write, &[1])?, 1);
let status = child.join().map_err(|_| "spawn thread panicked")??;
assert!(status.success());
assert!(matches!(during, Err(WriterLockAcquireError::Busy)),
"a child before exec must retain inherited writer authority");
let after = FilesystemWriterLock::try_acquire(root)?;
drop(after);
println!("controlled pre-exec child: parent drop => Busy; child exec/reap => acquired");
Ok(())
}
4 changes: 2 additions & 2 deletions fuzz/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion repository-process-spawn/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ license = "Apache-2.0"
publish = false

[dependencies]
rustix = { version = "=1.1.4", default-features = false, features = ["process", "std"] }
rustix = { version = "=1.1.5", default-features = false, features = ["process", "std"] }

[lints]
workspace = true
11 changes: 11 additions & 0 deletions tests/durable_locator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//! This executable owns the process-isolated durable locator laws.
//!
//! Child creation must not share a process with golden-store writer handoffs:
//! a child can inherit a live flock description until exec. Separate test
//! executables have separate descriptor tables even when Cargo runs in parallel.
//! The locator parent owns no store; each admitted child runs one law serially.

#![cfg(target_os = "linux")]

#[path = "durable_locator/suite.rs"]
mod suite;
16 changes: 16 additions & 0 deletions tests/durable_locator/suite.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//! This module owns the isolated locator suite's fixture imports.

#[path = "../golden_file_worldline/durable_fixture.rs"]
#[allow(
dead_code,
reason = "locator laws use complete stores; golden laws cover partial-record construction"
)]
mod durable_fixture;
#[path = "../golden_file_worldline/durable_locator_laws.rs"]
mod durable_locator_laws;
#[path = "../segment_filesystem_stage/sandbox.rs"]
#[allow(
dead_code,
reason = "locator laws use Drop cleanup; other filesystem laws check explicit removal"
)]
mod durable_sandbox;
3 changes: 0 additions & 3 deletions tests/golden_file_worldline/suite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ mod durable_fixture;
#[path = "durable_layout_laws.rs"]
mod durable_layout_laws;
#[cfg(target_os = "linux")]
#[path = "durable_locator_laws.rs"]
mod durable_locator_laws;
#[cfg(target_os = "linux")]
#[path = "durable_namespace_laws.rs"]
mod durable_namespace_laws;
#[cfg(target_os = "linux")]
Expand Down
2 changes: 1 addition & 1 deletion xtask/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ md-5 = { version = "=0.11.0", default-features = false, optional = true }
# Dedicated unsafe boundary sets exact child working directories by descriptor.
repository-process-spawn = { path = "../repository-process-spawn", optional = true }
# Safe POSIX descriptor flags and process-group signaling bound repository tools.
rustix = { version = "=1.1.4", default-features = false, features = ["fs", "process", "std"], optional = true }
rustix = { version = "=1.1.5", default-features = false, features = ["fs", "process", "std"], optional = true }
# Serde drives duplicate-refusing repository JSON admission; no types escape xtask.
serde = { version = "=1.0.229", default-features = false, features = ["std"], optional = true }
# Typed JSON admission checks the committed documentation-tool lock graph.
Expand Down
Loading