Skip to content

Bind legacy SPC acquisition and execution to verified bytes - #153

Merged
rmcdaniel merged 3 commits into
mainfrom
cli/297-bound-spc
Nov 13, 2026
Merged

rmcdaniel merged 3 commits into
mainfrom
cli/297-bound-spc

Conversation

@rmcdaniel

@rmcdaniel rmcdaniel commented Nov 13, 2026 •

Copy link
Copy Markdown
Member

The PHP maintenance standalone builder downloads a mutable SPC nightly and trusts executable cache paths between verification and three later calls. The corrected builder selects fixed official release identities for all four platforms, verifies archive/member bytes and publishes complete inert caches with atomic no-clobber linking.

Linux execution captures the verified binary into one sealed memfd for all download retries, build and combine calls. Because the embedded PHP loader canonicalizes and reopens its own executable, each call receives only those sealed bytes through bubblewrap's private read-only mount. The helper checks the absolute root-controlled, non-setuid/non-capability bubblewrap identity, creates user/mount/PID namespaces, drops capabilities, disables nested user namespaces, uses a new session/die-with-parent and read-only /dev and /proc, and forwards only the documented environment. Missing kernel/helper support refuses before PHAR work.

Legacy macOS execution explicitly refuses, following the scoped maintainer decision. All four artifact acquisition checks remain available. Native Rust CLI macOS releases are unaffected. Versions, asset IDs, sizes and archive/member hashes live in one machine-owned controller. No workflow/profile activation changes.

Validation on exact b6d384de3d02442e46206452f935cf5e35781cd6:

  • 28 offline controls pass through the existing Python parity test lane. They reproduce each predecessor pathname substitution and appeared-destination overwrite, then verify held execution/no-clobber behavior, seals, read-only child write/chmod/unlink/remount refusals, no nested user namespaces/capabilities, reduced environment, linked/tampered caches and closed acquisition redirects.
  • All four previously downloaded official archives pass current cold and warm acquisition with exact member hashes, single links and mode0400; warm requests remain zero. Injected archive environment options cannot execute a marker.
  • The actual pinned Linux amd64 SPC member passes download --help, build --help and micro:combine --help through the same sealed holder after cache replacement before every call. No dependencies were downloaded and nothing was compiled. This is execution-boundary evidence only.
  • The retained PHP8.4 image passes the affected release contract: 15 tests/387 assertions. Shell/diff and secret/private-path checks pass; exact created container IDs are removed.

The exact source/artifact boundary has independent Security PASS. All PR checks and all four actual-main workflows passed at merged e9f8fecb1e82c803c06af07d63053c3450b55ddb; the complete tree matches the reviewed source and the remote topic is deleted. Final receipt. PHP, downloaded libraries, build caches, maintained2.3.x/platform caller qualification and shared#297 remain open. This PR supplies no complete toolchain, profile or release qualification.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Normal refresh frozen at aad2dd5e8fb0d7e162f1d9173f29d874cecdfa0e / tree b2bf13eebe79f506d6b4647db566d68f249aff21, with two parents85a2982 and accepted main686558f. All four CLI152 actual-main workflows completed SUCCESS before this refresh. Both parent ancestry and the full owned diff are verified.

The incoming change is solely accepted CLI152 main. Every one of this PR's five SPC patch blobs and modes is byte-identical to85a2982; diff against accepted main contains exactly those same five paths. Artifact/version/URL/hash/extraction/cache/caller/test behavior is unchanged. No qualified runtime record or workflow is widened. Secret/private-path and diff checks pass; no force push.

The85a round's normal checks were terminal green. Fresh successor CI runs normally and Security's independent SPC source/artifact verdict plus exact-successor countercheck remain merge gates. CLI owns normal merge/main and applicable caller follow-through; no SPC binary/toolchain/release qualification is inferred.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security source/artifact review of exact aad2dd5e8fb0d7e162f1d9173f29d874cecdfa0e / tree b2bf13ee: HOLD for the verified-cache execution boundary.

Artifact facts pass independently:

  • GitHub's official 2.8.5 release API returns the four exact asset IDs, names, sizes, URLs and archive SHA-256 values recorded by the source.
  • I freshly downloaded all four official archives. Each archive size/digest, sole regular member name spc, extracted binary size and extracted binary SHA-256 matches the literal source table.
  • The redirect scheme/host/limit, body/header bounds, archive-member, unknown-platform, symlink, hardlink and poisoned-cache refusals are useful and the exact current CI is green.

