Skip to content

fix(detection): shebang tokenisation, ambiguous triggers, trampolines, prefix anchors (#3116, #3118, #3133, #3134) - #3127

Merged
squid-protocol merged 3 commits into
mainfrom
fix/detection-3116
Sep 17, 2026
Merged

squid-protocol merged 3 commits into
mainfrom
fix/detection-3116

Conversation

@squid-protocol

@squid-protocol squid-protocol commented Sep 17, 2026 •

Copy link
Copy Markdown
Owner

Fixes #3116, closes #3118.

The bug

A shebang trigger was tested as a bare substring of the whole line (if trigger in first_line), so shell's sh fired on tclsh/wish/jimsh/swift-sh/racketsh and javascript's node fired on ts-node. The bogus shebang language then contradicted the file's own extension, and the Identity Conflict Trap forced the file to Tier 5 — Absolute Distrust: undeterminable, intensity 0.0, plus an Identity Masking anomaly flag. A customer with an ordinary Tcl script didn't see a classification error; they saw the scanner accusing their file of masquerading.

Measured on main before the fix:

file shebang before after
hello.tcl #!/usr/bin/tclsh Tier 5 undeterminable tcl Tier 0
gui.tcl #!/usr/bin/wish Tier 5 undeterminable tcl Tier 0
j.tcl #!/usr/bin/env jimsh Tier 5 undeterminable tcl Tier 0
s.swift #!/usr/bin/env swift-sh Tier 5 undeterminable swift Tier 0
r.scm #!/usr/bin/env racketsh Tier 5 undeterminable scheme Tier 0
t.ts #!/usr/bin/env ts-node Tier 5 undeterminable typescript Tier 0

#!/usr/bin/pwsh only ever passed by accident of registry order (powershell precedes shell), not by design.

The fix

Resolve the interpreter basename and match it as a token: handles the plain path form, env, env -S, and VAR=value prefixes. A trigger matches exactly, or with a pure version suffix — so python still names python3 and python3.12 while never naming micropython.

Two more bugs the #3118 invariant test then found

Writing "every trigger must resolve to the language that declares it" surfaced two further defects, both fixed here:

1. Three triggers are claimed by two languages each. deno and bun both run TypeScript and JavaScript; csi is both Chicken Scheme's interpreter and the C# script runner. Registry order picked a winner, so the other claimant's files contradicted their own extension and hit Tier 5 — the same bug via a second route. Measured on main: .ts + #!/usr/bin/env deno run and .scm + #!/usr/bin/csi -s both returned undeterminable with an Identity Masking flag, and deno+TypeScript is a mainstream combination.

Such triggers now yield no shebang verdict and the extension decides — correct for every claimant, and the same refuse-rather-than-guess posture Tier 4's margin requirement already takes. All four claimants (.ts/.js/.scm/.csx) now classify correctly.

2. tcl's path-shaped bin/expect trigger only matched under the old substring behaviour. Basename-normalising triggers at boot restores it, and upgrades it: #!/usr/bin/expect on a .tcl file now reaches Tier 0 consensus instead of extension-only Tier 2.

The trigger table is now built once in _calibrate_lookup_maps (normalised, longest-first, ambiguities dropped) rather than rebuilt per file.

Also: source_proof now names the mechanism that fired

_tier_2_fingerprint_check has always resolved shebangs and internal discriminators, but every caller labelled the result Shebang. So a .asm resolved by ACCTPGM CSECT reported Single Indicator (Shebang) with no #! anywhere in the file, and an identity conflict raised against a discriminator match reported a contradiction with a shebang that didn't exist. source_proof is a load-bearing claim in this engine — the mechanism now travels with the verdict (Shebang vs Internal Signature).

Tests

tests/core_engine/test_detection_resolution_order.py, 81 cases:

9,594 core_engine+extraction tests pass; the single failure is the pre-existing fidelity-table pin staleness. ruff/mypy audits baseline-clean.

🤖 Generated with Claude Code


Update: two more fixes, found by the #3117 harness

Once the harness (#3130) existed, it was run against this branch and surfaced four more defects. Two are fixed here; the third is disproven twice and documented; the fourth is left open.

#3133 — the portable-trampoline idiom

#!/bin/sh followed by exec tclsh "$0" ${1+"$@"} is the canonical Tcl portability trick, and the crucible's two cases are sqlite's own test-harness header verbatim. The shebang genuinely is sh, so it legitimately contradicts the .tcl extension — and the Identity Conflict Trap read that as masquerading and refused the file at Tier 5 with an Identity Masking flag. A customer would see the scanner call a stock sqlite script a disguised payload.

A generic shell shebang is the standard bootstrap vehicle; an exec <interpreter> in the opening lines is what the file actually runs as. The escape hatch is deliberately narrow: only an interpreter the extension's own language claims clears the conflict, and only when the conflicting shebang resolved to the shell family. A .tcl that re-execs python3 still trips the trap — pinned by a test.

Note this is not the #3116 bug and was not fixed by the original commits here: there the matcher was wrong, here it is right.

#3134 — a real extension outranks a filename prefix

BUILD.mk locked to python at Tier 1 via Prefix Anchor (BUILD) before its own .mk was consulted. Now an extension outranks a prefix, while an exact filename match (Makefile, Dockerfile) still wins outright — those have no extension to defer to.

Scoped so a template extension does not outrank a prefix either: .in names a template, not a language, so Makefile.pre.in (which is Makefile syntax) stays makefile rather than following .in to m4. The unscoped version flipped that file, and the crucible caught it before it shipped.

#3132 — not fixed, and two candidate fixes measured and rejected

Both plausible fixes make detection materially worse, and both are now recorded as comments at the relevant lines so nobody re-derives them:

candidate fix overall contested subset
baseline (this branch) 0.9974 0.9859
gravity abstains on same-extension collisions 0.9840 0.9034
exclude the contested extension from discriminator scoring 0.9850 0.9095

Both break 32 sqlite files to db2_sql to recover 7 MicroPython files. The root cause is sharper than the issue's original framing: python lists .py as its own discriminators entry, which is literally where the reported "72% Local Dominance" comes from (base 14 + 14×2 = 42 against embedded_python's 14 + 1×2 = 16) — and that same accidental self-reference is what currently holds .sql together. It is load-bearing by accident, and .sql needs a real content signal before it can be removed.

I would have shipped the first of those two without the harness. That is the clearest argument for #3130 I can offer.

Measured effect of what did land

metric before after
overall accuracy 0.9964 0.9974
auto subset 0.9984 0.9996
refusals 3 1
Tier 5 conflicts 3 1
misclassified files 11 8

The 8 remaining are 7 × #3132 and 1 × the sh/PowerShell polyglot (download.ps1, which has no exec line and so is genuinely a different shape from the trampoline).

Golden masters

Regenerated in both environments, drift fully explained: 462 diffs but only three identity-level changes — LICENSE.adoc's proof and tier (same language), and the excluded-artifacts queue shrinking 548 → 546 as the two trampoline files join the analysed population. tcl composition goes 29 → 31, and the remaining ~459 diffs are that group's aggregates and population-relative z-scores recomputing around two new members. Zero language flips.

87 resolution-order tests (up from 81); 9,600 core_engine+extraction pass.

squid-protocol and others added 2 commits September 17, 2026 05:40
…#3116)

shell's `sh` trigger fired on `tclsh`/`wish`/`jimsh`/`swift-sh`/`racketsh`
and javascript's `node` on `ts-node`, because a trigger was tested as a bare
substring of the whole shebang line. The bogus shebang language then
contradicted the file's own extension, so the Identity Conflict Trap forced
real Tcl/Swift/Scheme/TypeScript scripts to Tier 5 -- undeterminable,
intensity 0.0, plus an "Identity Masking" anomaly flag that reads as a
malware finding on an ordinary script.

Now resolves the interpreter basename (handling `env`, `env -S`, and
VAR=value forms) and matches a trigger exactly or with a pure version suffix,
so `python` still names python3/python3.12 while never naming micropython.
Triggers are tried longest-first, which also removes the registry-order
dependency for shebangs (#3118).

Verified: the six failing shebangs now resolve to their real language at
Tier 0; bash/sh -e/pwsh/node/perl/regina/python3/python3.12/env -S/micropython
controls unchanged. 9,513 core_engine+extraction tests pass (the one failure
is the pre-existing fidelity-table pin staleness).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ution order (#3116, #3118)

Follow-ups found by the #3118 invariant test, which asserts that every
shebang trigger resolves to the language declaring it:

1. Three triggers are claimed by TWO languages -- `deno` and `bun` (both run
   TypeScript and JavaScript) and `csi` (Chicken Scheme's interpreter AND the
   C# script runner). Registry order picked a winner, so the other claimant's
   files contradicted their own extension and hit Tier 5. Measured on main:
   `.ts` + '#!/usr/bin/env deno run' and `.scm` + '#!/usr/bin/csi -s' both
   returned undeterminable with an "Identity Masking" flag -- the #3116 bug
   via a second route, and deno+TypeScript is a mainstream combination. Such
   triggers now yield NO shebang verdict and the extension decides, which is
   correct for every claimant (the refuse-rather-than-guess posture Tier 4
   already takes). Both claimants of all three now classify correctly.

2. tcl's path-shaped `bin/expect` trigger only matched under the old
   substring behaviour; basename-normalising triggers at boot restores it and
   upgrades it to Tier 0 consensus.

The trigger table is now built once in _calibrate_lookup_maps (normalised,
longest-first, ambiguities dropped) instead of rebuilt per file.

Also: source_proof now names the mechanism that actually fired. This method
has always resolved shebangs AND internal discriminators, but every caller
labelled the result "Shebang", so a .asm resolved by 'ACCTPGM CSECT' reported
"Single Indicator (Shebang)" with no #! in the file, and an identity conflict
raised against a discriminator reported a shebang that did not exist.
source_proof is a load-bearing claim in this engine.

Adds tests/core_engine/test_detection_resolution_order.py (81 cases): the
claimant table per contested extension in registry order, the invariant that
every multi-claimant extension is a registered collision, per-claimant
behavioural resolution for 18 pairs, the shebang order-independence
invariant, the ambiguity pin, and the #3116 regression matrix.

9,594 core_engine+extraction tests pass (the one failure is the pre-existing
fidelity-table pin staleness). ruff/mypy audits baseline-clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🐦‍⬛ Muninn Security Scan

✅ No security issues found.

🐦‍⬛ Powered by Muninn · Skald Lab

…rate golden masters (#3116)

The crucible audit surfaced two things token matching exposed:

1. Nine files dropped from Tier 0 consensus to Tier 2 extension-only because
   their real interpreters were never in any shebang list -- `#!/bin/csh` and
   `#!/bin/tcsh`, `#!/usr/bin/pfsh` (Solaris profile shell), `#!/bin/Shell`
   (SerenityOS), and `#!/usr/local/bin/luatrace` (darwin-xnu's tracing
   scripts). All five resolved under the old behaviour only because `sh`/`lua`
   matched as a bare SUBSTRING of the line, so the same bug was causing false
   positives (tclsh -> shell) AND covering for these false negatives. Named
   explicitly now; all nine are back at Tier 0, intentionally.

2. Golden-master drift, both environments, fully explained: 243 diffs, ALL of
   them `Single Indicator (Shebang)` -> `Single Indicator (Internal
   Signature)` (239) or the same correction on the consensus path (4). Same
   language, same lock tier, on every one -- these are files resolved by their
   internal discriminator that the proof string used to attribute to a shebang
   they do not have. Zero language flips, zero tier changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@squid-protocol
squid-protocol merged commit d657d79 into main Sep 17, 2026
32 checks passed
@squid-protocol
squid-protocol deleted the fix/detection-3116 branch September 17, 2026 10:27
@squid-protocol squid-protocol changed the title fix(detection): match shebang interpreter as a token; ignore ambiguous triggers (#3116, #3118) fix(detection): shebang tokenisation, ambiguous triggers, trampolines, prefix anchors (#3116, #3118, #3133, #3134) Sep 17, 2026
squid-protocol added a commit that referenced this pull request Sep 17, 2026
…rank a prefix (#3133, #3134) (#3136)

* fix(detection): clear trampoline bootstraps; let a real extension outrank a prefix (#3133, #3134)

Re-landed on current main. These two fixes were written on the #3127 branch
but that PR merged as a SQUASH of only its first half, so they never reached
main -- `grep -c _trampoline_interpreter` on main returns 0. The original
branch is also based on the pre-#3128/#3130 main, so merging it now would
have DELETED the detection-accuracy harness and reverted the archetype parity
gate; this branch is cut fresh from main with only the two source files.

#3133 -- `#!/bin/sh` + `exec tclsh "$0" ${1+"$@"}` is the canonical Tcl
portability idiom (the crucible's two cases are sqlite's own test-harness
header verbatim). The shebang genuinely IS `sh`, so it legitimately
contradicts the `.tcl` extension, and the Identity Conflict Trap read that as
masquerading: Tier 5, `undeterminable`, plus an "Identity Masking" flag on a
stock sqlite script. A generic shell shebang is a bootstrap vehicle; the
`exec <interpreter>` in the opening lines is what the file runs as. Narrow by
construction: only an interpreter the EXTENSION's own language claims clears
the conflict, and only for a shell-family shebang, so a `.tcl` that re-execs
python3 still trips the trap.

#3134 -- `BUILD.mk` locked to python at Tier 1 via `Prefix Anchor (BUILD)`
before its own `.mk` was consulted. An extension now outranks a filename
PREFIX; an EXACT filename match (`Makefile`, `Dockerfile`) still wins
outright. Scoped so a TEMPLATE extension does not outrank a prefix either --
`.in` names a template, not a language, so `Makefile.pre.in` (which IS
Makefile syntax) stays makefile instead of following `.in` to m4. The
unscoped version flipped that file and the crucible caught it.

Measured on the #3117 harness, this base: overall 0.9964 -> 0.9974, auto
subset 0.9984 -> 0.9996, refusals 3 -> 1, Tier 5 conflicts 3 -> 1, errors
11 -> 8. Detection baseline regenerated so the gate holds the improvement
rather than the old looser number.

Golden masters regenerated in both environments from the CORRECT base.
87 resolution-order tests (up from 81). Full suite 10,048 pass; the 12
failures are pre-existing (11 numpy/xgboost-absent security_auditor, 1
fidelity pin). ruff/mypy baseline-clean.

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

* style: describe the trampoline shebang instead of quoting it (X-Ray)

The comment documenting #3133's idiom quoted a literal shell shebang, and a
'#!' + '/bin/' sequence anywhere in a file's first 8 KiB trips the X-Ray
binary-anomaly detector's embedded-execution-header check. Its exemption is
scoped to .sh/.bash/.zsh/.command extensions, so a .py file DOCUMENTING a
shebang is flagged. Described rather than quoted, with a note in place so a
future reader does not helpfully restore the literal and re-break CI.

X-Ray now reports 0 anomalies; 87 resolution-order tests still pass.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant