Sunday, October 18, 2026

Inside semantic_firewall: how sfw fingerprints Go functions to catch supply-chain backdoors

Inside semantic_firewall: how sfw fingerprints Go functions to catch supply-chain backdoors

semantic_firewall computes deterministic semantic fingerprints of Go functions from SSA to detect xz-style backdoors hidden in refactors, for defenders and authorized code review.

ToolBlackVectorOps/semantic_firewall — SSA-based semantic fingerprinting and diffing CLI (sfw) for Go source, aimed at detecting backdoors smuggled in as refactors
CategoryStatic analysis / supply-chain integrity tooling for Go
Primary UseDiffing Go code across versions with sfw diff and sfw deps diff, and matching function topologies against a signature store with sfw scan, to surface capability-adding changes a commit message hides
Safe UseDefensive security research and authorized code review, CI pipelines and internal supply-chain auditing; the README explicitly restricts the tool to defensive and authorized testing
Telemetry NotePurely local static analysis — the code under test is never executed; sfw audit optionally sends diff hunks and commit messages to an external LLM API via --api-key/--api-base, which defenders should treat as the only data egress point

semantic_firewall is a statically-typed answer to a problem the xz backdoor made famous: malicious changes that survive review because they look like ordinary refactoring. The CLI, sfw, never executes the code under test — instead it builds Static Single Assignment form with golang.org/x/tools/go/ssa, rewrites each function into a canonical intermediate representation, and fingerprints that function as the SHA-256 of the canonicalized IR. Because the hash is computed over semantics rather than raw text, a rename, reformat, or reshuffled branch structure that leaves behavior intact produces the same fingerprint, while a silent new net.Dial in a Handle function changes it decisively. That determinism is the core pitch, and it is what makes the tool usable as a regression gate rather than a linter.

The interesting engineering choice is the canonicalization pipeline itself. Raw go/ssa output is sensitive to instruction ordering and temporary naming, so the project normalizes each function into a comparable form before hashing — meaning two independently compiled views of equivalent logic converge on one digest. The README is refreshingly honest about the edges of this: an unhandled SSA instruction emits an UnhandledInstr<T> marker unless --strict is set, in which case the canonicalizer fails loudly. For an integrity tool, that trade-off matters, and operators running this in CI should almost certainly default to --strict so a parser gap cannot silently downgrade a real change into an unreadable but ignored one.

The workhorse command is sfw diff, which compares two Go files function by function. Matching is two-layered: functions are first paired by name, and anything unmatched is then paired by topology — a compact structural signature rendered as L<loops>B<branches>I<instructions>[<call signatures>]. This is how the tool catches the oldest trick in the backdoor book: rename Add to Sum, keep the body identical, and the fingerprints match even though the names do not. The diff output tags such cases renamed rather than modified, which is exactly the signal a reviewer wants when auditing a dependency bump that claims to be a harmless API cleanup.

Risk scoring is where the tool shows its defensive bias. Each changed function is scored by what it newly reaches and what protective structure it loses — in the README's worked example, a function that gains net.Dial picks up +20 for the network capability and +10 for an added goroutine, landing at a risk_score of 30. Conversely, removing a function that contained a verification call scores a RemovedCheck(...) at 10, directly modeling the pattern where a backdoor is enabled not by adding code but by deleting a guard. The scoring is coarse and readable by design; it is a triage ranking for humans, not a verdict.

A subtle but well-considered detail is the dual literal policy used to decide preserved status. The default fingerprint masks strings and integers outside [-16, 16], so routine literal churn does not flag a function; a parallel keep-all-literals fingerprint is also computed, and both must be equal for a function to count as untouched. The practical consequence is spelled out in the README: changing a URL, a command string, a limit, or a file mode flips the function to modified and surfaces the literal in added_ops/removed_ops. That is precisely the class of small-diff, high-impact edits — an endpoint swap, a permission-mode change — that manual review glides over.

