Repository navigation
External Action Boundary Evaluation P1b: local doubles, isolation, reference guard and XM gate - #22
Merged
Merged
Conversation
Timing and retry values, the retry hooks and order proofs, the keep-alive group window, isolation and privileges, recipient doubles, closure, the reference guard rules and XM mutants, the gate, the P1b profile, run record, schema extension and the canonical comparison form.
…ference guard and XM patches Recipient doubles P and Q with hash-chained journals, fences and control sockets; canary double; netem backlog reader; executor (guard host) and per-identity forwarders; case runner (driver and observation collector); root orchestrator with per-case namespaces, packet filter, netem, cgroup and mount hiding; environment check; run validity layer; canonical comparison form; P1b profile and run configuration; observation source and report kind extended without changing P1a bytes.
…recheck Witness cases, required verdicts, required decision and effect findings and the evidence kind of each mutant, derived from the cases, the oracle and the mutant semantics. Exact pins and named deviations are left empty for the precheck.
…s and CI job The gate builds each XM mutant from its rule-confined patch and checks the whole run against the declared assertions and exact pins; precheck output never passes. Privileged regressions PR1-PR7 inject missing isolation, a writable or missing observation channel, a failed provisional closure, a forbidden effect beside cleanup and a separate retry state. The CI job runs the environment check, two control runs with a canonical comparison, the recorded-CLI comparison, the mutant runs, the gate and the regressions.
…y link exists On the CI kernel the link-local IPv6 addresses of the veth links changed state during longer cases, which the configuration record rightly reported as an isolation change. The record now also names the changed lines.
…eeps the normalized configuration lines The netem qdisc shows a random seed that differs between runs (it stays the same within a case, so the before/after check is unchanged). The canonical form now keeps the normalized configuration lines rather than their hash, so a difference names the changed line.
…ndles as per-run identifiers
…k to the invoking user
…iations Exact per-case pins of the control and of every mutant over the full P1b profile, from the precheck of CI run 38503539437 (identical on Node 22 and Node 24). Every declared witness assertion held. Named deviations: XM11's harness condition in retry-once and retry-delayed, and XM13b's T_q overrun in the three cases with a denied HTTP-tool request. The declared assertions are unchanged.
…dout goes to its own directory
…ed results, inferences and open gaps
… attempt and prevent executor descendant processes Review finding 1: the guard could route one attempt's forward through another attempt's forwarder by changing the request id, so a per-attempt address did not by itself give unforgeable attribution. The executor now binds the forwarding right to the attempt the harness dispatched, carried across the guard's async handling (and its deferred, signal-triggered sends) by an AsyncLocalStorage store, and routes every forward through that attempt's own forwarder whatever id the guard supplies. Each forwarder serves only its own identity's attempts and rejects any other. A component test with substituted forwarder sockets reproduces the original attack and shows it is now bound. Review finding 2: natural quiescence covered only the orchestrator-started processes plus an empty cgroup, so a descendant that exited by a signal could pass unseen. The executor and forwarders (the only processes in the case cgroup) now run under Node's permission model with child-process and worker creation denied, so no descendant process can be created and the harness-started set is the whole process tree. The executor confirms the prevention at startup and the run validity layer fails the case otherwise. Regression PR8 injects a guard that tries to spawn a child and asserts the denial. decisions.md canonicalization and the executor-quiescence and per-attempt identity decisions are aligned with the implementation; p1b.md updated.
… and state the trust boundary Review follow-up on finding 1. The guard runs in-process, so an in-process binding must not be reachable and no guard-supplied id may be a fallback: - The AsyncLocalStorage that carries the forwarding right is no longer exported; it is module-private, so the guard has no reference to open a binding for an identity it was not dispatched. A guard's own AsyncLocalStorage cannot change the executor's. - afterSignal takes the deferred send's identity only from the active dispatch binding. A deferred send registered outside a dispatched attempt is refused (recorded as a technical error), never bound to the id the guard passed. Regressions cover both: a guard that wraps the forward in its own AsyncLocalStorage still routes through the dispatched attempt's forwarder; the binding store is not exported; a deferred send naming another attempt binds to the dispatch and a signal for the other attempt never fires it; a deferred send with no active dispatch is refused. decisions.md and p1b.md now state the trust boundary precisely: attribution is grounded in the peer address the doubles record (per-identity network namespaces, outside the guard process); the env interface makes the right unforgeable for a guard that uses it (the reference guard and every XM mutant do); P1b does not sandbox arbitrary in-process guard code, which would need the guard and each identity's egress in separate processes or namespaces. This narrower claim is recorded for separate approval.
…oup's forwards settle The provisional closure of a keep-alive group's window used to fence as soon as the doubles reported no open connection. A forward the guard started but had not finished — notably XM12's fire-and-forget send under a reported DENY — then raced the fence: its effect at the recipient was journaled before or after the fence by host timing alone, so XM12 on retry-once and retry-delayed produced `:late` effect findings on some hosts and not others, and the mutant gate's exact pins differed between the push and pull_request CI runs on the same SHA. Close the window only once every forward the group's attempts started has settled (a `forward_done` for each `forward_started`), so such a forward has reached the recipient and its effect is journaled before the fence. The delay-element attempt is excluded on purpose: its traffic is held by the netem element so its effect is observed after its own fence, which the retry-delayed order proof relies on. Genuinely late effects are unaffected: the deferred-persist store still lands after the response, and an after-signal send has not started at fence time. Add the `forward_in_flight` window failure reason (type, observation schema) for a forward that never settles within T_w. Also scope the trust-boundary and descendant-prevention wording in decisions.md and p1b.md to the demonstrated claim: P1b evaluates a named reference guard and XM mutants that use the interface; it does not demonstrate preventing identity forgery, harness manipulation or descendant-process creation by arbitrary in-process guard code, and in particular does not demonstrate that a direct forwarder misuse surfaces as a detectable missing/unexpected effect rather than a silent misattribution.
… rule Extract the provisional-closure decision into two pure, exported helpers of the case runner — `groupForwardsSettled` (every `forward_started` the group waits on has a `forward_done`, the delay-element attempt excluded) and `windowClosure` (close / wait / fail with a reason) — leaving the loop's behaviour unchanged, and add deterministic tests for the rule that closed the XM12 `:late` race: - an in-flight forward of the group blocks the fence even when the doubles show no open connection (the fire-and-forget-under-DENY case); - `forward_done` releases the wait; a forward that never settles fails the window as `forward_in_flight` at T_w, distinct from `connections_open` and `fence_missing`; - the delay-element attempt (retry-delayed a1) is excluded from the wait while the retry a2 is waited on; - a send scheduled for after the closure signal has not started at fence time, so it does not block closure and stays genuinely late. Reverting the rule to close on no-open-connections alone (the behaviour before the fix) makes the first two tests fail, so the regression pins the fix rather than only the current code.
mikko-lab
marked this pull request as ready for review
October 14, 2026 09:23
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Work order "External Action Boundary Evaluation P1b". Draft; no merge, release, tag or version bump.
Results cover only the local synthetic reference guard and its planted XM mutants against local doubles under the profile
external-actions-p1b-local-reference. They are not a security result for any other guard or a real service.Start and head
8da7a608be603849e14f479ef774a28f8543e3c8. Main did not move during the work (re-checked before this update).1733a35(fix30c2a2a+ its regression test).Status
Under independent review. Three review rounds have been applied (see Review follow-up). Round 3 resolves a same-SHA CI difference, scopes the trust-boundary and descendant-prevention wording to the narrower, now-approved claim, and adds a dedicated regression for the new window-closure rule. Merge is not requested; final SHA approval awaits both CI runs (push and pull_request) on this head.
Pins (unchanged by the review fixes)
profiles/external-actions/p1b-local-reference.profile.jsonb279917161905d34cf0a8aff30940269e6b2ed6fe9796476d849ca40d5ee2843p1b-local-reference.run.json5ecbce524721ac10ddb4aa431f79eb00e6977cfb73566c329a090abb77b40058mutations/external-actions/guard-mutants.json3ea1d0df4329d55db1f81fa01d6c258156b5c886772b6e4db992c52dc0f21ca8d838caab3ccaf2d3809d6c0137e00e90f181dee6225338648a6f928027ce5442The decisions (
bdb15f2), the XM assertions declared before the precheck (ced72c4) and the pinned precheck results (78b615e) were each committed in order. Round 3 changes the harness timing of the provisional closure, the documentation and the tests only; the guard, the evaluator rules and the pinned per-case results are unchanged, and the gate still reproduces them.Environment
ubuntu-24.04, Node 22 and Node 24.iptables1.8.10 (nf_tables);sch_netem; cgroup v2 withcgroup.kill;setpriv; Node's permission model (child-process and worker creation denied) for the in-cgroup processes.sudo -l. The host's general security settings are untouched.sch_netem; there the full profile fails the environment check with exit 2 and no case is skipped, soretry-delayedand its mutants are exercised only in CI.What is in it
immediate,failing,deferred_persist, and the retry-once hold.Shared changes outside the P1b directory: the observation
sourceenum gainslocal_doubles; the report schema pairs the evidence kind with the source;buildReporttakes a kind parameter defaulting to P1a's; the evaluator lets a keep-alive group share a failed window. P1a bytes are unchanged.Review follow-up
Round 1 (
10db7d8).Round 2 (
520e302): two in-process bypasses of the identity binding, both closed with regressions.AsyncLocalStoragethat carries the forwarding right is now module-private (not exported), so the in-process guard has no reference to open a binding for an identity it was not dispatched; a guard's own store cannot change the executor's.afterSignaltakes the deferred send's identity only from the active dispatch binding (never the guard-supplied id), and refuses a deferred send registered outside a dispatch.Round 3 (
30c2a2a,1733a35): same-SHA CI difference, documentation scope, and a regression.520e302the push CI run passed but the pull_request run failed the P1b mutant gate (exit 2) on Node 24. The confirmed difference: XM12 onretry-onceandretry-delayedproduced:lateeffect findings that the pins do not carry (the verdict stayed VIOLATION, but the run did not reproduce the pinned timing). Root cause, from the raw observations: the provisional window was fenced as soon as the doubles reported no open connection, so XM12's fire-and-forget forward under a reported DENY raced the fence — its effect at the recipient was journaled before or after the fence by host timing alone. The fix fences a group's window only once every forward the group's attempts started has settled (aforward_donefor eachforward_started), so such a forward has reached the recipient and its effect precedes the fence. The delay-element attempt is excluded on purpose so that the retry-delayed order proof still observes its effect after its own fence. Lateness is neither removed from the comparison nor re-pinned: genuinely late effects are unchanged (the deferred-persist store still lands after the response and stays:late; an after-signal send has not started at fence time). A newforward_in_flightwindow failure reason covers a forward that never settles withinT_w.1733a35). The closure decision is extracted into two pure, exported helpers of the case runner (groupForwardsSettled,windowClosure) with the loop behaviour unchanged, and a deterministic test pins the rule: an in-flight forward blocks the fence even with no open connection;forward_donereleases it; an unsettled forward fails asforward_in_flightatT_w; the delay-element attempt is excluded while the retry is waited on; an after-signal send does not block closure and stays late. A sensitivity check of the old logic — temporarily reverting the closure rule in the working tree to close on no-open-connections alone — makes the two race tests fail, while all four pass on the fixed head; this is a local sensitivity test of the pre-fix rule, not a run against the unmodified520e302commit. It pins the fix rather than only the current code.decisions.mdandp1b.mdnow state the demonstrated claim consistently: P1b evaluates a named reference guard and XM mutants that use the interface; preventing arbitrary in-process guard code from forging identity, manipulating the harness, or creating descendant processes is not demonstrated. The earlier, undemonstrated general guarantee that a direct forwarder misuse yields a detectable missing/unexpected effect rather than a silent misattribution is removed, and the one place describing descendant-process prevention as absolute is scoped to a guard that uses the interface.Local validation of Round 3
Typecheck/build OK; 55/55 external-actions tests (34 P1a + 21 P1b) pass; the oracle-boundary check passes. With no local
sch_netem, the XM12 cases that do not need it were run directly:retry-once,fill-no-submitanddeferred-persist-forbiddenreproduce the pins exactly (retry-oncehas no:late;deferred-persist-forbiddenkeeps its singleunexpected_submission_received@P:late), stable across repeatedretry-onceruns; the reference guard stays clean on the witnesses and controls; XM13a keeps its:latefindings and XM13b itsT_qoverrun.retry-delayedneedssch_netemand is exercised in CI.Not done
P2–P5, alias extensions, ACS integration and model runs.