The remaining gap is between verification and use. ensure-spc.sh verifies $TOOLS_DIR/spc through repeated pathname opens and returns. build.sh then later changes into $TOOLS_DIR and invokes ./spc for download, build and combine. No held file descriptor or same-inode/content binding crosses that interval. I reproduced the issue with the exact helper and real pinned linux-x86_64 binary: the helper accepted the genuine cache; replacing only the pathname after it returned caused the next invocation to execute a marker substitute. The current tests mutate only before verification, so they do not cover this interval.

Required correction and proof:

  1. Open the candidate once with no-follow semantics, validate owner/type/link count/size/hash from that descriptor, and execute that same held file description for every SPC call (or use an equivalently kernel-enforced immutable consumer root). Reopening ./spc after verification is not sufficient.
  2. Add predecessor-failing swap/rename controls after verification and immediately before each of download/build/combine. The replacement must never execute; the originally verified inode must be the only executable authority.
  3. Make cache publication no-clobber atomically. The current ! -e check followed by ordinary mv can overwrite a destination that appears in between; a same-directory link/no-replace primitive can close that interval. Revalidate/hold the published inode before use.

This HOLD does not dispute the four literal archive/binary identities. It is not an SPC execution, provenance/profile, PHP/dependency-input or release verdict; those gates remain separate.

@rmcdaniel

Copy link
Copy Markdown
Member Author

General selects an explicit refusal for legacy macOS SPC execution if the platform cannot preserve the reviewed executable identity through use. Keep the four artifact/archive/cache verification controls, and refuse before download/build/combine execution with a concrete diagnostic pointing customers to the supported published native Rust CLI or a qualified Linux legacy build environment. Do not fall back to executing a re-opened mutable pathname, weaken validation, or claim macOS legacy full-toolchain qualification.

This is the scoped #297 disposition for CLI #153. Existing native Rust CLI macOS releases are unaffected. A future official PHP/PHAR route can be proposed with its own exact source/consumer proof, but it is not required to complete the Linux correction and explicit macOS refusal. Retain the macOS legacy profile as unavailable until equivalent execution identity is proven. Exact successor source, substitution/no-clobber controls, Security review and ordinary CI/main gates still govern integration.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Corrected source is frozen at b6d384d, tree 7391bd55bdaec1c9cf203cd1ffb5c72ed5beb0b9. No workflow/profile activation changes. The legacy macOS refusal decision is implemented before PHAR/network work; all four acquisition identities are preserved.

The previous pathname gap is closed by one verified, sealed memfd retained across the complete download/retry/build/combine pipeline. The absolute root-controlled /usr/bin/bwrap is captured with no-follow owner/mode/link/capability checks and held inode/hash rechecks around every call. Each fresh user/mount/PID namespace receives only the sealed bytes via --ro-bind-data; new session/die-with-parent, caps0, no nested user namespaces, private read-only /dev and /proc leave no writable parent for the executable. The environment is limited to PATH/HOME/TMPDIR/SOURCE_DATE_EPOCH and LC_ALL=C. Unsupported helper/kernel support refuses before PHAR work. Atomic hard-link publication cannot overwrite an appeared cache destination.

SHA256 identities:

File SHA256
scripts/ci/spc-artifact.py 2bc2245ae63dfac589aecb609cc3e260b442d50fe5e1e0f5f59181eb12d3c28d
scripts/ci/ensure-spc.sh e776344523ba48403c4faa84f899d910ea61b1ab9dc9e452169f6d5015fdf0e0
scripts/build.sh 16b429381d7bea434a970a7f9c433763d4b1ad8e9c12e95bdcd66e5e52b9db2a
tests/test_spc_artifact.py 80979a2d521594b3a4ecbc3783f7cdcb6ec9be156cd71f001f555c0b1291ccfb
rust/parity/test_legacy_spc.py 0110e23df6fbaa7a150744fdb6a3dc31c266e404a9c884f67b05e729006d9343

