Skip to content

Make the llama wrapper quoting test pass on SIP-enabled Macs (wrapper unaffected) - #178

Closed
seathatflowsinourveins wants to merge 1 commit into
mainfrom
claude/relaxed-heisenberg-b90415
Closed

seathatflowsinourveins wants to merge 1 commit into
mainfrom
claude/relaxed-heisenberg-b90415

Conversation

@seathatflowsinourveins

Copy link
Copy Markdown
Owner

Summary

tests.test_adoption_bootstrap_macos.LlamaWrapperQuotingTests fails on a real Mac with System Integrity Protection (SIP) enabled and passes on GitHub's hosted macos-15 runner. The bug is in the test harness. The generated bin/llama-server wrapper works under SIP. This PR changes only the test and re-hashes it in manifests/evidence.json.

Root cause

  • The test's stand-in llama-server was a #!/bin/sh script that printed $0 and $DYLD_LIBRARY_PATH.
  • With SIP enabled, macOS purges every DYLD_* variable when it launches a SIP-protected binary. /bin/sh is one (ls -lO /bin/sh shows restricted).
  • The wrapper sets and exports DYLD_LIBRARY_PATH after its own /bin/sh has started, then execs its target:
    • With the real binary, the target is an unprotected Mach-O, so the value arrives.
    • With the stand-in, the target's image is /bin/sh again, so the value is purged and the stand-in printed an empty line.

Reproduced at the untouched head 4911baf (the report was against 8eacd8b). Path shortened:

AssertionError: Lists differ: ['<root>/tools/llama-cpp-b1/llama-server', ''] != ['<root>/tools/llama-cpp-b1/llama-server', '<root>/tools/llama-cpp-b1']

Each exec hop was probed on macOS 26.5.1 (25F80) arm64 with csrutil status reporting enabled. The native probe was compiled and ad-hoc signed; PLAIN_PROBE is a control variable:

Hop DYLD_LIBRARY_PATH PLAIN_PROBE
E1: value handed into /bin/sh purged kept
E2: set and exported inside /bin/sh, then exec a native binary (the wrapper's shape) kept kept
E3: set and exported inside /bin/sh, then exec a #!/bin/sh script (the old stand-in's shape) purged kept
E4: Python parent runs the #!/bin/sh script directly purged kept
E5: Python parent runs the native binary directly kept kept

Why CI passed: on main at 4911baf, the validate-macos job (job 107565829718, label macos-15) passed "Gate on the adoption test modules" with the old stand-in. So that runner did not purge DYLD_* for /bin/sh. This is consistent with GitHub running hosted macOS images with SIP disabled, which actions/runner-images#8162 documents for macos-13. csrutil status was not observed on the macos-15 runner itself.

The product wrapper works under SIP (unchanged)

I downloaded the pinned asset llama-b11057-bin-macos-arm64.tar.gz (11,178,107 bytes); its sha256 matched the pin. Its llama-server is a Mach-O arm64 binary, signed flags=0x20002(adhoc,linker-signed) with no hardened runtime and no entitlements. It loads @rpath/libllama*.dylib and @rpath/libggml*.dylib and has LC_RPATH @loader_path.

  • Install and run: fetch, find_one and install_llama_cpp were extracted verbatim from the script. The archive was pre-placed, so fetch's checksum short-circuit ran and curl never did. The install root contained $(touch PWNED), backticks, quotes and $HOME.
    • <root>/bin/llama-server --version printed version: 0.4.1-dev (build 11057, commit 59657a613) and exited 0.
    • sh <wrapper> --version gave the same result.
    • No PWNED or PWNED2 file was created.
  • DYLD_LIBRARY_PATH alone loads the dylibs: a copy of the binary with no dylibs beside it aborts with dyld: Library not loaded: @rpath/libllama-server-impl.dylib. The same copy runs when started by the wrapper's own three lines, evaluated by /bin/sh.
  • launchd is unaffected: com.native-stack.llama-embed runs ${ECO_ROOT}/bin/llama-server directly and sets no DYLD_* of its own.
  • Observations, not acted on:
    • For b11057, @loader_path resolves the dylibs even through a bare symlink from another directory. The wrapper's DYLD_LIBRARY_PATH is therefore a fallback for this build, and the script comment saying a symlink would fail does not hold for it.
    • Under SIP, a caller's own DYLD_LIBRARY_PATH never reaches the wrapper. The ${DYLD_LIBRARY_PATH:+:…} append only takes effect on hosts without SIP.

Test change

  • Stand-in: it is now a script whose shebang is a symlink to sys.executable, the test's own interpreter. Like the real binary, that interpreter is not SIP-protected.
  • Control run: the stand-in is first run directly with DYLD_LIBRARY_PATH=control and must print that value. If the interpreter drops it (a SIP-protected or hardened-runtime interpreter), the test skips only the DYLD_LIBRARY_PATH assertion, and only after the PWNED, exit-code and exec-target assertions have run.
  • Unset input: the wrapper now runs with DYLD_LIBRARY_PATH unset instead of empty. A POSIX shell keeps exporting a variable it inherited, even after reassignment. With "" inherited, a wrapper missing its export line still passed on Linux and on the SIP-disabled runners. With the variable unset, every host needs the export line. On this host, a plain variable showed the rule: inherited empty then reassigned, the child sees x; unset then assigned, the child sees nothing.
  • Unchanged checks: the same hostile install root, the same PWNED/PWNED2 checks, and the same exact exec target and DYLD_LIBRARY_PATH value.

Mutation checks on this SIP-enabled Mac, each run against a scratch copy of the script (none committed):

Script or interpreter change New test
unchanged passes (not skipped)
export DYLD_LIBRARY_PATH removed fails: the value is empty
llama_prefix="$prefix" (double quotes) fails: $(...) executed
unquoted exec target fails: exec of a truncated path
stand-in interpreter = SIP-protected /usr/bin/python3 the control run detects the drop; skipped with a reason after the other assertions

Limitation: I could not run the missing-export mutant against a shell without SIP. This host has no non-Apple shell, and a copied /bin/bash is killed at launch. This PR's Linux and macos-15 CI runs exercise the new test there.

Checks (this host: macOS 26.5.1 arm64, Python 3.14.7)

  • Target test: python3 -m unittest -v tests.test_adoption_bootstrap_macos.LlamaWrapperQuotingTests passes (not skipped). Before this change it failed.
  • Adoption test modules:
    • Ran test_adoption_bootstrap, test_adoption_bootstrap_macos, test_adoption_contract, test_adoption_docs_consistency, test_adoption_guarded_runners, test_adoption_launchd and test_adoption_status.
    • Also ran the CI gate's test_render_config, test_workflow_hardening and test_workflow_security_coverage, plus test_evidence_manifest.
    • Result: 379 tests with skipped=41, plus 6 errors. All 6 were EmbedAcceptanceScriptTests hitting a PermissionError when binding 127.0.0.1 inside the command sandbox. That class re-run outside the sandbox: 8 tests OK.
  • Validation: python3 scripts/validate.py passed (5228 hashed files, 143 receipts).
  • Shellcheck 0.11.0 (-S style):
    • adoption/bootstrap-macos.sh and adoption/bootstrap-linux.sh are clean.
    • A wrapper generated for an ordinary install path is clean.
    • For the hostile test path, shellcheck reports SC2089, SC2090 and SC2016 on the single-quoted literal. That is the intended literal-data behavior.
  • Evidence: manifests/evidence.json was re-hashed with host_receipts.register_file, for tests/test_adoption_bootstrap_macos.py only.

Evidence class: local integration on one SIP-enabled Mac, plus CI job metadata. No launchd service, embedding acceptance or model run, and no receipt added.

🤖 Generated with Claude Code

LlamaWrapperQuotingTests failed on a SIP-enabled Mac (macOS 26.5.1) and
passed on GitHub's hosted macos-15 runner. Its stand-in llama-server was a
#!/bin/sh script: with System Integrity Protection enabled, macOS purges
DYLD_* variables when it launches a SIP-protected binary such as /bin/sh,
so the stand-in printed an empty DYLD_LIBRARY_PATH even though the wrapper
had exported it.

The product wrapper is not affected. It sets DYLD_LIBRARY_PATH after its
own /bin/sh has started and execs the real llama-server, an unprotected,
ad-hoc signed Mach-O, so the value arrives. On the same SIP-enabled Mac the
pinned b11057 asset installed through install_llama_cpp into a
shell-syntax path runs through the generated wrapper, and with its dylibs
moved away the wrapper's DYLD_LIBRARY_PATH alone still loads them.

The stand-in is now a script for the test's own Python, unprotected like
the real binary. A control run first checks that this interpreter sees a
DYLD_* value handed to it directly; if not, the test skips only the
DYLD_LIBRARY_PATH assertion, after the data-safety assertions have run.
The wrapper now receives DYLD_LIBRARY_PATH unset rather than empty: an
inherited variable stays exported, so a missing export line had gone
unnoticed on every non-SIP host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

seathatflowsinourveins added a commit that referenced this pull request Sep 24, 2026
…nd the macOS pnpm 12.6.0 re-qualification (#201)

* Record macos-m5pro-20260924 application and data use receipts

Seven use-stage native_proven receipts were recorded on the Mac from a
detached checkout of main 3699012, so catalog_revision stays reachable
after a squash merge. All are pin-bound, all commands exit 0, and each
carries --second-physical-machine and --hardware-profile-ref
macos-m5pro-20260924:

- postgresql 18.6: SHA-256-pinned source build, server running, Alembic
  0001, a temp-table transaction summing to 6, then rolled back;
- fastapi 0.141.1: 12 API tests against ledger_test and schema/type
  agreement;
- nextjs 16.3.5: typegen, TypeScript check and production build;
- react 19.3.0: production build and the Chrome create/update workflow;
- playwright-test 1.63.0: 1 passed, 0 flaky;
- playwright-cli 0.1.21: an isolated Chrome session created a run
  (revision 1), 0 console messages, servers stopped;
- duckdb 1.5.5: mise-pinned; a 1,000-row Parquet round trip returned
  sum 332833500.

The receipts ran through the committed blueprint (pnpm@12.4.2 self-pin),
outside the Claude Code sandbox, which blocks PostgreSQL shared memory,
loopback database connections and pnpm 12 network/store operations
here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Record the macOS application re-qualification at the 2026-09-24 pins

The 2026-09-24 catalog pin bumps (uv 0.12.18, pnpm 12.6.0) left the Mac
application-slice gate unverified. The typed run-ledger blueprint was
re-run on host macos-m5pro-20260924 and recorded as a platform-scoped
convergence record, experiment-macos-20260924.json (observed,
adopt_within_scope, native_cli_execution, zero model calls).

pnpm 12 stores its own version in pnpm-lock.yaml as well as in
packageManager, so the committed blueprint keeps running pnpm 12.4.2
whatever the global pin is. The qualifying run used a scratch copy with
only that self-pin raised to 12.6.0. Both changed files are retained
under evidence/artifacts/macos-application-20260924/variant/, and the
lockfile's project dependency section is byte-identical. The blueprint
itself is unchanged, so experiment.json's frozen inputs stay valid; the
README says how to move the recipe with a new frozen record.

All 15 attempts are retained in order. Six failed:
- five inside the Claude Code Bash sandbox: pnpm self-pin/lockfile
  mismatch, pnpm registry TLS, initdb shared memory, loopback database
  connect, pnpm store lock;
- one native attempt, a post-SIGTERM bind probe that saw a TIME_WAIT
  socket.
The qualification is the last native attempt: setup and repeat, native
PostgreSQL 18.6, Alembic up/repeat and down/base/re-upgrade, 12 API
tests, drift/type/build checks, the Chrome Playwright Test, an isolated
playwright-cli create/update, the same record after database and server
restart, and release of the owned ports.

Checks: validate_convergence.py on the record and --all-recorded (19
valid). Tampered copies (failed run as the qualification, altered
variant hash, dropped failure entry) are each rejected. validate.py,
validate_catalogs.py, build_ecosystem.py --check and evidence_manifest.py
--check pass. unittest ran 3939 tests with 1 failure, the pre-existing
SIP-related llama wrapper quoting test that #178 addresses; the 34
sandbox-only PermissionError cases pass outside the sandbox.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Regenerate the component evidence matrix for the macos-m5pro-20260924 application receipts

component_matrix.py --write and evidence_manifest.py --write after the
seven new receipts and the macOS convergence record. host_receipts
validate (16), validate.py, component_matrix --check,
new_host_grand_list --check, evidence_manifest --check,
validate_convergence --all-recorded (19), validate_catalogs.py and
build_ecosystem.py --check pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* List the retained macOS pnpm variant in the OSV lockfile inventory

tests/test_osv_lockfile_coverage.py requires every tracked lockfile and
manifest in .github/osv-scanner-lockfiles.json. The retained variant
pnpm-lock.yaml joins lockfiles, and its package.json joins
covered_by_lockfile against that lockfile, matching how the
blueprint's own pair is listed. Its project dependency section is
byte-identical to the blueprint lockfile already scanned. The only
additions are pnpm's own 12.6.0 entries, for which the OSV API reports
no known vulnerabilities (pnpm and @pnpm/exe, queried 2026-09-24).
The inventory's hash is re-registered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Record independent reviews of the macos-m5pro-20260924 application and data receipts

The foundation-lane-setup session reviewed the seven use receipts from #201
(postgresql, fastapi, nextjs, react, playwright-test, playwright-cli, duckdb)
and appended an independent_session agree review to each.

It re-ran every recorded command from a clean detached checkout of the
recorded catalog_revision 3699012. Every command exited 0. Deterministic
outputs match the recorded output_sha256 byte for byte. The rest match once
the PID, checkout path, temp-dir name, timings or snapshot ref id are
substituted.

The review re-registers the receipt hashes and regenerates the component
evidence matrix and the new-host grand list.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: seathatflowsinourveins <234074349+seathatflowsinourveins@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@seathatflowsinourveins

Copy link
Copy Markdown
Owner Author

Closing as superseded by #228, which carries this PR's commit 3fd39cd unchanged (cherry-picked with -x) and adds the missing npm skip for the symlinked-prefix test. Landing #228 instead. (native-agent-stack-9a, foundation PR queue)

seathatflowsinourveins added a commit that referenced this pull request Sep 25, 2026
…(tests only; supersedes #178) (#228)

* Make the llama wrapper quoting test observe DYLD_LIBRARY_PATH under SIP

LlamaWrapperQuotingTests failed on a SIP-enabled Mac (macOS 26.5.1) and
passed on GitHub's hosted macos-15 runner. Its stand-in llama-server was a
#!/bin/sh script: with System Integrity Protection enabled, macOS purges
DYLD_* variables when it launches a SIP-protected binary such as /bin/sh,
so the stand-in printed an empty DYLD_LIBRARY_PATH even though the wrapper
had exported it.

The product wrapper is not affected. It sets DYLD_LIBRARY_PATH after its
own /bin/sh has started and execs the real llama-server, an unprotected,
ad-hoc signed Mach-O, so the value arrives. On the same SIP-enabled Mac the
pinned b11057 asset installed through install_llama_cpp into a
shell-syntax path runs through the generated wrapper, and with its dylibs
moved away the wrapper's DYLD_LIBRARY_PATH alone still loads them.

The stand-in is now a script for the test's own Python, unprotected like
the real binary. A control run first checks that this interpreter sees a
DYLD_* value handed to it directly; if not, the test skips only the
DYLD_LIBRARY_PATH assertion, after the data-safety assertions have run.
The wrapper now receives DYLD_LIBRARY_PATH unset rather than empty: an
inherited variable stays exported, so a missing export line had gone
unnoticed on every non-SIP host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 3fd39cd)

* Skip the symlinked-prefix platform-dependency test when npm is absent

test_places_the_verified_tarball_correctly_when_the_prefix_is_reached_through_a_symlink
uses the npm-packed fixtures that PlatformDependencyInstallTests.setUpClass
builds only when npm is on PATH: self.verified_tarball directly and
self.verified_sha256 through _platform_dependency_pin. Unlike its
npm-dependent siblings it had no @unittest.skipUnless(NPM, ...) since it
was added in #94, so on a host without npm it errored with AttributeError
instead of skipping. Hosted runners have npm, so CI never hit it.

The class's other three undecorated tests use no npm fixtures and still
run without npm.

Re-hash tests/test_adoption_bootstrap_macos.py in manifests/evidence.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: seathatflowsinourveins <234074349+seathatflowsinourveins@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Scout <scout@local>
@seathatflowsinourveins
seathatflowsinourveins deleted the claude/relaxed-heisenberg-b90415 branch September 25, 2026 18:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant