Guides

Execution Harness

Running generated shellcode for real — the opt-in execution suite and the Linux loader.

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]
OptionEffect
--cave NMap 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 HEXByte value pre-filled into that leading region (default 0x90). Requires --cave.
--breakpointEmit 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

StatusMeaning
0..255The child's own exit status — payload ran to completion
124Timeout reached; child killed
125Load error (mmap/read failure)
128+NChild terminated by signal N (e.g. 139 = SIGSEGV)
133SIGTRAP at the payload boundary (128 + 5) — the breakpoint
2Usage 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 N payload really exits N. 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 0xCC on that region must produce SIGTRAP, which a silently ignored prefill could not.
  • INT3 — --breakpoint produces 133 at 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.

Copyright © 2026