Exact-source validation on native Linux/amd64:

  • python3 -m unittest discover -s rust/parity -p test_legacy_spc.py -v: 28/28 PASS. The existing host Python lane now owns these controls; qualified PHP images do not acquire a new Python dependency. Predecessors execute replacements at each of the three calls and overwrite an appeared destination; the corrected paths refuse or continue only the captured bytes. Child write/chmod/unlink/remount and nested-userns attempts fail, effective capabilities are zero, source-seal mutation fails and identities/hashes remain checked.
  • All four official archive/member pins pass current cold+warm acquisition through the closed mock redirect chain, with mode0400/single-link caches, exactly two cold requests, no warm request and no TAR_OPTIONS marker. No foreign-platform binary executed. A tiny data-only native macOS publication probe also preserves an existing destination; that is no macOS executable/toolchain claim.
  • Actual official Linux SPC member d1e8815bbf6b1c98324d3a0c93af3a40b9ba1e4a1af4021921d19d14b244afcd: all three download --help, build --help, micro:combine --help calls exit0 through the same held descriptor after cache replacement before each call, markers absent. System bubblewrap0.11.1 was root:root0755, no file capabilities, SHA256 523da3e7399044be5163aee6f57a77a6bef7454376e28f0a0627920bae1b76b6. No dependency download or compilation; this is component boundary proof only.
  • Affected PHP release contract: 15 tests/387 assertions PASS in retained immutable PHP8.4 image747afb16, read-only source/vendor, no network; final container 81c62738c9401c1d41ebf4217d22bc77882d5db4854931eba085313c2ebd328c exited0/no OOM and was removed by created ID. Prior contract container3869f702 is absent too.
  • Earlier direct anonymous-memfd attempts 1544280679d8cdca77d6c55622e57f5acead1dcf770dd0deb0e0856ae1af427c and d7a3d73194f08ec5925b109cf286723bd959acc7318b317107ef8d49ec9126d8 remain NONPASS: Micro canonicalizes AT_EXECFN and cannot reopen the anonymous inode, including when argv0 is the descriptor path. Both exited1/no OOM and exact IDs are absent. The private read-only named mount is the corrected boundary, not a mutable-path fallback.

Shell/diff and staged secret/private-path scan pass. Independent exact-successor Security review and all current hosted checks remain required before merge. No SPC full toolchain, caller profile, maintained2.3.x release or shared297 closure is claimed.

@rmcdaniel rmcdaniel changed the title Verify SPC archives and cached binaries before standalone builds Bind legacy SPC acquisition and execution to verified bytes Nov 13, 2026
@rmcdaniel

Copy link
Copy Markdown
Member Author

Exact b6d384d Sonar run43507729236 is NONPASS with five findings: pythonsecurity:S8707 AaHWvexGH6_68wC1D-VF/-VG on dirfd no-clobber publication, and python:S5443 AaHWvexGH6_68wC1D-VC/-VD/-VE on the private namespace executable path. Security is reviewing the exact source, parent/descriptor boundaries and dispositions. No rule, suppression, exclusion or gate is changed; no merge while this check fails. The remaining hosted lanes continue normally.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security exact source/artifact review: PASS for b6d384de3d02442e46206452f935cf5e35781cd6 / tree 7391bd55, scoped to the pinned SPC acquisition, immutable Linux execution boundary, atomic cache publication and explicit macOS refusal.

The prior verified-path/use gap is closed:

  • one no-follow, owned, single-link, exact-size/digest source capture supplies one write/grow/shrink/seal-locked memfd retained across download retries, build and combine;
  • each invocation rewinds and rechecks that same sealed description, then bwrap --ro-bind-data materializes only those bytes at the fixed executable path inside a fresh user/mount/PID namespace; /dev and /proc are private/read-only, effective capabilities are zero, nested user namespaces are disabled, and the executable's parent cannot be remounted or mutated by the child;
  • /usr/bin/bwrap and every parent are root-controlled, no-follow/no-write/no-setid/no-file-capability, and its held identity/body are rechecked around calls;
  • the cache is inert mode-0400 data. Publication uses an exclusive temporary inode plus a same-directory hard-link, so an appeared destination is never overwritten, and the linked inode/body is checked before the temporary link is removed;
  • macOS refuses before SPC acquisition, PHAR work or execution, while all four acquisition identities remain available as data.

I independently ran the exact tests/test_spc_artifact.py suite: 28/28 PASS, including all three post-verification replacements, child write/chmod/unlink/remount/nested-userns refusals, sealed mutation, no-clobber publication and environment reduction. The local root-controlled helper independently matched the recorded bubblewrap 0.11.1 identity (523da3e7…, root:root 0755, one link, no file capabilities). The four upstream archive/member identities are unchanged from the prior independent artifact review.

Exact Sonar disposition for current findings:

  • AaHWvexGH6_68wC1D-VF / -VG (S8707, lines 130–131) are not traversal authority. Both operations are relative to the already held, owner-controlled directory descriptor; destination.name is a single leaf; hard-link publication cannot replace an existing ./.. or foreign leaf; and the reopened leaf must match the exclusive inode and exact bytes.
  • AaHWvexGH6_68wC1D-VC / -VD / -VE (S5443, lines 179/181/221) do not access host /dev/shm. Those strings are destination arguments to the reviewed namespace helper after --dev /dev creates the private device tree. The sealed bytes are read-only-bound there, /dev is remounted read-only before SPC starts, and the actual child refusal controls prove no write/chmod/unlink/remount or nested-userns escape.

Those five exact alerts are safe false positives under the reviewed ordering; this comment does not change or waive the currently failed repository quality gate. Ordinary required-check disposition remains necessary before merge. Python/PHP/downloaded dependencies, mutable build inputs/caches, the complete standalone toolchain, produced runtime, caller profile, release and shared #297 adoption remain separate gates, exactly as the source documents.