The component the README calls the Semantic Zipper is the alignment engine that makes the diffs meaningful. Instead of naively comparing instruction sequences, it pairs instructions by data flow, which means it correctly handles a swapped if/else, a negated condition, or a call hoisted out of a guard — changes that are semantically significant but textually confusing. Each such transformation is still reported as modified, with the moved instructions appearing in both the added and removed operation lists so a reviewer can see the relocation explicitly. This is the kind of compiler-theory plumbing that separates a serious differ from a fancy diff -u.

Where sfw diff covers first-party code, sfw deps diff extends the same machinery to the third-party code two trees of a module actually compile in — the real attack surface in the xz scenario. It reports changed dependency functions, exported functions that newly reach a capability, and, importantly, changed files the analysis cannot see, so opaque or vendored changes are at least flagged as unknown rather than dropped. A --deps flag on sfw scan pushes this into topology matching against a signature store, with --deps-depth choosing between direct and transitive dependency traversal.

That signature infrastructure — sfw index and sfw scan — turns the fingerprints into a detection system. index ingests functions into a store backed by PebbleDB or JSON, tagged with --name, --severity (default HIGH, stored unvalidated) and --category (default malware); scan then matches function topologies against the store with a similarity threshold, defaulting to 0.75 and overridable with --exact. The threshold approach acknowledges that copied malware gets lightly edited in transit; a topological near-match at 0.75 is a lead worth human eyes, not a proof. A migrate command moves stores between backends and stats summarizes one.

The most opinionated feature is sfw audit, which sends a diff's high-risk changes, their source, and the commit message to an LLM — a Claude model, default claude-opus-5-5, invoked through a tools-based interface — and returns a verdict: MATCH, LIE, SUSPICIOUS, or ERROR, with exit code 1 on anything but MATCH. This is essentially automated commit-message-integrity checking: did the human-readable description actually describe what the code did? It is a clever application of LLM code reading, but it is also the only place the pipeline sends data off-host, via --api-key and --api-base; anyone running this on proprietary or sensitive code should point it at an internally controlled endpoint.

Operational ergonomics are documented with unusual candor. Flags must precede positional arguments because the CLI uses Go's flag package, which stops parsing at the first non-flag token — flags after a file name are silently ignored, a genuine footgun the README calls out rather than hides. Exit codes are similarly explicit: check --strict with any file error exits 1, scan exits 0 even with alerts present (so CI gating must parse the JSON, not the exit code), and audit exits 1 on a LIE verdict. Directory walks skip vendor/, hidden directories, and _test.go files, and build-tag-excluded files are deliberately loaded under a permissive configuration so they cannot be used to hide code from analysis — a small detail that tells you the author has thought about evasion.

Installation is a single go install github.com/BlackVectorOps/semantic_firewall/v7/cmd/sfw@v7.1.1, with go.mod requiring go 1.27.2; sfw version prints the build string. There is also a GitHub Marketplace action (semantic-firewall) and a public workflow badge (semantic_analysis.yml), so wiring the analyzer into pull-request gates is clearly an intended deployment mode, and the repo's topic list — supply-chain-security, devsecops, ssa, compiler-theory — matches that positioning. At 36 stars the project is early, MIT-licensed, and the README carries an explicit caution restricting use to defensive research and authorized testing.

For teams running Go in production, the realistic adoption path is narrow and valuable: gate dependency bumps and internal refactors through sfw diff and sfw deps diff in CI, seed the signature store with families of functions worth alerting on, and reserve sfw audit for high-trust-cost commits where the commit message itself is the artifact under suspicion. The tool will not replace human review, and its scores are heuristics — but it converts the xz-class question "did anything in this update quietly gain a capability or lose a check?" from a matter of hope into a queryable, hash-backed property of the code.

Official project repository for BlackVectorOps/semantic_firewall.
Download Tool

Educational analysis for authorized security professionals. Use only in controlled, authorized environments.

Share articleFacebookXLinkedIn

Continue exploring

Browse all articles →

0 comentários:

Post a Comment

Note: Only a member of this blog may post a comment.