docs(security): publish the AV false-positive evidence; harden arm64 W^X

SECURITY.md gains an "Antivirus False Positives" section placed directly after
the existing Verification block so it reuses those commands rather than
restating them. It states the detection plainly, shows the measured evidence
(the verdict inverts across architectures and link modes; identical macOS
segment structure splits clean/flagged; entropy is 3.5-4.2 bits/byte, nowhere
near packed), lists the cross-project precedent, and — deliberately — records
that removing the embedded scripts and externalizing the assets did NOT move the
detection. A negative result is still evidence, and publishing it is the point.

It also documents the release policy, invites independent audit through the
provenance/cosign/checksum path we already ship, and adds an `av-analysis` issue
label route for anyone who finds something real. Tone is evidence-first: no
vendor blame, and an explicit note that signing helps Windows but that no
AV-honoured signing scheme exists for Linux ELF, so it is not a full answer.

README carries a short pointer near Quick Start, where someone who just hit a
Defender warning will actually look.

Unrelated to AV, found while dissecting the artifacts: add -Wl,-z,separate-code.
GNU ld enables it by default on x86-64 but not on aarch64, so arm64 emitted ONE
R E PT_LOAD covering the whole image and mapped ~259 MB of tree-sitter parse
tables executable at runtime. Section flags said A, not AX -- but the kernel
applies segment permissions, so the section flags were never the control. The
amd64 build of the same source mapped the same data R only.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-08-09 16:17:54 +02:00
1 parent a4336dc40a
commit 93696aeb55
3 files changed
+132 -4

No files matched your search

