commit b7bb677fe3c6a57411463120d5b3e27d60cc085d
parent 771ea6e79386c386bfdc40a35019c81a8de76f22
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 16 Jul 2026 10:24:29 -0700
test: cover linked image stripping across formats
Diffstat:
2 files changed, 279 insertions(+), 1 deletion(-)
diff --git a/doc/RELEASE_AUDIT_2026_6_0.md b/doc/RELEASE_AUDIT_2026_6_0.md
@@ -500,7 +500,7 @@ Implementation checkpoint (updated 2026-07-16):
| KIT-P2-001 | **Complete** | `test-driver-stack-protector`: 111 pass, 0 fail; cJSON build/test passes |
| KIT-P1-015 | **Complete** | 21 multi-source dependency assertions pass in the driver suite |
| KIT-P2-003 | **Complete** | `test-driver-diagnostics`: 15 pass, 0 fail |
-| KIT-P1-007 | In progress | public ELF/Mach-O/PE rewrite unit: 23 checks, 0 failures; runnable matrix pending |
+| KIT-P1-007 | **Complete** | `test-driver-strip`: object API 23/0, relocatable/archive 5/0, linked ELF/Mach-O/PE matrix 91/0 |
| KIT-P1-017 | In progress | implementation under focused CLI audit |
| KIT-P2-002 | In progress | implementation under focused CLI audit |
| KIT-P3-003 | **Complete** | `test-link-macho-symbols` passes aarch64 and x86-64 Kit/platform oracles |
diff --git a/test/strip/linked.sh b/test/strip/linked.sh
@@ -0,0 +1,278 @@
+#!/bin/sh
+# Linked-image strip acceptance: loader metadata is an exact oracle, executable
+# modes survive transactional -o rewrites, native images remain runnable, and
+# signed Mach-O inputs require an explicit signature-removal opt-in.
+
+set -u
+
+script_dir=$(cd "$(dirname "$0")" && pwd)
+repo_root=$(cd "$script_dir/../.." && pwd)
+KIT_KIT_DIR="$repo_root/test/lib"
+. "$repo_root/test/lib/kit_sh_kit.sh"
+
+KIT=${KIT:-"$repo_root/build/kit"}
+export KIT
+kit_require_kit strip-linked
+
+kit_workdir strip-linked
+work=$KIT_WORK
+kit_report_init
+
+loader_dump() { # INPUT OUTPUT
+ "$KIT" objdump -p -R "$1" 2> "$2.err" |
+ awk '/^ __DWARF[[:space:]]/ { getline; next } { print }' |
+ sed -e 's|^.*:\t\(.*private headers\)$|<image>:\t\1|' \
+ -e 's|^.*:\t\(file format.*\)$|<image>:\t\1|' > "$2"
+}
+
+dynamic_dump() { # INPUT OUTPUT
+ "$KIT" objdump -T "$1" 2> "$2.err" |
+ sed -e 's|^.*:\t\(file format.*\)$|<image>:\t\1|' > "$2"
+}
+
+assert_no_debug() { # NAME INPUT
+ and_name=$1
+ and_input=$2
+ "$KIT" objdump -h "$and_input" > "$work/$and_name.sections" 2>&1
+ if grep -E '(^|[, ])(__DWARF|\.debug_|\.zdebug_)' \
+ "$work/$and_name.sections" >/dev/null 2>&1; then
+ not_ok "$and_name" "$work/$and_name.sections"
+ else
+ ok "$and_name"
+ fi
+}
+
+strip_shape() { # NAME INPUT [REMOVE_SIGNATURE] [PRESERVE_ALL_NLIST]
+ ss_name=$1
+ ss_input=$2
+ ss_signature=${3:-0}
+ ss_preserve_all_nlist=${4:-1}
+ ss_sigflag=
+ [ "$ss_signature" -eq 0 ] || ss_sigflag=--remove-signature
+
+ chmod 0751 "$ss_input"
+ loader_dump "$ss_input" "$work/$ss_name.before"
+ dynamic_dump "$ss_input" "$work/$ss_name.before.dynamic"
+
+ # shellcheck disable=SC2086 -- optional single flag is intentional.
+ run_ok "$ss_name-strip-debug" "$KIT" strip --strip-debug $ss_sigflag \
+ -o "$work/$ss_name.debug" "$ss_input"
+ check_mode "$ss_name-debug-mode" "$work/$ss_name.debug" 751
+ assert_no_debug "$ss_name-debug-sections" "$work/$ss_name.debug"
+ loader_dump "$work/$ss_name.debug" "$work/$ss_name.debug.loader"
+ same_file "$ss_name-debug-loader" "$work/$ss_name.before" \
+ "$work/$ss_name.debug.loader"
+ dynamic_dump "$work/$ss_name.debug" "$work/$ss_name.debug.dynamic"
+ same_file "$ss_name-debug-dynamic" "$work/$ss_name.before.dynamic" \
+ "$work/$ss_name.debug.dynamic"
+
+ # shellcheck disable=SC2086 -- optional single flag is intentional.
+ run_ok "$ss_name-strip-all" "$KIT" strip --strip-all $ss_sigflag \
+ -o "$work/$ss_name.all" "$ss_input"
+ check_mode "$ss_name-all-mode" "$work/$ss_name.all" 751
+ assert_no_debug "$ss_name-all-sections" "$work/$ss_name.all"
+ loader_dump "$work/$ss_name.all" "$work/$ss_name.all.loader"
+ same_file "$ss_name-all-loader" "$work/$ss_name.before" \
+ "$work/$ss_name.all.loader"
+ if [ "$ss_preserve_all_nlist" -ne 0 ]; then
+ dynamic_dump "$work/$ss_name.all" "$work/$ss_name.all.dynamic"
+ same_file "$ss_name-all-dynamic" "$work/$ss_name.before.dynamic" \
+ "$work/$ss_name.all.dynamic"
+ fi
+}
+
+# Cross-format, cross-architecture real linked images. These do not need a
+# hosted sysroot and therefore run on every build host.
+cat > "$work/x64-start.c" <<'EOF'
+__attribute__((noreturn)) void _start(void) {
+ register long nr __asm__("rax") = 231;
+ register long code __asm__("rdi") = 42;
+ __asm__ volatile("syscall" : : "r"(nr), "r"(code) : "memory");
+ __builtin_unreachable();
+}
+EOF
+
+run_ok elf-static-build "$KIT" cc -target x86_64-linux-gnu -g \
+ -ffreestanding -fno-stack-protector -fno-PIC -fno-pie -nostdlib -static \
+ -Wl,-e,_start "$work/x64-start.c" -o "$work/elf-static"
+if [ -f "$work/elf-static" ]; then
+ strip_shape elf-static "$work/elf-static"
+fi
+
+run_ok elf-pie-build "$KIT" cc -target x86_64-linux-gnu -g \
+ -ffreestanding -fno-stack-protector -fPIE -pie -nostdlib \
+ -Wl,-e,_start "$work/x64-start.c" -o "$work/elf-pie"
+if [ -f "$work/elf-pie" ]; then
+ strip_shape elf-pie "$work/elf-pie"
+fi
+
+cat > "$work/elf-shared.c" <<'EOF'
+extern int imported_value;
+_Thread_local int tls_value = 7;
+int exported_value(void) { return imported_value + tls_value; }
+EOF
+run_ok elf-shared-build "$KIT" cc -target x86_64-linux-gnu -g \
+ -ffreestanding -fno-stack-protector -fPIC -shared -nostdlib \
+ "$work/elf-shared.c" -o "$work/elf-shared.so"
+if [ -f "$work/elf-shared.so" ]; then
+ strip_shape elf-shared "$work/elf-shared.so"
+fi
+
+cat > "$work/pe-start.c" <<'EOF'
+void _start(void) { for (;;) {} }
+int exported_value(void) { return 7; }
+EOF
+for pe_arch in x86_64 aarch64; do
+ run_ok "pe-$pe_arch-build" "$KIT" cc -target "$pe_arch-windows" -g \
+ -ffreestanding -fno-stack-protector -nostdlib -Wl,-e,_start \
+ "$work/pe-start.c" -o "$work/pe-$pe_arch.exe"
+ if [ -f "$work/pe-$pe_arch.exe" ]; then
+ strip_shape "pe-$pe_arch" "$work/pe-$pe_arch.exe"
+ fi
+done
+
+host_os=$(uname -s 2>/dev/null || printf unknown)
+host_arch=$(uname -m 2>/dev/null || printf unknown)
+
+if [ "$host_os" = Darwin ]; then
+ # Kit's native output is a dynamically linked PIE carrying an ad-hoc code
+ # signature. Default failure must leave both input and a pre-existing -o
+ # destination byte-for-byte unchanged.
+ printf 'int helper(void) { return 0; } int main(void) { return helper(); }\n' \
+ > "$work/native-app.c"
+ run_ok macho-native-build "$KIT" cc -g "$work/native-app.c" \
+ -o "$work/macho-native"
+ if [ -f "$work/macho-native" ]; then
+ run_ok macho-native-run-before "$work/macho-native"
+ cp "$work/macho-native" "$work/macho-native.input-snapshot"
+ printf 'preexisting output\n' > "$work/macho-signed-destination"
+ cp "$work/macho-signed-destination" "$work/macho-signed-destination.want"
+ "$KIT" strip --strip-debug -o "$work/macho-signed-destination" \
+ "$work/macho-native" > "$work/macho-signed-default.out" \
+ 2> "$work/macho-signed-default.err"
+ macho_signed_rc=$?
+ if [ "$macho_signed_rc" -eq 1 ]; then
+ ok macho-signed-default-status
+ else
+ printf 'wanted 1, got %s\n' "$macho_signed_rc" \
+ > "$work/macho-signed-default.diag"
+ not_ok macho-signed-default-status "$work/macho-signed-default.diag"
+ fi
+ contains macho-signed-default-guidance "$work/macho-signed-default.err" \
+ "remove-signature"
+ same_file macho-signed-input-unchanged \
+ "$work/macho-native.input-snapshot" "$work/macho-native"
+ same_file macho-signed-output-unchanged \
+ "$work/macho-signed-destination.want" \
+ "$work/macho-signed-destination"
+
+ # MH_EXECUTE's ordinary external nlist is removable when its export trie
+ # owns loader visibility; the dylib case below requires an exact dynamic
+ # symbol view as well.
+ strip_shape macho-native "$work/macho-native" 1 0
+ run_ok macho-native-debug-resign codesign -f -s - \
+ "$work/macho-native.debug"
+ run_ok macho-native-all-resign codesign -f -s - \
+ "$work/macho-native.all"
+ run_ok macho-native-debug-run "$work/macho-native.debug"
+ run_ok macho-native-all-run "$work/macho-native.all"
+ fi
+
+ # A platform-linked dylib supplies an independent producer/oracle and proves
+ # exports, imports, unwind data, and install-name metadata remain usable.
+ sdk=$(xcrun --sdk macosx --show-sdk-path 2>/dev/null || printf '')
+ if [ -n "$sdk" ] && command -v clang >/dev/null 2>&1; then
+ printf 'int linked_answer(void) { return 42; }\n' > "$work/dylib.c"
+ printf 'int linked_answer(void); int main(void) { return linked_answer() == 42 ? 0 : 1; }\n' \
+ > "$work/dylib-main.c"
+ run_ok macho-dylib-build env SDKROOT="$sdk" clang -isysroot "$sdk" -g \
+ -dynamiclib "$work/dylib.c" -install_name @rpath/libanswer.dylib \
+ -o "$work/libanswer.dylib"
+ run_ok macho-dylib-client-build env SDKROOT="$sdk" clang -isysroot "$sdk" \
+ "$work/dylib-main.c" -L"$work" -lanswer \
+ -Wl,-rpath,@loader_path -o "$work/dylib-client"
+ run_ok macho-dylib-run-before "$work/dylib-client"
+ if [ -f "$work/libanswer.dylib" ]; then
+ strip_shape macho-dylib "$work/libanswer.dylib" 1
+ run_ok macho-dylib-debug-resign codesign -f -s - \
+ "$work/macho-dylib.debug"
+ run_ok macho-dylib-all-resign codesign -f -s - \
+ "$work/macho-dylib.all"
+ cp "$work/libanswer.dylib" "$work/libanswer.original"
+ cp "$work/macho-dylib.debug" "$work/libanswer.dylib"
+ run_ok macho-dylib-debug-run "$work/dylib-client"
+ cp "$work/macho-dylib.all" "$work/libanswer.dylib"
+ run_ok macho-dylib-all-run "$work/dylib-client"
+ cp "$work/libanswer.original" "$work/libanswer.dylib"
+ fi
+ else
+ skip_test macho-dylib-runtime "native SDK/clang unavailable"
+ fi
+elif [ "$host_os" = Linux ]; then
+ # Native hosted output covers a runnable dynamic PIE.
+ printf 'int main(void) { return 0; }\n' > "$work/linux-native.c"
+ run_ok linux-dynamic-build "$KIT" cc -g "$work/linux-native.c" \
+ -o "$work/linux-dynamic"
+ if [ -f "$work/linux-dynamic" ]; then
+ strip_shape linux-dynamic "$work/linux-dynamic"
+ run_ok linux-dynamic-run-before "$work/linux-dynamic"
+ run_ok linux-dynamic-debug-run "$work/linux-dynamic.debug"
+ run_ok linux-dynamic-all-run "$work/linux-dynamic.all"
+ fi
+
+ # Execute a genuinely static freestanding artifact on supported native
+ # Linux architectures; the cross-format structural cases above remain
+ # unconditional.
+ case "$host_arch" in
+ x86_64|amd64)
+ native_static_target=x86_64-linux-gnu
+ cp "$work/x64-start.c" "$work/native-static.c"
+ ;;
+ aarch64|arm64)
+ native_static_target=aarch64-linux-gnu
+ cat > "$work/native-static.c" <<'EOF'
+__attribute__((noreturn)) void _start(void) {
+ register long nr __asm__("x8") = 94;
+ register long code __asm__("x0") = 42;
+ __asm__ volatile("svc #0" : : "r"(nr), "r"(code) : "memory");
+ __builtin_unreachable();
+}
+EOF
+ ;;
+ riscv64)
+ native_static_target=riscv64-linux-gnu
+ cat > "$work/native-static.c" <<'EOF'
+__attribute__((noreturn)) void _start(void) {
+ register long nr __asm__("a7") = 94;
+ register long code __asm__("a0") = 42;
+ __asm__ volatile("ecall" : : "r"(nr), "r"(code) : "memory");
+ __builtin_unreachable();
+}
+EOF
+ ;;
+ *) native_static_target= ;;
+ esac
+ if [ -n "$native_static_target" ]; then
+ run_ok linux-static-build "$KIT" cc -target "$native_static_target" -g \
+ -ffreestanding -fno-stack-protector -fno-PIC -fno-pie -nostdlib \
+ -static -Wl,-e,_start "$work/native-static.c" \
+ -o "$work/linux-static"
+ if [ -f "$work/linux-static" ]; then
+ strip_shape linux-static "$work/linux-static"
+ "$work/linux-static" >/dev/null 2>&1; static_before_rc=$?
+ "$work/linux-static.debug" >/dev/null 2>&1; static_debug_rc=$?
+ "$work/linux-static.all" >/dev/null 2>&1; static_all_rc=$?
+ if [ "$static_before_rc" -eq 42 ]; then ok linux-static-run-before;
+ else not_ok linux-static-run-before; fi
+ if [ "$static_debug_rc" -eq 42 ]; then ok linux-static-debug-run;
+ else not_ok linux-static-debug-run; fi
+ if [ "$static_all_rc" -eq 42 ]; then ok linux-static-all-run;
+ else not_ok linux-static-all-run; fi
+ fi
+ else
+ skip_test linux-static-runtime "unsupported native architecture $host_arch"
+ fi
+fi
+
+kit_summary strip-linked
+kit_exit