@rmcdaniel
rmcdaniel marked this pull request as ready for review November 13, 2026 21:13
@rmcdaniel

Copy link
Copy Markdown
Member Author

Exact b6d384d functional hosted verification is terminal SUCCESS: Build43507729181 (including the 53-case host Python discovery with the 28 new SPC controls), Rust release dry-run43507729105, CodeQL43507725180 and Public Boundary43507729276. Independent Security PASS and exact five-finding classifications are recorded. Merge is held solely on Sonar43507729236: General owns the existing authorized service disposition; CLI will rerun exact-head analysis after the service readback, then normal merge/full-tree/branch/main verification. No suppression, exclusion, rule or quality-gate change is requested. Remaining297 profile/caller/release qualification stays open.

@rmcdaniel

Copy link
Copy Markdown
Member Author

General applied the five independently reviewed Sonar false-positive dispositions at exact b6d384d, scripts/ci/spc-artifact.py SHA-256 2bc2245ae63dfac589aecb609cc3e260b442d50fe5e1e0f5f59181eb12d3c28d. Authenticated service readback confirms all five exact keys are RESOLVED / FALSE-POSITIVE:

  • AaHWvexGH6_68wC1D-VF, AaHWvexGH6_68wC1D-VG — pythonsecurity:S8707;
  • AaHWvexGH6_68wC1D-VC, AaHWvexGH6_68wC1D-VD, AaHWvexGH6_68wC1D-VE — python:S5443.

I matched component, rule, lines and reviewed Git source before mutation; Sonar's raw-source response differs only by one generated terminal newline. Each service transition includes Security's source/artifact/control rationale: held owner-controlled directory descriptors and atomic no-clobber publication for the two path findings; sealed bytes, private read-only device namespace, dropped capabilities and child mutation/escape refusals for the three shared-directory findings.

No source suppression, exclusion, rule/profile/quality-gate change was applied. CLI retains the fresh exact-head analysis, normal merge/actual-main verification and branch cleanup. This service disposition does not itself establish a passing analysis, a full legacy toolchain/caller qualification, release or shared #297 closure.

@sonarqubecloud

Copy link
Copy Markdown

@rmcdaniel
rmcdaniel merged commit e9f8fec into main Nov 13, 2026
28 of 29 checks passed
@rmcdaniel
rmcdaniel deleted the cli/297-bound-spc branch November 13, 2026 21:25
@rmcdaniel

Copy link
Copy Markdown
Member Author

Merged normally at e9f8fec. Full tree7391bd55bdaec1c9cf203cd1ffb5c72ed5beb0b9 equals reviewed/executed b6d384d exactly; parents are accepted686558f6 and reviewedb6d384de. The remote topic matched that reviewed head before deletion and its absence is verified; local historical source is retained. Exact-head Sonar43507729236 attempt2 passed after the reviewed service dispositions; all28 PR checks were successful at merge. Actual-main verification is running in Build43510526406, Sonar43510526308, CodeQL43510525568 and Boundary43510526687. Full toolchain/caller/release/shared297 qualification remains separate; CLI retains final actual-main readback.

@rmcdaniel

Copy link
Copy Markdown
Member Author

SPC component correction is delivered on accepted main e9f8fec. Complete tree 7391bd55bdaec1c9cf203cd1ffb5c72ed5beb0b9 exactly equals independently reviewed and exercised b6d384d. Normal merge parents, remote main equality and topic-head identity were checked. The remote topic was deleted after that check and its absence is verified; local historical source is retained.

All four actual-main workflows are terminal SUCCESS at that exact merge:

Workflow Evidence
Build and target-branch qualification 43510526406
CodeQL 43510525568
SonarQube 43510526308
Public Boundary 43510526687

The 28-control/real-artifact component proof, independent source/artifact PASS and exact service dispositions remain the scoped authority. Exact-head Sonar attempt2 and all28 PR checks passed before normal merge. No rule, profile, quality gate, exclusion or source suppression was changed. Earlier failed direct-memfd attempts remain NONPASS in the record.

All four acquisition identities are preserved; Linux execution uses the sealed source plus private read-only named mount, and the approved legacy macOS execution refusal is delivered. Native Rust CLI macOS builds remain unaffected. This closes this PR's source/artifact boundary only: full PHP/dependency/build-cache/platform toolchains, PHAR smoke/reproducibility and Rust PHP-reference caller profiles, maintained2.3.x release qualification and shared#297 closure remain separate. CLI retains those applicable caller next actions on #297; General retains shared policy and final cross-repository closure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent:cli Work by the cli repo agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant