Add static musl binary build (Docker + local)
ober
bd768a5523407e2e337b8017a07e670c659c40ff
--- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ gitsafe-bin +gitsafe-musl +gitsafe-musl.sha256 *.so *.wpo new file mode 100644 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,97 @@ +# Dockerfile — Build gitsafe-musl in a clean environment +# +# Produces a fully static binary with zero runtime dependencies. +# No Chez Scheme or Jerboa installation needed on the target host. +# +# Usage: +# docker build -t gitsafe-builder . +# docker run --rm gitsafe-builder > gitsafe-musl && chmod +x gitsafe-musl +# +# Or extract via docker cp: +# docker build -t gitsafe-builder . +# id=$(docker create gitsafe-builder) +# docker cp $id:/out/gitsafe-musl ./gitsafe-musl +# docker cp $id:/out/gitsafe-musl.sha256 ./gitsafe-musl.sha256 +# docker rm $id + +FROM ubuntu:24.04 AS builder + +ARG DEBIAN_FRONTEND=noninteractive + +# ── System dependencies ────────────────────────────────────────────────────── +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + musl-tools \ + musl-dev \ + git \ + ca-certificates \ + libncurses-dev \ + uuid-dev \ + liblz4-dev \ + zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* + +# Set HOME early — everything under /build so no real paths leak into binary +ENV HOME=/build +WORKDIR /build + +# ── Build Chez Scheme (stock glibc, for compilation steps) ─────────────────── +RUN git clone --depth 1 https://github.com/ober/ChezScheme.git && \ + cd ChezScheme && \ + git submodule update --init --depth 1 && \ + ./configure --threads --disable-x11 --installprefix=/usr/local && \ + make -j$(nproc) && \ + make install && \ + cd /build && rm -rf ChezScheme + +# ── Build Chez Scheme (musl, for static linking) ──────────────────────────── +# Two-pass build: +# Pass 1: Full build with stock gcc to generate boot files +# Pass 2: Rebuild kernel only with musl-gcc --static, reusing boot files +RUN git clone https://github.com/ober/ChezScheme.git chez-musl-src && \ + cd chez-musl-src && \ + git submodule update --init && \ + ./configure --threads --disable-x11 --installprefix=/build/chez-musl && \ + make -j$(nproc) && \ + cp ta6le/boot/ta6le/petite.boot /tmp/petite.boot && \ + cp ta6le/boot/ta6le/scheme.boot /tmp/scheme.boot && \ + make clean && \ + ./configure --threads --disable-x11 --static CC=musl-gcc --installprefix=/build/chez-musl && \ + mkdir -p ta6le/boot/ta6le && \ + cp /tmp/petite.boot ta6le/boot/ta6le/ && \ + cp /tmp/scheme.boot ta6le/boot/ta6le/ && \ + make -j$(nproc) kernel && \ + make install && \ + cd /build && rm -rf chez-musl-src /tmp/petite.boot /tmp/scheme.boot + +# ── Clone Jerboa ───────────────────────────────────────────────────────────── +ARG CACHE_BUST=0 +WORKDIR /build/mine +RUN git clone --depth 1 https://github.com/ober/jerboa.git + +# ── Copy gitsafe source ───────────────────────────────────────────────────── +COPY . /build/mine/jerboa-gitsafe + +# ── Build gitsafe-musl ─────────────────────────────────────────────────────── +ENV JERBOA_MUSL_CHEZ_PREFIX=/build/chez-musl +ENV JERBOA_HOME=/build/mine/jerboa +WORKDIR /build/mine/jerboa-gitsafe +RUN make gitsafe-musl-local + +# ── Verify ─────────────────────────────────────────────────────────────────── +RUN ./gitsafe-musl --version +RUN echo "--- Binary info ---" && \ + ls -lh gitsafe-musl && \ + file gitsafe-musl && \ + echo "--- Hardening checks ---" && \ + { file gitsafe-musl | grep -qE 'stripped|no section header' && echo " PASS: stripped" || echo " FAIL: not stripped"; } && \ + { test -f gitsafe-musl.sha256 && echo " PASS: integrity hash present" || echo " FAIL: no hash"; } && \ + echo "--- Path leak check ---" && \ + count=$(strings gitsafe-musl | grep -c '/home/' || true) && \ + { [ "$count" -gt 0 ] && echo " WARNING: home paths found ($count)" || echo " PASS: no home path leaks"; } + +# ── Output ─────────────────────────────────────────────────────────────────── +FROM ubuntu:24.04 +COPY --from=builder /build/mine/jerboa-gitsafe/gitsafe-musl /out/gitsafe-musl +COPY --from=builder /build/mine/jerboa-gitsafe/gitsafe-musl.sha256 /out/gitsafe-musl.sha256 +CMD ["cat", "/out/gitsafe-musl"] --- a/Makefile +++ b/Makefile @@ -4,7 +4,8 @@ BIN_DIR := $(HOME)/.local/bin TEMPLATE_DIR := $(HOME)/.git-templates HOOK_DIR := $(TEMPLATE_DIR)/hooks -.PHONY: run test binary install clean +.PHONY: run test binary install clean \ + gitsafe-musl gitsafe-musl-local docker verify-harden help run: JERBOA_HOME=$(JERBOA_HOME) \ @@ -37,7 +38,79 @@ install: binary @echo "All new repos (git init / git clone) will use gitsafe automatically." @echo "To add to an existing repo: cd repo && git init" +# ── Static musl binary ────────────────────────────────────────────────────── +# Use `make gitsafe-musl` to build in Docker (canonical, reproducible). +# Use `make gitsafe-musl-local` to build directly on the host (requires +# musl-gcc and a musl-built Chez at ~/chez-musl or JERBOA_MUSL_CHEZ_PREFIX). + +gitsafe-musl: docker + +gitsafe-musl-local: + JERBOA_HOME=$(JERBOA_HOME) \ + ./build-gitsafe-musl.sh + +docker: + @echo "=== Building gitsafe-musl in Docker ===" + docker build --platform linux/amd64 --build-arg CACHE_BUST=$$(date +%s) -t gitsafe-builder . + @id=$$(docker create --platform linux/amd64 gitsafe-builder) && \ + docker cp $$id:/out/gitsafe-musl ./gitsafe-musl && \ + docker cp $$id:/out/gitsafe-musl.sha256 ./gitsafe-musl.sha256 && \ + docker rm $$id >/dev/null && \ + chmod +x gitsafe-musl + @echo "" + @ls -lh gitsafe-musl + @file gitsafe-musl + +install-musl: gitsafe-musl + mkdir -p $(BIN_DIR) + cp gitsafe-musl $(BIN_DIR)/gitsafe + @echo "Installed gitsafe-musl to $(BIN_DIR)/gitsafe" + mkdir -p $(HOOK_DIR) + printf '#!/bin/sh\nexec gitsafe pre-commit\n' > $(HOOK_DIR)/pre-commit + chmod +x $(HOOK_DIR)/pre-commit + printf '#!/bin/sh\nwhile read local_ref local_sha remote_ref remote_sha; do\n gitsafe pre-push --local-ref "$$local_ref" --remote-ref "$$remote_ref" || exit $$?\ndone\n' > $(HOOK_DIR)/pre-push + chmod +x $(HOOK_DIR)/pre-push + git config --global init.templateDir $(TEMPLATE_DIR) + @echo "" + @echo "Global git hooks installed. Static binary — no runtime dependencies." + +verify-harden: gitsafe-musl + @echo "=== Hardening verification ===" + @(file gitsafe-musl | grep -qE 'stripped|no section header') && echo " PASS: binary is stripped" || echo " FAIL: binary not stripped" + @if strings gitsafe-musl | grep -q "$(HOME)"; then \ + echo " WARN: home directory path found in binary"; \ + else \ + echo " PASS: no home directory paths leaked"; \ + fi + @if [ -f gitsafe-musl.sha256 ]; then \ + echo " PASS: gitsafe-musl.sha256 exists ($$(wc -c < gitsafe-musl.sha256) bytes)"; \ + else \ + echo " FAIL: gitsafe-musl.sha256 not found"; \ + fi + @./gitsafe-musl --version >/dev/null 2>&1 && echo " PASS: binary runs" || echo " FAIL: binary doesn't run" + +# ── Cleanup ────────────────────────────────────────────────────────────────── clean: find . -name '*.so' -delete find . -name '*.wpo' -delete - rm -f gitsafe-bin + rm -f gitsafe-bin gitsafe-musl gitsafe-musl.sha256 + +# ── Help ───────────────────────────────────────────────────────────────────── +help: + @echo "gitsafe — secret-scanning git hooks" + @echo "" + @echo "Development:" + @echo " make run ARGS='...' Run gitsafe in interpreter mode" + @echo " make test Run test suite" + @echo "" + @echo "Native binary (requires local Chez + Jerboa):" + @echo " make binary Build gitsafe-bin (dynamic, native)" + @echo " make install Build + install to ~/.local/bin" + @echo "" + @echo "Static binary (zero runtime dependencies):" + @echo " make gitsafe-musl Docker build (canonical, reproducible)" + @echo " make gitsafe-musl-local Local build (requires musl-gcc + musl Chez)" + @echo " make install-musl Docker build + install to ~/.local/bin" + @echo " make verify-harden Verify binary hardening (stripped, no leaks)" + @echo "" + @echo " make clean Remove all build artifacts" new file mode 100755 --- /dev/null +++ b/build-gitsafe-musl.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# build-gitsafe-musl.sh — Build gitsafe as a fully static binary using musl libc +# +# Prerequisites: +# - musl-gcc installed (apt install musl-tools) +# - Chez Scheme built with: ./configure --threads --static CC=musl-gcc +# and installed to ~/chez-musl (or set JERBOA_MUSL_CHEZ_PREFIX) +# - Jerboa libraries available +# +# The build uses stock scheme (glibc) for the Scheme compilation steps, +# then musl-gcc for the C compilation and linking steps. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# Resolve jerboa +if [ -n "${JERBOA_HOME:-}" ]; then + JERBOA_LIB="${JERBOA_HOME}/lib" +elif [ -d "${SCRIPT_DIR}/../jerboa/lib" ]; then + JERBOA_LIB="$(realpath "${SCRIPT_DIR}/../jerboa/lib")" +elif [ -d "${HOME}/mine/jerboa/lib" ]; then + JERBOA_LIB="${HOME}/mine/jerboa/lib" +else + echo "ERROR: Cannot find Jerboa. Set JERBOA_HOME." + exit 1 +fi +export JERBOA_HOME="${JERBOA_HOME:-$(dirname "$JERBOA_LIB")}" + +echo "===================================" +echo "Building gitsafe-musl (static)" +echo "===================================" +echo "" +echo "Jerboa: $JERBOA_LIB" +echo "" + +# Check musl availability +if ! command -v musl-gcc &>/dev/null; then + echo "ERROR: musl-gcc not found" + echo "Install: sudo apt install musl-tools" + exit 1 +fi + +echo "[1/2] Validating musl toolchain..." +echo " musl-gcc: $(command -v musl-gcc)" +echo "" + +echo "[2/2] Running musl build..." +scheme -q --libdirs "${SCRIPT_DIR}:${JERBOA_LIB}" --script build-gitsafe-musl.ss + +# Verify +if [ -f "gitsafe-musl" ]; then + echo "" + echo "===================================" + echo "gitsafe-musl built successfully!" + echo "===================================" + ls -lh gitsafe-musl + echo "" + file gitsafe-musl + echo "" + ldd gitsafe-musl 2>&1 || echo " (Fully static — no dynamic dependencies)" + echo "" + echo "Test: ./gitsafe-musl --version" +else + echo "ERROR: gitsafe-musl not created" + exit 1 +fi new file mode 100644 --- /dev/null +++ b/build-gitsafe-musl.ss @@ -0,0 +1,245 @@ +#!chezscheme +;; Build gitsafe as a fully static binary using musl libc. +;; +;; Usage: make gitsafe-musl-local +;; (runs via build-gitsafe-musl.sh → this script) +;; +;; Prerequisites: +;; - musl-gcc installed (apt install musl-tools) +;; - Chez Scheme built with: ./configure --threads --static CC=musl-gcc +;; installed to ~/chez-musl (or set JERBOA_MUSL_CHEZ_PREFIX) +;; - Stock scheme (glibc) for compilation steps +;; +;; Produces: ./gitsafe-musl (fully static ELF binary, zero runtime dependencies) + +(import (chezscheme)) + +;; --- Helper: generate C byte-array from binary file --- +(define (file->c-header input-path output-path array-name size-name) + (let* ([port (open-file-input-port input-path)] + [data (get-bytevector-all port)] + [size (bytevector-length data)]) + (close-port port) + (call-with-output-file output-path + (lambda (out) + (fprintf out "/* Auto-generated — do not edit */\n") + (fprintf out "static const unsigned char ~a[] = {\n" array-name) + (let loop ([i 0]) + (when (< i size) + (when (= 0 (modulo i 16)) (fprintf out " ")) + (fprintf out "0x~2,'0x" (bytevector-u8-ref data i)) + (when (< (+ i 1) size) (fprintf out ",")) + (when (= 15 (modulo i 16)) (fprintf out "\n")) + (loop (+ i 1)))) + (fprintf out "\n};\n") + (fprintf out "static const unsigned int ~a = ~a;\n" size-name size)) + 'replace) + (printf " ~a: ~a bytes\n" output-path size))) + +;; --- Locate musl-built Chez Scheme --- +(define musl-chez-prefix + (or (getenv "JERBOA_MUSL_CHEZ_PREFIX") + (let* ([home (getenv "HOME")] + [p (format "~a/chez-musl" home)]) + (and (file-exists? p) p)))) + +(unless musl-chez-prefix + (display "Error: Cannot find musl Chez install.\n") + (display " Set JERBOA_MUSL_CHEZ_PREFIX or install to ~/chez-musl\n") + (display " See: https://github.com/ober/ChezScheme (build with --static CC=musl-gcc)\n") + (exit 1)) + +(define (find-csv-dir lib-dir mt) + (let ([csv-dir + (let lp ([dirs (guard (e [#t '()]) (directory-list lib-dir))]) + (cond + [(null? dirs) #f] + [(and (> (string-length (car dirs)) 3) + (string=? "csv" (substring (car dirs) 0 3))) + (format "~a/~a/~a" lib-dir (car dirs) mt)] + [else (lp (cdr dirs))]))]) + (and csv-dir + (file-exists? (format "~a/main.o" csv-dir)) + csv-dir))) + +(define musl-chez-dir + (let ([mt (symbol->string (machine-type))]) + (or (find-csv-dir (format "~a/lib" musl-chez-prefix) mt) + (begin + (printf "Error: Cannot find Chez ~a dir under ~a/lib\n" + (machine-type) musl-chez-prefix) + (printf " Expected: ~a/lib/csv<version>/~a/main.o\n" + musl-chez-prefix mt) + (exit 1))))) + +;; --- Locate Jerboa --- +(define home (getenv "HOME")) +(define jerboa-dir + (or (getenv "JERBOA_HOME") + (let ([sibling (format "~a/../jerboa" (current-directory))]) + (and (file-exists? sibling) sibling)) + (begin + (display "Error: Cannot find Jerboa. Set JERBOA_HOME.\n") + (exit 1)))) + +(printf "=== gitsafe musl static build ===\n") +(printf "Musl Chez dir: ~a\n" musl-chez-dir) +(printf "Jerboa dir: ~a\n" jerboa-dir) +(printf "Machine type: ~a\n" (machine-type)) + +;; Add library paths (stock Chez for compilation) +(library-directories + (append + (list (cons (current-directory) (current-directory)) + (cons (format "~a/lib" jerboa-dir) + (format "~a/lib" jerboa-dir))) + (library-directories))) + +;; --- Step 1: Compile all modules (optimize-level 3, WPO) --- +(printf "\n[1/6] Compiling all modules (optimize-level 3, WPO)...\n") +(parameterize ([compile-imported-libraries #t] + [optimize-level 3] + [cp0-effort-limit 500] + [cp0-score-limit 50] + [cp0-outer-unroll-limit 1] + [commonization-level 4] + [enable-unsafe-application #t] + [enable-unsafe-variable-reference #t] + [enable-arithmetic-left-associative #t] + [debug-level 0] + [generate-inspector-information #f] + [generate-wpo-files #t]) + (compile-program "gitsafe/main-binary.ss")) + +;; --- Step 2: Whole-program optimization --- +(printf "[2/6] Running whole-program optimization...\n") +(let ([missing (compile-whole-program "gitsafe/main-binary.wpo" "gitsafe-all.so")]) + (unless (null? missing) + (printf " WPO: ~a libraries not incorporated (missing .wpo):\n" (length missing)) + (for-each (lambda (lib) (printf " ~a\n" lib)) missing))) + +;; --- Step 3: Create boot file + C headers --- +(printf "[3/6] Creating boot file and C headers...\n") + +(define (existing-so-files paths) + (filter file-exists? paths)) + +(define gitsafe-modules + '("gitsafe/entropy" + "gitsafe/config" + "gitsafe/allowlist" + "gitsafe/patterns" + "gitsafe/git" + "gitsafe/scanner" + "gitsafe/output")) + +(apply make-boot-file "gitsafe.boot" '("scheme" "petite") + (existing-so-files + (map (lambda (m) (format "~a.so" m)) gitsafe-modules))) + +;; Embed musl Chez boot files (must match musl kernel for ABI compatibility) +(file->c-header "gitsafe-all.so" + "gitsafe_program.h" + "gitsafe_program_data" "gitsafe_program_size") +(file->c-header (format "~a/petite.boot" musl-chez-dir) + "gitsafe_petite_boot.h" + "petite_boot_data" "petite_boot_size") +(file->c-header (format "~a/scheme.boot" musl-chez-dir) + "gitsafe_scheme_boot.h" + "scheme_boot_data" "scheme_boot_size") +(file->c-header "gitsafe.boot" + "gitsafe_boot.h" + "gitsafe_boot_data" "gitsafe_boot_size") + +;; --- Step 4: Generate C main with embedded program --- +(printf "[4/6] Generating C main...\n") + +(call-with-output-file "gitsafe-main-musl.c" + (lambda (out) + (fprintf out "/* Auto-generated — do not edit */\n") + (fprintf out "#define _GNU_SOURCE\n") + (fprintf out "#include <stdlib.h>\n") + (fprintf out "#include <stdio.h>\n") + (fprintf out "#include <string.h>\n") + (fprintf out "#include <unistd.h>\n") + (fprintf out "#include \"scheme.h\"\n") + (fprintf out "#include \"gitsafe_petite_boot.h\"\n") + (fprintf out "#include \"gitsafe_scheme_boot.h\"\n") + (fprintf out "#include \"gitsafe_boot.h\"\n") + (fprintf out "#include \"gitsafe_program.h\"\n") + (fprintf out "\n") + ;; dlopen/dlsym stubs for static musl build + ;; Chez kernel references these; returning non-NULL from dlopen prevents + ;; load-shared-object from erroring, while dlsym returns NULL for any lookup. + (fprintf out "/* dlopen/dlsym stubs — no shared libraries in static binary */\n") + (fprintf out "void *dlopen(const char *f, int m) { (void)f; (void)m; return (void*)1; }\n") + (fprintf out "void *dlsym(void *h, const char *s) { (void)h; (void)s; return NULL; }\n") + (fprintf out "int dlclose(void *h) { (void)h; return 0; }\n") + (fprintf out "char *dlerror(void) { return \"static build\"; }\n") + (fprintf out "\n") + (fprintf out "int main(int argc, char *argv[]) {\n") + (fprintf out " char prog_path[256];\n") + (fprintf out " const char *tmpdir = getenv(\"TMPDIR\");\n") + (fprintf out " if (!tmpdir) tmpdir = \"/tmp\";\n") + (display " snprintf(prog_path, sizeof(prog_path), \"%s/gitsafe-XXXXXX\", tmpdir);\n" out) + (fprintf out " int fd = mkstemp(prog_path);\n") + (fprintf out " if (fd < 0) { perror(\"mkstemp\"); return 1; }\n") + (fprintf out " if (write(fd, gitsafe_program_data, gitsafe_program_size)\n") + (fprintf out " != (ssize_t)gitsafe_program_size) {\n") + (fprintf out " perror(\"write\"); close(fd); unlink(prog_path); return 1;\n") + (fprintf out " }\n") + (fprintf out " close(fd);\n") + (fprintf out "\n") + (fprintf out " Sscheme_init(NULL);\n") + (fprintf out " Sregister_boot_file_bytes(\"petite\", (void*)petite_boot_data, petite_boot_size);\n") + (fprintf out " Sregister_boot_file_bytes(\"scheme\", (void*)scheme_boot_data, scheme_boot_size);\n") + (fprintf out " Sregister_boot_file_bytes(\"gitsafe\", (void*)gitsafe_boot_data, gitsafe_boot_size);\n") + (fprintf out " Sbuild_heap(NULL, NULL);\n") + (fprintf out " int status = Sscheme_script(prog_path, argc, (const char **)argv);\n") + (fprintf out " unlink(prog_path);\n") + (fprintf out " Sscheme_deinit();\n") + (fprintf out " return status;\n") + (fprintf out "}\n")) + 'replace) + +;; --- Step 5: Compile and link with musl-gcc --- +(printf "[5/6] Compiling and linking with musl-gcc (static)...\n") + +(define link-libs "-lkernel -llz4 -lz -lm -ldl -lpthread -luuid -lncurses") + +(let ([rc (system (format "musl-gcc -c -O2 -I~a -o gitsafe-main-musl.o gitsafe-main-musl.c" + musl-chez-dir))]) + (unless (= rc 0) (printf "Error: C compilation failed\n") (exit 1))) + +(let ([rc (system (format "musl-gcc -o gitsafe-musl gitsafe-main-musl.o -L~a ~a -static -Wl,--allow-multiple-definition" + musl-chez-dir link-libs))]) + (unless (= rc 0) (printf "Error: linking failed\n") (exit 1))) + +;; Strip and generate integrity hash +(printf " Stripping binary...\n") +(system "strip --strip-all gitsafe-musl") +(system "sha256sum gitsafe-musl > gitsafe-musl.sha256") + +;; --- Step 6: Cleanup --- +(printf "[6/6] Cleaning up intermediate files...\n") +(for-each (lambda (f) (when (file-exists? f) (delete-file f))) + '("gitsafe-main-musl.c" "gitsafe-main-musl.o" + "gitsafe_program.h" "gitsafe_petite_boot.h" + "gitsafe_scheme_boot.h" "gitsafe_boot.h" + "gitsafe-all.so" "gitsafe.boot" + "gitsafe/main-binary.wpo" "gitsafe/main-binary.so")) + +(for-each (lambda (m) + (for-each (lambda (ext) + (let ([f (format "~a~a" m ext)]) + (when (file-exists? f) (delete-file f)))) + '(".so" ".wpo"))) + gitsafe-modules) + +(printf "\nDone! Binary: ./gitsafe-musl\n") +(printf " Size: ") +(system "ls -lh gitsafe-musl | awk '{print $5}'") +(printf " SHA256: ") +(system "cat gitsafe-musl.sha256") +(printf "\n Test: ./gitsafe-musl --version\n") +(printf " Verify: file gitsafe-musl && ldd gitsafe-musl\n")