Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
776c199
pi-agent: U7 hardening matrix validator
ahrav Aug 25, 2026
1a2e1b7
pi-agent: U1 Rust strict decoders + fuzz
ahrav Aug 25, 2026
96dc47f
Merge branch 'pi-agent-fc07058b-9a6f-448' into feat/shm-failure-harde…
ahrav Aug 25, 2026
03c6224
pi-agent: U1 TS grant + N-API validation
ahrav Aug 25, 2026
0004833
Merge branch 'pi-agent-4be34b3b-6059-439' into feat/shm-failure-harde…
ahrav Aug 25, 2026
f514628
feat(mc-host): isolate crashed-client cleanup so a stale provider can…
ahrav Aug 25, 2026
81a4d98
feat(mc-host-client): recover from transient TCP fallback back to sha…
ahrav Aug 25, 2026
8a52d1a
test(mc-host): prove a SIGKILLed client cannot harm the daemon or ano…
ahrav Aug 25, 2026
f4d7f88
test(mc-host): detect resource leaks across repeated crash-recovery c…
ahrav Aug 25, 2026
3555515
ci(shm): block untested hardening claims from merging while the matri…
ahrav Aug 25, 2026
a9da6cd
fix(shm): close review gaps so weakened hardening tests cannot pass s…
ahrav Aug 25, 2026
340194c
refactor(mc-host-client): drop the unused recovery-deadline knob and …
ahrav Aug 25, 2026
073258b
fix(shm): align client grants and hardening gates
ahrav Aug 25, 2026
643225b
fix(shm): address review findings on replay scope, fallback reason, a…
ahrav Aug 26, 2026
e75f917
Merge remote-tracking branch 'origin/main' into feat/shm-failure-hard…
ahrav Aug 26, 2026
6ce4174
fix(shm): close three review gaps in recovery, drain, and soak gating
ahrav Aug 26, 2026
c4c743e
fix(shm): make attach exclusivity process-wide and bound the fuzz cam…
ahrav Aug 26, 2026
7cd828a
fix(shm): admit candidates atomically with the readiness decision
ahrav Aug 26, 2026
c252efd
Merge remote-tracking branch 'origin/main' into feat/shm-failure-hard…
ahrav Aug 26, 2026
babdf81
fix(shm): keep a draining predecessor alive until callers release its…
ahrav Aug 26, 2026
acd7ce7
fix(shm): classify failed connection-file stats as discovery churn
ahrav Aug 26, 2026
a2c1dbc
fix(shm): split stat errno classes and key the replay watermark by da…
ahrav Aug 26, 2026
a3316f5
fix(shm): split open errno classes and floor active caps at one candi…
ahrav Aug 26, 2026
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
24 changes: 24 additions & 0 deletions .config/nextest.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Serialize the shared-memory crash tests. commentlint: allow(JUDGE)
[test-groups]
shm-crash = { max-threads = 1 }

[[profile.default.overrides]]
filter = 'package(mc-host) and binary(shm_failure_modes)'
test-group = 'shm-crash'

# The soak binary uses shm-crash serialization. commentlint: allow(JUDGE)
[[profile.default.overrides]]
filter = 'package(mc-host) and binary(shm_soak)'
test-group = 'shm-crash'
slow-timeout = { period = "120s", terminate-after = 5 }

# Run the ignored full soak with:
# cargo nextest run -P shm-soak --run-ignored ignored-only
# commentlint: allow(JUDGE)
[profile.shm-soak]
default-filter = 'package(mc-host) and binary(shm_soak) and test(full_soak_cycles_conserve_resources)'

[[profile.shm-soak.overrides]]
filter = 'package(mc-host) and binary(shm_soak)'
test-group = 'shm-crash'
slow-timeout = { period = "300s", terminate-after = 12 }
68 changes: 66 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,65 @@ permissions:
# gauntlet.

jobs:
shm-hardening-gate:
name: SHM failure-hardening matrix gate
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false

- uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun install --frozen-lockfile
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Matrix validator unit tests
working-directory: packages/e2e-tests
run: bun test scripts/validate-shm-hardening-matrix.test.ts

# --allow-unresolved keeps an unresolved manifest loud but
# non-fatal; a FROZEN manifest is validated strictly with no
# workflow edit. commentlint: allow(JUDGE)
- name: Validate hardening matrix (strict when frozen)
working-directory: packages/e2e-tests
run: bun scripts/validate-shm-hardening-matrix.ts --allow-unresolved

shm-crash-recovery:
name: SHM crash isolation + recovery (Linux, provisional ring tuple)
runs-on: ubuntu-latest
needs: [shm-hardening-gate]
timeout-minutes: 45
permissions:
contents: read
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false

- uses: dtolnay/rust-toolchain@stable

- uses: taiki-e/install-action@nextest

- name: Provision metadata-only sibling stubs
run: sh scripts/provision-rust-ci-stubs.sh

# Serialized shm-crash nextest group; the ignored full soak
# stays opt-in via shm-hardening-optin.yml; no artifact upload.
# The --lib pass runs the provider_recovery controller unit
# suite (stale retries, deadline isolation, wedged cleanup,
# late-result fencing, inbox bounds, custody accounting), which
# no integration binary exercises. commentlint: allow(JUDGE)
- name: Crash, recovery, and soak-smoke suites
run: |
cargo nextest run -p mc-host --lib \
--test shm_failure_modes --test shm_soak

shm-source-build:
name: Shared memory source build (${{ matrix.os }})
runs-on: ${{ matrix.os }}
Expand Down Expand Up @@ -79,10 +138,15 @@ jobs:
cargo nextest run -p mc-host \
--test shm_transport --test transport_negotiation

- name: Rust contracts and clean host omission (macOS)
# No retained macOS provider: Linux-gated crash/soak harnesses
# are absent by cfg, so this proves side-effect-free omission
# (R15), not active parity. commentlint: allow(JUDGE)
- name: Contracts, observer self-tests, and omission proof (macOS)
if: runner.os == 'macOS'
run: |
cargo nextest run -p mc-shm-transport --test contract
cargo nextest run -p mc-shm-transport \
--test contract --test fuzz_corpus
cargo nextest run -p mc-host --test shm_soak
cargo test -p mc-host --lib \
shm_provider::tests::platform_preflight_is_side_effect_free

Expand Down
81 changes: 81 additions & 0 deletions .github/workflows/shm-hardening-optin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: SHM hardening opt-in

on:
workflow_dispatch:
inputs:
soak_cycles:
description: Measured soak cycles (MC_SHM_SOAK_CYCLES)
required: false
default: "1000"
fuzz_seconds:
description: Per-target libFuzzer -max_total_time seconds
required: false
default: "60"

permissions:
contents: read

jobs:
full-soak:
name: Full resource soak (Linux, provisional ring tuple)
runs-on: ubuntu-latest
timeout-minutes: 180
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false

- uses: dtolnay/rust-toolchain@stable

- uses: taiki-e/install-action@nextest

- name: Provision metadata-only sibling stubs
run: sh scripts/provision-rust-ci-stubs.sh

- name: Ignored full soak (shm-soak nextest profile)
env:
MC_SHM_SOAK_CYCLES: ${{ inputs.soak_cycles }}
run: cargo nextest run -P shm-soak --run-ignored ignored-only

bounded-fuzz:
name: Bounded libFuzzer campaign (nested fuzz workspace)
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false

- uses: dtolnay/rust-toolchain@nightly

- name: Install cargo-fuzz
run: cargo install cargo-fuzz --locked

- name: Nested fuzz workspace fmt and build
working-directory: crates/mc-shm-transport/fuzz
run: |
cargo fmt --check
cargo check --locked

- name: Bounded fuzz per target
working-directory: crates/mc-shm-transport
env:
FUZZ_SECONDS: ${{ inputs.fuzz_seconds }}
run: |
case "$FUZZ_SECONDS" in
''|*[!0-9]*)
echo "fuzz_seconds must be an unsigned integer, got: $FUZZ_SECONDS" >&2
exit 1
;;
Comment on lines +65 to +69

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject a zero-second fuzz campaign

If a manual dispatch supplies fuzz_seconds as 0, this validation accepts it and invokes every target with -max_total_time=0. Checked with a local libFuzzer binary's -help=1: max_total_time limits execution only "if positive," while the default run count is infinite, so the first target runs until the 60-minute workflow timeout and the remaining targets never execute. Require a positive integer here, as the job promises a bounded per-target campaign.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c4c743e — the workflow now rejects fuzz_seconds: 0 explicitly (in addition to the unsigned-integer check), since -max_total_time only bounds the run when positive and a zero dispatch would run the first target to the 60-minute job timeout while starving the other two.

esac
# libFuzzer's -max_total_time bounds the run only when positive;
# zero would run the first target until the workflow timeout and
# starve the remaining targets.
if [ "$FUZZ_SECONDS" -eq 0 ]; then
echo "fuzz_seconds must be a positive number of seconds" >&2
exit 1
fi
for target in frame_descriptor provider_grant provider_sample; do
cargo +nightly fuzz run "$target" -- \
-max_total_time="$FUZZ_SECONDS"
done
1 change: 1 addition & 0 deletions Cargo.lock

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

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ resolver = "2"
members = ["crates/mc-core", "crates/mc-store", "crates/mc-host", "crates/mc-module", "crates/mc-tokenizer", "crates/mc-shm-transport", "packages/mc-shm-native"]
# The evidence probe depends on the PUBLISHED subc crates from crates.io rather than the
# sibling sources, so it must never join this workspace's dependency graph.
exclude = ["packages/dashboard/src-tauri", "docs/evidence/subc-surface-probe"]
# The cargo-fuzz workspace under crates/mc-shm-transport/fuzz declares its own
# [workspace] and stays excluded here.
exclude = ["packages/dashboard/src-tauri", "docs/evidence/subc-surface-probe", "crates/mc-shm-transport/fuzz"]

