abcpwn parses attacker-controlled binaries. A bug in the parser is a
real vulnerability. This document defines the threat model, the
guarantees the project provides, and the rules every contribution
must satisfy.
The adversary provides a malicious binary or input crafted to exploit
abcpwn itself. The adversary cannot:
- Modify
abcpwn's installation, configuration, or environment. - Inject into
abcpwn's process beyond what the parsed input allows.
- Remote code execution in the
abcpwnprocess. - Denial of service (out-of-memory, infinite loop, crash).
- Memory corruption (read or write out of bounds).
- Information disclosure (read sensitive files, leak heap contents).
- Side-channel attacks on the tool itself (timing, power).
- Attacks via compromised dependencies (mitigated by SBOM and pinned versions).
- Physical access attacks.
- No execution of analyzed binaries.
abcpwnonly parses; it never runs, loads, or interprets the target binary's code. - No
system(),popen(), orexec*()anywhere insrc/. Enforced byscripts/check-no-system.sh. - No network by default. Only
libc downloadandpwninitmake HTTPS requests, both gated by the global--allow-networkflag. - No telemetry. No metrics, no crash reports, no analytics, no
phone-home. Enforced by
scripts/check-no-telemetry.sh. - No auto-update.
abcpwnnever checks for updates or downloads itself. - Bounded resources. Every input is size-capped
(
ABCPWN_MAX_FILE_SIZE, default 2 GB) and parse depth-limited (default 64). - Memory-safe by construction. No raw
new/delete, no raw C strings at API boundaries, RAII throughout. - Fuzzed parsers. Every binary-parsing path has a libFuzzer harness; the nightly fuzz workflow runs one hour per harness.
- Sanitizer-clean. Debug builds run with AddressSanitizer and UndefinedBehaviorSanitizer; CI fails on any sanitizer error.
- Reproducible builds. With
-ffile-prefix-mapand a fixedSOURCE_DATE_EPOCH, the released binary is intended to be byte-identical when rebuilt on the same OS, compiler, and arch.
All file reads go through core/safe_io.hpp::open() which:
- Canonicalizes the path via
std::filesystem::weakly_canonical(). - Confirms the file is a regular file (not a FIFO, device, or symlink to a device).
- Enforces the size cap before reading.
- Returns a
Result<FileHandle, FileError>rather than throwing.
Direct std::ifstream use in source code is rejected at review.
Use std::unique_ptr, std::shared_ptr, RAII helpers. Enforced by
clang-tidy cppcoreguidelines-owning-memory,
modernize-make-unique, and modernize-make-shared.
Use std::string_view and std::span<const std::byte>. Raw char*
appears only inside thin wrappers around LIEF, Capstone, and
Keystone.
LIEF parses ELF, PE, and Mach-O. The project does not write its own parsers. If LIEF lacks a feature, document the gap and defer.
The scripts/check-no-system.sh orchestrator entry greps the source
tree and fails the build on any match.
Network code lives in:
src/commands/libc.cpp(thelibc downloadaction).src/commands/pwninit.cpp(interpreter download).
Both inspect the context's allow_network flag and exit with
NetworkDisabled (exit code 12) when it is false.
There is no opt-in or opt-out for telemetry; there is no telemetry.
scripts/check-no-telemetry.sh greps for known SDK function names
(Sentry, Rollbar, Segment) and fails on any match. The URL allowlist
in scripts/check-urls.sh covers every host the source references
in tracked content.
Every arithmetic operation on a value derived from input uses
saturating arithmetic (std::add_sat, std::sub_sat, std::mul_sat)
or an explicit overflow check via __builtin_add_overflow and
friends. Silent integer overflow on parsed sizes is a bug.
Every loop that iterates over data derived from input must have an upper bound derived from the file size cap, NOT from a count field in the input:
const auto safe_count = std::min(untrusted_count, MAX_ENTRIES);
for (std::size_t i = 0; i < safe_count; ++i) { ... }Variables derived from parsed input are prefixed untrusted_ until
validated. Code review enforces this; clang-tidy cannot.
Recursive parse paths carry an explicit depth parameter and bail when the configured maximum (default 64) is reached. Stack overflow is not the depth check.
cmake/Hardening.cmake applies:
- Common:
-fstack-protector-strong,-fPIE,-pie. - Release:
_FORTIFY_SOURCE=3(or=2on older glibc). - Linux:
-fstack-clash-protection,-Wl,-z,relro,-Wl,-z,now,-Wl,-z,noexecstack. - Linux x86_64:
-fcf-protection=full(Intel CET). - aarch64 (Linux and macOS):
-mbranch-protection=standard(ARM BTI plus PAC).
Debug builds run with AddressSanitizer and UndefinedBehaviorSanitizer. ThreadSanitizer runs in its own preset on PR. CI fails on any sanitizer error.
tests/fuzz/ contains four harnesses at the v0.1 baseline:
fuzz_binary_loader- LIEF wrapper.fuzz_seccomp_decoder- cBPF decoder.fuzz_fmt_parser- format-string offset finder.fuzz_gadget_filter- ROP gadget filter.
Each runs one hour per night in fuzz.yml. Any crash, leak, OOM, or
timeout opens a GitHub issue automatically and fails the workflow.
Release builds add:
add_compile_options(
-ffile-prefix-map=${CMAKE_SOURCE_DIR}=.
-fmacro-prefix-map=${CMAKE_SOURCE_DIR}=.
)SOURCE_DATE_EPOCH is set from the git commit timestamp in
release.yml. The output should be byte-identical across
reproducible-build environments.
Every source file starts with the SPDX header. Enforced by
scripts/check-spdx.sh in the orchestrator.
Run on every PR:
- clang-tidy with
cert-*,cppcoreguidelines-*,bugprone-*,concurrency-*,clang-analyzer-*(categories disabled at v0.1 are listed in STATIC_ANALYSIS.md). - cppcheck with
--enable=all --inconclusiveand the project-level suppression file.cppcheck-suppress. - include-what-you-use (informational at v0.1; gating in v0.2).
- clang-format-21 for style. Mismatches fail the lint job.
- shellcheck for every shell script under
scripts/andtests/.
- Seed corpus committed at
tests/fuzz/seeds/<harness>/. - Generated corpus stored as a workflow artifact and restored on the next run.
- Crash inputs preserved at
tests/fuzz/crashes/<harness>/.
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
if (size > 10 * 1024 * 1024) { return 0; }
abcpwn::formats::BinaryLoader loader;
[[maybe_unused]] auto result = loader.parse(std::span{data, size});
return 0;
}- PR smoke fuzz: 5 minutes per harness (built but not currently in every PR run; gating moves to PR in v0.2 once timing is tuned).
- Nightly fuzz: one hour per harness.
- Release-time fuzz: four hours per harness before publishing a tag.
Reports go to https://github.com/manop55555/abcpwn/security/advisories/new (private). Acknowledgement within 7 days; fix and CVE assignment within 30 days for critical issues.
See ../SECURITY.md for the full process summary.
Releases include a CycloneDX JSON SBOM listing every transitive dependency with its version and license.
Release artifacts carry SLSA Level 3 build provenance attestations
generated by actions/attest-build-provenance@v1 in release.yml.
Sigstore (cosign) signing of release artifacts is planned for v0.2.
v0.1 releases ship checksums via SHA256SUMS only.
- vcpkg manifest mode with
builtin-baselinecommit pinned invcpkg-configuration.json. - FetchContent uses fixed commit hashes for picosha2 and Keystone.
- GitHub Actions reference dependencies are pinned (Dependabot keeps these current).
- Reporter submits via GitHub Security Advisory (private).
- Maintainer acknowledges within 7 days.
- Patch developed in a private fork.
- CVE assigned via GitHub.
- Fix released; advisory published.
CHANGELOG.mdrecords the fix underSecurity.- Severely affected versions are yanked from the release page.