Commit d30c75d
fix(S2): physical confinement + the frozen envelope, inside the executable
The two unclosed fragments of the first blocker. Both are the same mistake in different
clothes: trusting a proof that does not prove what it was being asked to prove.
1. CONFINEMENT WAS LEXICAL AT INTERMEDIATE SYMLINKS. ResolveLinkTarget(path, true)
returns null when `path` itself is not a link — it is not os.path.realpath. So with
root/linked -> /outside
the final `root/linked/source.cs` is not a link, the old resolve returned the lexical
path, and the escape was called confined. `--out` was worse: PrepareOutDir used
Path.GetFullPath + a string IsInside only, so a parent linking INTO the source root
could have put the staging dir and the published bundle physically in the tree while
the string still looked external.
RealPath now resolves EVERY existing component: on each link it restarts from the
target's root with the target's segments ahead of the remaining ones (so a link whose
own target sits behind further links still resolves), and `..` is applied AFTER the
prefix resolves — which is what makes it physical rather than textual. The source must
physically resolve inside the physical root. The out-dir's parent is resolved first,
proven off the tree, and the staging dir + out-dir are then built under that VERIFIED
physical parent — and the parent is re-proven immediately before the publishing rename.
2. THE FROZEN ENVELOPE WAS NOT RESTATED. ValidateBundleShape checked some strings and a
non-empty allowed_actions, and an action was "permitted" merely by appearing in the
candidate's own list. But the hash binding proves the plan and the candidates agree
with EACH OTHER; it says nothing about whether the candidates obey the permission
policy. A self-consistent forgery could carry `event_contract: name_only` with
`allowed_actions: ["convert_acquire"]`, re-hash, and be honoured. Cryptography is not
a substitute for meaning. The bundle is now checked against the frozen contract:
version/operation, exactly one allowed type and one source file, type.file ==
source.path (each was previously compared only against its own copy, so type -> A.cs /
source -> B.cs was possible), selected_findings null-or-exactly-these-ids, known
event_contract, allowed_actions subset of the S1 enum with no duplicates, and
convert_acquire ONLY for a proven inotify_property_changed contract — whatever the
bundle claims to allow. Every candidate must belong to the one selected (type, file).
The controlled-refusal contract is closed too: Path.GetFullPath / LinkTarget failures
(a NUL in a path) are normalized to a refusal, span bounds are checked as
`start >= 0 && length >= 0 && start <= len && length <= len - start` BEFORE
`new TextSpan` can throw on the overflow, and a final catch-all turns any
unanticipated exception into exit 2 rather than a traceback. Fail closed.
Regressions: two intermediate-symlink cases (source reached through `root/linked ->
outside` with the bundle re-hashed; an `--out` whose parent links into the repo — refused,
with the tree untouched), five frozen-envelope forgeries (forged name_only tiering,
type/source file drift, malformed selected_findings, unknown contract, action outside the
enum), plus the NUL path and the overflowing span. Each forgery is fully self-consistent
and freshly hash-bound, and its plan is hand-built as the canonical projection — because
validate_plan, correctly, will not produce one from a forged bundle. So only the
rewriter's own restatement of the contract can refuse them, which is exactly the property
under test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JxKjqdGEFzq4UzZupw379G1 parent a44784d commit d30c75d
2 files changed
Lines changed: 399 additions & 47 deletions
0 commit comments