[workspace.dependencies]
# CortexKit cache-stability core + storage substrate. Sibling path-deps for local
Expand Down
1 change: 1 addition & 0 deletions crates/mc-host/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ getrandom = "0.2"
mc-shm-transport = { path = "../mc-shm-transport", default-features = false }

[dev-dependencies]
libc = "0.2"
tempfile = "3"
tokio = { workspace = true, features = ["test-util", "signal"] }
# `stdio` only serves the broca_subprocess fixture that replaces its own
Expand Down
66 changes: 40 additions & 26 deletions crates/mc-host/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ use crate::transport_negotiation::{
};
use crate::transport_provider::{
fresh_activation_token, Candidate, GrantBinding, GrantRecord, InjectedProvider,
PreparedCandidate, ProviderContext, TCP_CAPABILITY_VERSION,
PreflightEligibility, PreparedCandidate, ProviderContext, TCP_CAPABILITY_VERSION,
};
use crate::wire::{encode_owned_frame, pure_header_flags, response_flags, FrameId};

Expand Down Expand Up @@ -863,59 +863,73 @@ async fn handle_negotiate<H: McHostHandler>(
}
// The first serveable offer in client preference order wins.
let mut capability_mismatch = false;
let mut non_tcp_offered = false;
let mut dynamically_unavailable = false;
for offer in &request.offers {
if offer.transport == TRANSPORT_TCP {
if offer.capability_version == TCP_CAPABILITY_VERSION {
break;
}
continue;
}
non_tcp_offered = true;
// Provider identity is `(transport, capability_version)`: a
// name-only lookup would hide a serveable provider behind a
// mismatched sibling at the same name.
match shared
.providers
.find(&offer.transport, offer.capability_version)
{
Some(provider)
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
Some(provider) => {
// A panicking preflight fails toward static omission: reasonless TCP and no client probe (KTD6). commentlint: allow(JUDGE)
let eligibility = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
crate::panic_boundary::redact_sync(|| {
provider.preflight(offer.parameters.as_ref())
})
}))
.unwrap_or(false) =>
{
let provider = Arc::clone(provider);
let selected = SelectedTransport {
transport: offer.transport.clone(),
capability_version: offer.capability_version,
};
return grant_candidate(
shared,
gen,
corr,
selected,
provider,
offer.parameters.clone(),
setup,
)
.await;
.unwrap_or(PreflightEligibility::StaticallyOmitted);
match eligibility {
PreflightEligibility::Serveable => {
let provider = Arc::clone(provider);
let selected = SelectedTransport {
transport: offer.transport.clone(),
capability_version: offer.capability_version,
};
return grant_candidate(
shared,
gen,
corr,
selected,
provider,
offer.parameters.clone(),
setup,
)
.await;
}
// Exact `unavailable` is reserved for an installed, statically eligible provider's dynamic readiness or admission pressure (KTD6). commentlint: allow(JUDGE)
PreflightEligibility::DynamicallyUnavailable => {
dynamically_unavailable = true;
}
PreflightEligibility::StaticallyOmitted => {}
}
}
Some(_) => {}
// Known transport at another version: name the real cause
// (§7.7.3) rather than reporting it as unavailable.
None if shared.providers.serves_transport(&offer.transport) => {
capability_mismatch = true;
}
// Permanent absence selects reasonless TCP, never `unavailable`, so a client cannot probe for a provider that cannot appear (KTD6). commentlint: allow(JUDGE)
None => {}
}
}
let reason = if capability_mismatch {
Some(FallbackReason::CapabilityVersionMismatch)
} else if non_tcp_offered {
// `unavailable` outranks `capability_version_mismatch` across the
// evaluated offers: it is the only reason that authorizes a client
// re-upgrade probe (§7.7.3), and a dynamically unavailable eligible
// offer is transient — a later probe can succeed. Reporting a static
// mismatch from a lower-preference sibling would permanently suppress
// recovery of the unavailable transport.
let reason = if dynamically_unavailable {
Some(FallbackReason::Unavailable)
} else if capability_mismatch {
Some(FallbackReason::CapabilityVersionMismatch)
} else {
None
};
Expand Down
2 changes: 2 additions & 0 deletions crates/mc-host/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub mod connection_file;
pub mod handler;
pub mod lifecycle;
#[doc(hidden)]
pub mod provider_recovery;
#[doc(hidden)]
pub mod shm_provider;
pub mod synapse;
#[doc(hidden)]
Expand Down
Loading
Loading