diff --git a/packages/gef/build.ncl b/packages/gef/build.ncl new file mode 100644 index 00000000..d308d80c --- /dev/null +++ b/packages/gef/build.ncl @@ -0,0 +1,77 @@ +let { standaloneTest, Attrs, BuildSpec, Local, OutputBin, OutputData, Source, Test, .. } = import "minimal.ncl" in +let base = import "../base/build.ncl" in +let bash = import "../bash/build.ncl" in +let gdb = import "../gdb/build.ncl" in +let python = import "../python/build.ncl" in + +let version = "2026.01" in +{ + name = "gef", + build_deps = [ + { file = "build.sh" } | Local, + { + url = "https://github.com/hugsy/gef/archive/refs/tags/%{version}.tar.gz", + sha256 = "6e58afbda13ef976aa46a2f138b5422829a0d3c4b71246f4546cbd7383ce2e76", + } | Source, + base, + ], + runtime_deps = [ + bash, + # gef is a gdb EXTENSION — it is not a program, it is 433 KB of Python that + # gdb loads. Both of these are hard requirements, not conveniences: + # gef.py asserts GDB_MIN_VERSION (10,0) and PYTHON_MIN_VERSION (3,10) at + # import time and refuses to load otherwise. Ours are gdb 17.2 and Python + # 3.14, comfortably past both. + # + # The gdb package is built --with-python specifically so this works; a gdb + # without it reports "Python scripting is not supported in this copy of + # GDB" and gef cannot load at all. That flag is pinned by a test over + # there rather than trusted. + gdb, + python, + ], + + cmd = "./build.sh", + build_args = { + include version, + }, + + outputs = { + gef = { glob = "usr/bin/gef" } | OutputBin, + gef_py = { glob = "usr/share/gef/gef.py" } | OutputData, + }, + + tests = { + smoketest = standaloneTest "/bin/gef --version", + + # Loading is the whole package. gef is a single Python file, so there is no + # compile step that could fail — the only way this package breaks is that + # gdb cannot IMPORT it, and that failure is quiet: gdb prints the Python + # traceback to stderr and then carries on to a normal prompt, exiting 0. + # So assert on gef's own output, never on gdb's exit code. + loads_into_gdb = + { + class = 'Standalone, + test_deps = [base], + cmds = [ + # `gef` is registered as a gdb command only if gef.py imported + # cleanly all the way to the bottom of the file. + ["/bin/bash", "-c", "gef -batch -ex 'gef help' 2>&1 | grep -qi 'gef'"], + # A traceback on stderr means a partial import — gef would look + # present while most commands were missing. + ["/bin/bash", "-c", "! gef -batch -ex 'gef help' 2>&1 | grep -q 'Traceback'"], + ], + } | Test, + }, + + attrs = + { + upstream_version = version, + license_spdx = "MIT", + source_provenance = { + category = 'GithubRepo, + owner = "hugsy", + repo = "gef", + }, + } | Attrs, +} | BuildSpec diff --git a/packages/gef/build.sh b/packages/gef/build.sh new file mode 100755 index 00000000..61dce77d --- /dev/null +++ b/packages/gef/build.sh @@ -0,0 +1,40 @@ +#!/bin/sh +set -eux + +# A GitHub tag archive arrives named after the URL's last component — for +# `.../archive/refs/tags/2026.01.tar.gz` that is `2026.01.tar.gz`, NOT +# `gef-2026.01.tar.gz`. Extract whatever tarball is here rather than encoding a +# guess: the failure mode for guessing wrong is tar's "Error is not +# recoverable", which says nothing useful. +for t in *.tar.gz; do + [ -f "$t" ] || continue + tar -xof "$t" +done + +# The archive expands to `gef-/`, but locate the file rather than +# assume — this is the second thing that would break silently on an upstream +# repackaging. +GEF_PY=$(find . -name gef.py -maxdepth 3 -type f | head -1) +if [ -z "$GEF_PY" ]; then + echo "cannot find gef.py after extraction; tree is:" >&2 + ls -la >&2 + exit 1 +fi + +mkdir -p "$OUTPUT_DIR/usr/share/gef" "$OUTPUT_DIR/usr/bin" +cp "$GEF_PY" "$OUTPUT_DIR/usr/share/gef/gef.py" + +# A LAUNCHER, not a dotfile edit. Upstream's install instructions append a +# `source` line to ~/.gdbinit, which would make this package mutate the user's +# home directory and silently change the behaviour of every unrelated `gdb` +# invocation on the system. Ship a separate entry point instead: `gef` is gdb +# with gef loaded, and `gdb` stays exactly what it was. +# +# -q suppresses the banner so gef's own header is the first thing you see. +# -x sources gef BEFORE the target is loaded, which is what gef expects; any +# further args (a binary, --args, -p PID) are forwarded untouched. +cat > "$OUTPUT_DIR/usr/bin/gef" << 'EOF' +#!/bin/sh +exec gdb -q -x /usr/share/gef/gef.py "$@" +EOF +chmod 755 "$OUTPUT_DIR/usr/bin/gef" diff --git a/packages/rizin/build.ncl b/packages/rizin/build.ncl new file mode 100644 index 00000000..82fbdda3 --- /dev/null +++ b/packages/rizin/build.ncl @@ -0,0 +1,139 @@ +let { standaloneTest, Attrs, BuildSpec, Local, OutputBin, OutputData, OutputLib, Source, Test, .. } = import "minimal.ncl" in +let base = import "../base/build.ncl" in +let bash = import "../bash/build.ncl" in +let binutils = import "../binutils/build.ncl" in +let gcc = import "../gcc/build.ncl" in +let glibc = import "../glibc/build.ncl" in +let lz4 = import "../lz4/build.ncl" in +let meson = import "../meson/build.ncl" in +let ninja = import "../ninja/build.ncl" in +let openssl = import "../openssl/build.ncl" in +let pcre2 = import "../pcre2/build.ncl" in +let pkgconf = import "../pkgconf/build.ncl" in +let python = import "../python/build.ncl" in +let toolchain = import "../toolchain/build.ncl" in +let tree-sitter = import "../tree-sitter/build.ncl" in +let xz = import "../xz/build.ncl" in +let zlib = import "../zlib/build.ncl" in +let zstd = import "../zstd/build.ncl" in + +let version = "0.9.1" in +{ + name = "rizin", + build_deps = [ + { file = "build.sh" } | Local, + # The official `rizin-src` tarball, NOT a GitHub tag archive: it vendors + # the meson subprojects (capstone-next, tree-sitter, pcre2, lz4, nettle, + # blake2/3, libmspack, softfloat, libzip...), which is what lets the build + # run with --wrap-mode=nodownload and no network. + { + url = "https://github.com/rizinorg/rizin/releases/download/v%{version}/rizin-src-v%{version}.tar.xz", + sha256 = "7ac1cd7daca7afdda742e15478b1f747fc1f813e496fee71839d1e109e543dca", + } | Source, + base, + toolchain, + gcc, + meson, + ninja, + pkgconf, + python, # meson is Python + glibc, + # The seven libraries we link from the SYSTEM rather than let rizin vendor, + # so a CVE in any of them is visible to pkgscan instead of buried in a + # static blob. See the use_sys_* rationale in build.sh. + zlib, + zstd, + xz, + lz4, + pcre2, + openssl, + tree-sitter, + ], + runtime_deps = [ + bash, + glibc, + zlib, + zstd, + xz, + lz4, + pcre2, + openssl, + tree-sitter, + ], + + cmd = "./build.sh", + build_args = { + include version, + }, + + outputs = { + # The headless/scriptable counterpart to Ghidra: Ghidra answers "what does + # this function do", rizin answers "run that over 400 binaries and diff the + # results". + rizin = { glob = "usr/bin/rizin" } | OutputBin, + rz_bins = { glob = "usr/bin/rz-*" } | OutputBin, + librz = { glob = "usr/lib/librz*.so*" } | OutputLib, + rz_data = { glob = "usr/share/rizin/**" } | OutputData, + }, + + tests = { + smoketest = standaloneTest "/bin/rizin -v", + + # Pin the two properties the use_sys_* flags exist for. Both regress + # SILENTLY: meson probes for each system library and falls back to the + # vendored subproject when one is not found, printing a note and carrying + # on to a successful build. You would get a rizin that works perfectly and + # has seven invisible bundled libraries — the exact outcome these flags + # exist to prevent, and one nothing else here would detect. + system_libs = + { + class = 'Standalone, + # binutils for readelf. The first version of this test used readelf + # with only `base` declared and failed with "readelf: command not + # found" — loudly, which is the correct behaviour and how this was + # caught, but it is a dependency and belongs here. + test_deps = [base, binutils], + cmds = [ + # Assert on the librz_* SHARED LIBRARIES, not on /usr/bin/rizin: + # the rizin binary is a thin frontend and carries none of these in + # its own DT_NEEDED — the linkage lives in the libraries. Checking + # the wrong file would have produced a test that always failed. + [ + "/bin/bash", + "-c", + m%" + set -eu + need="libz.so libzstd liblzma liblz4 libpcre2 libcrypto libtree-sitter" + all=$(readelf -d /usr/lib/librz*.so.* 2>/dev/null) + for lib in $need; do + case "$all" in + *"$lib"*) ;; + *) echo "$lib is NOT linked from the system — the use_sys_* flag" >&2 + echo "silently fell back to rizin's vendored copy, which pkgscan" >&2 + echo "cannot see." >&2 + exit 1 ;; + esac + done + "% + ], + # And that it actually analyses something. + ["/bin/bash", "-c", "rz-bin -I /bin/rizin | grep -qi 'bintype'"], + ], + } | Test, + }, + + attrs = + { + upstream_version = version, + # Primary licence is LGPL-3.0-only (1813 SPDX headers), but the linked + # binary aggregates a 16-licence REUSE union that includes GPL-3.0 and + # GPL-2.0 files — so the shipped artifact is effectively GPL-3.0, and + # recording the permissive core alone would understate it. + license_spdx = "GPL-3.0-or-later", + source_provenance = { + category = 'GithubRepo, + owner = "rizinorg", + repo = "rizin", + }, + } | Attrs, +} | BuildSpec diff --git a/packages/rizin/build.sh b/packages/rizin/build.sh new file mode 100755 index 00000000..1557ec31 --- /dev/null +++ b/packages/rizin/build.sh @@ -0,0 +1,64 @@ +#!/bin/sh +set -eux + +tar -xof "rizin-src-v${MINIMAL_ARG_VERSION}.tar.xz" +cd "rizin-v${MINIMAL_ARG_VERSION}" + +mkdir build +cd build + +case $(uname -m) in + x86_64) MARCH="-march=x86-64-v3" ;; + aarch64) MARCH="-march=armv8-a" ;; + *) MARCH="" ;; +esac +export CFLAGS="$MARCH -O2 -pipe -gno-record-gcc-switches -ffile-prefix-map=$(pwd)=/builddir" +export CXXFLAGS="$CFLAGS" +export LDFLAGS="-Wl,--build-id=none" +export ARFLAGS=Drc + +# THE use_sys_* DECISIONS ARE THE WHOLE JOB HERE, not the build. +# +# All seventeen `use_sys_*` options default to DISABLED, so out of the box +# rizin statically links its own copies of zlib, zstd, xz, lz4, pcre2, openssl +# and tree-sitter. Those copies are then INVISIBLE to pkgscan: a CVE in any of +# them would not appear against this package, because nothing in the tree +# declares them. For a distro whose entire premise is supply-chain vuln +# tracking, shipping seven silently-vendored libraries is the wrong default. +# +# So flip every one we actually package, and no more: +# zlib zstd lzma(xz) lz4 pcre2 openssl tree_sitter -> system +# +# Deliberately left VENDORED, with reasons: +# capstone rizin defaults to use_capstone_version=next, i.e. the +# unreleased capstone 6 — there is no released tarball that +# corresponds, so a "system" capstone would be a DIFFERENT +# disassembler than the one rizin was tested against. +# libzip, magic, zydis, xxhash, libmspack, softfloat, blake2, blake3 +# not packaged here; vendored is the only option today. Each is a +# future pkgscan blind spot, so they are named rather than left +# for someone to discover. +# +# --wrap-mode=nodownload is what makes the build hermetic: the official +# `rizin-src` tarball vendors its subprojects (capstone-next, tree-sitter, +# pcre2, lz4, nettle, blake2/3, libmspack, softfloat, ...), and this flag makes +# meson FAIL rather than reach for the network if one is ever missing. Note +# `liblzma` and `sigdb` ship as bare .wrap files, not bundled sources — the +# former is covered by use_sys_lzma below; the latter means no signature +# database, which is a real functional gap and not silently papered over. +meson setup \ + --prefix=/usr \ + --buildtype=release \ + --wrap-mode=nodownload \ + -Duse_sys_zlib=enabled \ + -Duse_sys_libzstd=enabled \ + -Duse_sys_lzma=enabled \ + -Duse_sys_lz4=enabled \ + -Duse_sys_pcre2=enabled \ + -Duse_sys_openssl=enabled \ + -Duse_sys_tree_sitter=enabled \ + .. + +ninja + +DESTDIR="$OUTPUT_DIR" ninja install diff --git a/stacks/reveng/stack.ncl b/stacks/reveng/stack.ncl new file mode 100644 index 00000000..76188aad --- /dev/null +++ b/stacks/reveng/stack.ncl @@ -0,0 +1,118 @@ +let { stack, .. } = import "minimal.ncl" in +# The reverse-engineering loadout: one session that takes an unknown binary from +# "what is this file" to "here is the C it decompiles to" to "here is what it +# does when it runs" — +# +# file ./blob # identify +# readelf -h ./blob # inspect (arch-agnostic, any e_machine) +# analyzeHeadless /tmp proj -import ./blob \ +# -postScript Decompile.java # DECOMPILE, scriptably +# ghidraRun # ...or drive it by hand +# gdb -q ./blob # break, step, inspect +# gcore # dump a live process +# strace -f ./blob # syscalls +# diffoscope ./blob ./blob.patched # what actually changed +# +# Opt-in by design: there is deliberately NO `matches_project_if_any`. Nothing +# about a source tree implies you want an RE bench — you ask for this one, the +# way you ask for the `aeneas` verification loadout. +# +# Everything here is OSI/FSF-open. Tools that would have been obvious additions +# were excluded on licence grounds and are named at the bottom so nobody +# re-proposes them. +stack { + name = "reveng", + + build_packages = [ + # Build base. Present so you can compile a minimal repro or a test harness + # against the thing you are reversing — half of RE is "does my theory about + # this function actually reproduce". + "gcc", + "binutils", + "make", + "pkgconf", + "linux_headers", + + # THE ANCHOR. Ghidra is the only credible open decompiler for native code, + # and `analyzeHeadless` is what makes it a tool rather than a GUI toy — it + # runs the same analysis over 400 binaries unattended. + # + # Shipped as the official prebuilt release (Apache-2.0): its Gradle build + # fetches 67 URLs from seven hosts before compiling a line, so a hermetic + # source build is not on the table. It carries no bundled JRE, so it runs + # on the `jdk` below rather than smuggling in a second Java. + # + # On arm64 the five platform natives are built from source at package time, + # because upstream publishes none for linux_arm_64 — without that Ghidra + # imports and disassembles and then silently produces no C. + "ghidra", + "jdk", + + # The scriptable half. Ghidra answers "what does this function do"; rizin + # answers "run that over 400 binaries and diff the results" — the shape + # this distro's own tooling works in. Built against our system + # zlib/zstd/xz/lz4/pcre2/openssl/tree-sitter rather than its seven vendored + # copies, so a CVE in any of them is visible to pkgscan. + "rizin", + + # The dynamic half, and the biggest hole in the distro before this loadout: + # nothing could set a breakpoint or open a core dump. Built --with-python + # (so gdb can host Python extensions) and --enable-targets=all (so it can + # disassemble foreign architectures). + "gdb", + # gef turns gdb into the thing people actually want next to a decompiler: + # context on every stop, heap inspection, pattern search. It is a gdb + # EXTENSION, not a program — installed as its own launcher so `gef` is gdb + # with gef loaded and `gdb` stays exactly what it was. + "gef", + + # Already-shipping tools that are load-bearing in this workflow, listed + # explicitly so the session is self-describing rather than inheriting them + # from the base image: identify, inspect, trace, patch, diff. + "file", + "elfutils", + "patchelf", + "strace", + "diffoscope", + "hexyl", + "python", + ], + + build_env_vars = { + # Ghidra's launcher searches a hardcoded list of distro JDK paths that does + # not include our merged /usr tree, and fails with "Failed to find a + # supported JDK" even when `java` is on PATH. Same value the `gradle` stack + # uses. + JAVA_HOME = "/usr/lib/jvm", + + # Read by Ghidra scripts and by PyGhidra (in-tree since 12.x, no extension + # needed) to locate the install root. + GHIDRA_INSTALL_DIR = "/usr/share/ghidra", + }, + + # An analysis bench has no canonical "build this directory" — the real use is + # the pipeline in the header comment, driven by hand against a target binary. + # Surface the anchor so a broken session fails loudly at start rather than + # when you first reach for the decompiler. + # Exercise all four anchors, so a session that cannot decompile, cannot + # debug, cannot script or cannot load gef fails AT START rather than an hour + # in. Each of these has a silent failure mode: analyzeHeadless exits 0 with + # no decompiler, gdb builds fine without Python (and then gef cannot load), + # and rizin builds fine having silently vendored seven libraries. + build_cmd = "analyzeHeadless -help > /dev/null && rizin -v && gdb --version && gef -batch -ex 'gef help' > /dev/null", +} +# +# DELIBERATELY ABSENT, for the record: +# +# volatility3 Volatility Software License — NOT OSI/FSF. Its copyleft is +# broader than AGPL: "Additions" explicitly includes "software +# designed to execute the software and parse its results, such +# as a wrapper". Memory forensics has no open replacement; +# MemProcFS was the fallback and bundles Elastic-License rules. +# peda CC-BY-NC-SA-3.0 (NonCommercial). Widely mislabelled "GPL". +# nmap NPSL, not plain GPL; §3 reaches software that merely executes +# it and parses the results. A judgement call, not a slip. +# frida Genuinely open (wxWindows 3.1, OSI-approved) — everyone +# assumes otherwise. Excluded on COST: all ten components are +# git submodules with ~36 pinned forks including a Vala +# compiler. Revisit when the rest of this is paying for itself.