+9 -1
View File
@@ -162,10 +162,18 @@ endif
# link), this flag fixes the OUTCOME, and the composition gate proves it on the
# shipped artifact. ELF-only: Apple's ld rejects -z noexecstack outright and it
# is meaningless for PE, so it is gated rather than made "common".
#
# -z separate-code additionally keeps read-only DATA out of the executable
# segment. GNU ld enables it by default on x86-64 but NOT on aarch64, so the
# arm64 binaries emitted ONE R E PT_LOAD spanning the whole image: .rodata is
# correctly marked A (not AX), but the kernel applies SEGMENT permissions, so
# ~259 MB of tree-sitter parse tables was mapped executable at runtime while the
# amd64 build of the same source mapped them R only. That is a large ROP gadget
# surface for data that is never executed, and it is invisible to section flags.
ELF_HARDENING_FLAGS :=
ifeq ($(IS_LINUX),yes)
ifneq ($(IS_MINGW),yes)
ELF_HARDENING_FLAGS := -Wl,-z,noexecstack
ELF_HARDENING_FLAGS := -Wl,-z,noexecstack -Wl,-z,separate-code
endif
endif
+8
View File
@@ -71,6 +71,14 @@ Unblock-File .\install.ps1
Options: `--skip-config` (binary only, no agent setup), `--dir=<path>` (custom location).
> **Antivirus note:** Microsoft Defender may flag a release binary as
> `Trojan:Script/Wacatac.B!ml`. This is a known false positive — typically 61 of
> ~62 engines return clean, and the same detection family hits `gh`, llama.cpp,
> Godot and Microsoft's own Go toolchain. See
> [Antivirus False Positives](SECURITY.md#antivirus-false-positives) for the
> evidence, how to verify the artifacts yourself, and how to report it if you
> think we are wrong.
Restart your coding agent. Say **"Index this project"** — done.
<details>
+115 -3
View File
@@ -148,9 +148,8 @@ is no exception path in this release gate.
- **CORS locked to localhost** — graph UI only accessible from localhost origins
- **Path containment** — `realpath()` check prevents reading files outside project root
- **Process-kill restriction** — only server-spawned PIDs can be terminated
- **Release-set verification** — installers verify downloaded archives, then the
candidate executable authenticates its adjacent runtime assets before
activation
- **Release-set verification** — installers verify the downloaded archive's exact
member set and SHA-256 before the candidate executable is activated
### Verification
@@ -172,6 +171,119 @@ sha256sum -c checksums.txt
# https://www.virustotal.com/
```
## Antivirus False Positives
Some release binaries are reported by **one** engine — Microsoft — as
`Trojan:Script/Wacatac.B!ml`. We believe this is a false positive, we do not hide
it, and this section exists so you can check that judgement yourself rather than
take our word for it.
### What the detection is
The `!ml` suffix marks a **machine-learning / heuristic** classification, not a
signature match. The `Script` token is a generic bucket in that naming scheme and
says nothing about script content — GitHub's own `gh` CLI and Anthropic's Claude
installer have both carried `Trojan:Script/Wacatac.H!ml` on native binaries.
Typically 61 of ~62 engines on VirusTotal return clean for the same file.
### What we measured
We dissected a full release matrix built from one commit. The verdicts split
across every axis at once:
| Binary | Link | Verdict |
|---|---|---|
| linux-amd64 | dynamic | flagged |
| linux-amd64-portable | static | clean |
| linux-arm64 | dynamic | clean |
| linux-arm64-portable | static | flagged |
| darwin-amd64 | — | flagged |
| darwin-arm64 | — | clean |
| windows amd64 / arm64 | — | clean |
The static/dynamic axis **inverts** between architectures, so no build or link
property explains it. The two macOS binaries have identical segment structure and
still split. Sibling artifacts from one build landed in *different* variant
buckets (`.B` vs `.C`).
We also tested and rejected the obvious structural hypothesis: entropy is low
everywhere (embedded vectors 4.17, parse tables 3.46 bits/byte, against 7.5–8.0
for genuinely packed payloads), so the binary does not resemble a packed dropper.
### What we changed, and what we reverted
We removed every embedded shell script from the binary and moved the UI bundle
and the agent integration templates out into separate verified files. **The
detection count did not drop** — it simply moved between artifacts. We reverted
both changes rather than keep permanent complexity that bought nothing. We are
documenting that here because a negative result is still evidence.
### This is endemic, not specific to us
The same `!ml` family repeatedly hits large, unsigned, native open-source
binaries:
- [llama.cpp #15874](https://github.com/ggml-org/llama.cpp/issues/15874),
[#24487](https://github.com/ggml-org/llama.cpp/issues/24487),
[#24558](https://github.com/ggml-org/llama.cpp/issues/24558) — including one
DLL of many in a single archive, with nothing found on reverse engineering
- [GitHub CLI #13306](https://github.com/cli/cli/issues/13306)
- [Microsoft's own Go toolchain #1255](https://github.com/microsoft/go/issues/1255)
- [Anthropic Claude Code #36796](https://github.com/anthropics/claude-code/issues/36796)
- [yt-dlp #7532](https://github.com/yt-dlp/yt-dlp/issues/7532),
[Godot #110612](https://github.com/godotengine/godot/issues/110612),
[PyInstaller #5854](https://github.com/pyinstaller/pyinstaller/issues/5854),
[Tauri #2486](https://github.com/tauri-apps/tauri/issues/2486),
[OpenAI Codex #2228](https://github.com/openai/codex/issues/2228),
[rust-lang/rust #88297](https://github.com/rust-lang/rust/issues/88297)
A Microsoft engineer on the Go team [put it plainly](https://github.com/microsoft/go/issues/1255):
*"we're aware of Windows Security/Defender issues with Go apps… we can't exactly
go fix something and solve all Go false positives."*
### Why we do not obfuscate around it
Deliberately reshaping a binary to avoid a classifier is what malware does, and
it measurably backfires — the same Microsoft engineer reports that obfuscation
*"increases scrutiny rather than avoiding it."* We would rather be scannable and
explain a false positive than be unreadable and score well.
### Verify it yourself
Every release ships the material needed to check our artifacts independently.
Use the commands in [Verification](#verification) above: SLSA Build Level 3
provenance ties the archive to the workflow run that produced it, Sigstore cosign
verifies the signature, and `checksums.txt` pins the bytes. The release notes
carry a durable per-object VirusTotal link for every scanned file, including any
tolerated detection — we publish those results whether or not they are clean.
You can also rebuild from source and compare: `scripts/build.sh --with-ui`
produces the shipped composition.
### Our release policy
A release may ship with **at most one** detection, and only when the engine is
Microsoft and the label ends in `!ml`. Two or more engines, any signature-based
label, any other vendor, or any "suspicious" verdict blocks the release. That
rule is enforced in `scripts/ci/check-virustotal.sh` and pinned by
`tests/test_vt_gate_policy_contract.sh`.
### If you find something real
We would genuinely rather be wrong in public than confidently wrong. If you find
anything that explains or contradicts the assessment above:
- Open an issue with the **`av-analysis`** label — include the artifact SHA-256,
the engine and label, and what you found.
- If it looks like an actual compromise rather than a classifier artifact, use
the private process in [Reporting a Vulnerability](#reporting-a-vulnerability)
instead.
A concrete finding changes our position. Signing is on the roadmap and will help
on Windows, but note that no code-signing scheme exists that AV engines honour
for Linux ELF binaries, so it is not a complete answer either.
## Supported Versions
| Version | Supported |