Execution Harness
Execution Harness
The structural test suite assembles payloads and inspects bytes. It never runs them. A payload can assemble perfectly, be null-free and look correct, and still fault on the first instruction, loop forever, or fail to decode itself.
The execution harness closes that gap: it maps shellcode RWX, forks, and reports the child's exit status. It is how padding is proven inert, how a decoder stub is proven to work, and how a code cave is proven to execute in the intended order.
Opt-in
Execution tests are skipped by default, so an ordinary test run never executes generated shellcode:
SHELLSNIP_RUN_EXECUTION=1 PYTHONPATH=$PWD poetry run pytest tests/e2e -v
Without the variable, those tests report as skipped. The structural suite (pytest tests/) is unaffected either way.
The loader
tests/harness/loader_linux.c builds to tests/harness/bin/loader_linux:
make -C tests/harness
tests/harness/bin/loader_linux <shellcode.bin> <timeout_seconds> \
[--cave N] [--fill HEX] [--breakpoint]
| Option | Effect |
|---|---|
--cave N | Map N bytes instead of len(shellcode) and place the payload so its last byte sits at the end, leaving N - len bytes to execute first. |
--fill HEX | Byte value pre-filled into that leading region (default 0x90). Requires --cave. |
--breakpoint | Emit INT3 immediately before the payload's first byte, so a debugger breaks at the payload boundary. |
--fill accepts decimal or 0x-prefixed hex.
Why the default fill is 0x90
Mapped pages are zero-filled, and 0x00 is not a nop — it is an illegal instruction in both 32- and 64-bit mode. A cave of zero bytes therefore faults with SIGILL before reaching the payload. The default is a 0x90 sled; --fill replaces it with whatever byte the real environment would contain.
This is exactly the reason the inert padding strategy exists: a filler that must avoid 0x90 cannot rely on the sled.
Exit status contract
| Status | Meaning |
|---|---|
0..255 | The child's own exit status — payload ran to completion |
124 | Timeout reached; child killed |
125 | Load error (mmap/read failure) |
128+N | Child terminated by signal N (e.g. 139 = SIGSEGV) |
133 | SIGTRAP at the payload boundary (128 + 5) — the breakpoint |
2 | Usage error (bad or missing argument) |
133 is the useful one for verifying layout: it proves the loader reached the payload boundary, independently of what the payload then does.
Without any of these options the loader behaves exactly as it did before they were added.
What the suite covers
tests/e2e/ runs the same payloads the recipes generate, through the loader:
- Payload execution — a payload that writes a file really writes it, and an
exit Npayload really exitsN. Asserting the side effect, not just that a process ran. - Encoder round-trips — an XOR- or chunk-encoded payload decodes itself and still performs its function, which is the only end-to-end proof that a decoder stub is correct.
- Code cave and prefill — with
--cave N, the leading region executes before the payload;--fill 0xCCon that region must produceSIGTRAP, which a silently ignored prefill could not. - INT3 —
--breakpointproduces133at the payload boundary. - Padding inertness — a payload runs identically behind a large inert filler, including behind an XOR-encoded payload, while a deliberately non-inert filler must change the result.
Requirements
The loader is Linux x86-64 and needs a C compiler (cc) plus make. It is the harness the Execution (POSIX x64) CI job builds and runs; that job is what makes these tests part of the merge gate rather than a local-only check.