From 9c27a1ebf56f605541ae524fd2126858bfe0a387 Mon Sep 17 00:00:00 2001 From: sechmachine <97589681+sechmachine727@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:27:58 +0700 Subject: [PATCH] fix(core): harden framework packaging --- core/scripts/build-xcframework.sh | 214 +++++++++++++++++++++++++----- core/tests/packaging.sh | 155 +++++++++++++++++++--- 2 files changed, 312 insertions(+), 57 deletions(-) diff --git a/core/scripts/build-xcframework.sh b/core/scripts/build-xcframework.sh index 1fe0cc7..d0ff133 100755 --- a/core/scripts/build-xcframework.sh +++ b/core/scripts/build-xcframework.sh @@ -1,5 +1,7 @@ #!/bin/sh set -eu +PATH=/usr/bin:/bin:/usr/sbin:/sbin +export PATH usage() { echo "usage: $0 --output ABSOLUTE_DIR --target-dir ABSOLUTE_DIR" >&2 @@ -26,10 +28,41 @@ done test -n "$output" || usage test -n "$target_dir" || usage -case "$output:$target_dir" in - /*:/*) ;; - *) usage ;; -esac +ROOT=$(CDPATH= cd -P -- "$(dirname "$0")/../.." && pwd -P) +CORE="$ROOT/core" + +resolve_new_path() { + requested=$1 + label=$2 + case "$requested" in + /*) ;; + *) usage ;; + esac + if test -e "$requested" || test -L "$requested"; then + echo "$label must not exist: $requested" >&2 + exit 2 + fi + parent=$(dirname -- "$requested") + name=$(basename -- "$requested") + case "$name" in + ''|.|..) usage ;; + esac + physical_parent=$(CDPATH= cd -P -- "$parent" 2>/dev/null && pwd -P) || { + echo "$label parent must already exist: $parent" >&2 + exit 2 + } + candidate="$physical_parent/$name" + case "$candidate" in + "$ROOT"|"$ROOT"/*) + echo "$label must be outside the repository: $requested" >&2 + exit 2 + ;; + esac + printf '%s\n' "$candidate" +} + +output=$(resolve_new_path "$output" output) +target_dir=$(resolve_new_path "$target_dir" "target directory") case "$output/:$target_dir/" in "$target_dir/"*:*|*:"$output/"*) echo "output and target directory must be separate" >&2 @@ -37,55 +70,164 @@ case "$output/:$target_dir/" in ;; esac -ROOT=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd) -case "$output" in - "$ROOT"|"$ROOT"/*) - echo "output must be outside the repository" >&2 +mkdir "$output" +if ! mkdir "$target_dir"; then + rmdir "$output" + exit 2 +fi +output=$(CDPATH= cd -P -- "$output" && pwd -P) +target_dir=$(CDPATH= cd -P -- "$target_dir" && pwd -P) +for reserved_path in "$output" "$target_dir"; do + case "$reserved_path" in + "$ROOT"|"$ROOT"/*) + echo "reserved packaging path resolved inside the repository: $reserved_path" >&2 + exit 2 + ;; + esac +done +case "$output/:$target_dir/" in + "$target_dir/"*:*|*:"$output/"*) + echo "reserved output and target directory must be separate" >&2 exit 2 ;; esac -case "$target_dir" in - "$ROOT"|"$ROOT"/*) - echo "target directory must be outside the repository" >&2 - exit 2 - ;; -esac -test ! -e "$output" || { - echo "output already exists: $output" >&2 - exit 2 -} -test ! -e "$target_dir" || { - echo "target directory already exists: $target_dir" >&2 - exit 2 -} -test "$(uname -s)" = Darwin || { + +test "$(/usr/bin/uname -s)" = Darwin || { echo "XCFramework packaging requires macOS" >&2 exit 2 } +test "$(/usr/bin/uname -m)" = arm64 || { + echo "XCFramework packaging requires an arm64 host" >&2 + exit 2 +} -source_epoch=$(git -C "$ROOT" show -s --format=%ct HEAD) -export CARGO_INCREMENTAL=0 -export CARGO_TARGET_DIR="$target_dir" -export MACOSX_DEPLOYMENT_TARGET=14.0 -export RUSTFLAGS="--remap-path-prefix=$ROOT=." -export SOURCE_DATE_EPOCH="$source_epoch" -export ZERO_AR_DATE=1 +user_record=$(/usr/bin/dscacheutil -q user -a name "$(/usr/bin/id -un)") +trusted_home=$(printf '%s\n' "$user_record" | awk '$1 == "dir:" { print $2; exit }') +rustup="$trusted_home/.cargo/bin/rustup" +test -x "$rustup" || { + echo "rustup not found at trusted user path" >&2 + exit 2 +} +cargo_bin=$(/usr/bin/env -i \ + HOME="$trusted_home" \ + PATH="$PATH" \ + RUSTUP_HOME="$trusted_home/.rustup" \ + "$rustup" which --toolchain 1.97.1 cargo) +rustc_bin=$(/usr/bin/env -i \ + HOME="$trusted_home" \ + PATH="$PATH" \ + RUSTUP_HOME="$trusted_home/.rustup" \ + "$rustup" which --toolchain 1.97.1 rustc) +rustc_version=$(/usr/bin/env -i HOME="$trusted_home" PATH="$PATH" "$rustc_bin" --version) +cargo_version=$(/usr/bin/env -i HOME="$trusted_home" PATH="$PATH" "$cargo_bin" --version) +test "$rustc_version" = 'rustc 1.97.1 (8bab26f4f 2026-07-14)' || { + echo "unexpected rustc identity: $rustc_version" >&2 + exit 2 +} +test "$cargo_version" = 'cargo 1.97.1 (c980f4866 2026-06-30)' || { + echo "unexpected cargo identity: $cargo_version" >&2 + exit 2 +} +xcodebuild=$(/usr/bin/env -i PATH="$PATH" /usr/bin/xcrun --find xcodebuild) +xcode_version=$(/usr/bin/env -i HOME="$trusted_home" PATH="$PATH" "$xcodebuild" -version) +test "$xcode_version" = 'Xcode 26.6 +Build version 17F113' || { + echo "unexpected Xcode identity: $xcode_version" >&2 + exit 2 +} +clang=$(/usr/bin/env -i PATH="$PATH" /usr/bin/xcrun --find clang) +ar=$(/usr/bin/env -i PATH="$PATH" /usr/bin/xcrun --find ar) +clang_identity=$(/usr/bin/env -i HOME="$trusted_home" PATH="$PATH" "$clang" --version) +clang_version=$(printf '%s\n' "$clang_identity" | sed -n '1p') +sdk=$(/usr/bin/env -i PATH="$PATH" /usr/bin/xcrun --sdk macosx --show-sdk-path) -cargo build \ - --manifest-path "$ROOT/core/Cargo.toml" \ - --target aarch64-apple-darwin \ - --release \ - --locked +config_dir=$CORE +while :; do + if test -e "$config_dir/.cargo/config" || test -e "$config_dir/.cargo/config.toml"; then + echo "Cargo config is not permitted in the packaging path: $config_dir/.cargo" >&2 + exit 2 + fi + test "$config_dir" = / && break + config_dir=$(dirname "$config_dir") +done + +source_commit=$(/usr/bin/env -i HOME="$trusted_home" PATH="$PATH" \ + /usr/bin/git -C "$ROOT" rev-parse HEAD) +source_epoch=$(/usr/bin/env -i HOME="$trusted_home" PATH="$PATH" \ + /usr/bin/git -C "$ROOT" show -s --format=%ct HEAD) +rustflags="--remap-path-prefix=$ROOT=. --remap-path-prefix=$target_dir=/cargo-target" +cflags="-fdebug-prefix-map=$ROOT=. -ffile-prefix-map=$ROOT=. -fdebug-prefix-map=$target_dir=/cargo-target -ffile-prefix-map=$target_dir=/cargo-target" +cargo_home="$target_dir/cargo-home" +build_tmp="$target_dir/tmp" +mkdir "$cargo_home" "$build_tmp" +for cache in registry git; do + if test -e "$trusted_home/.cargo/$cache"; then + ln -s "$trusted_home/.cargo/$cache" "$cargo_home/$cache" + fi +done +( + cd "$CORE" + /usr/bin/env -i \ + AR="$ar" \ + AR_aarch64_apple_darwin="$ar" \ + CARGO_HOME="$cargo_home" \ + CARGO_INCREMENTAL=0 \ + CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1 \ + CARGO_PROFILE_RELEASE_INCREMENTAL=false \ + CARGO_PROFILE_RELEASE_LTO=fat \ + CARGO_PROFILE_RELEASE_PANIC=abort \ + CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER="$clang" \ + CARGO_TARGET_DIR="$target_dir" \ + CC="$clang" \ + CC_aarch64_apple_darwin="$clang" \ + CFLAGS="$cflags" \ + CFLAGS_aarch64_apple_darwin="$cflags" \ + HOME="$trusted_home" \ + MACOSX_DEPLOYMENT_TARGET=14.0 \ + PATH="$PATH" \ + RUSTC="$rustc_bin" \ + RUSTFLAGS="$rustflags" \ + SDKROOT="$sdk" \ + SOURCE_DATE_EPOCH="$source_epoch" \ + TMPDIR="$build_tmp" \ + ZERO_AR_DATE=1 \ + "$cargo_bin" build \ + --target aarch64-apple-darwin \ + --release \ + --frozen +) headers="$target_dir/xcframework-headers" mkdir -p "$headers" "$output" cp "$ROOT/core/include/versevdi_core.h" "$headers/" cp "$ROOT/core/include/module.modulemap" "$headers/" -xcodebuild -create-xcframework \ +/usr/bin/env -i \ + HOME="$trusted_home" \ + PATH="$PATH" \ + TMPDIR="$build_tmp" \ + "$xcodebuild" -create-xcframework \ -library "$target_dir/aarch64-apple-darwin/release/libversevdi_core.a" \ -headers "$headers" \ -output "$output/VerseVDICore.xcframework" timestamp=$(date -r "$source_epoch" +%Y%m%d%H%M.%S) find "$output/VerseVDICore.xcframework" -exec touch -h -t "$timestamp" {} + + +{ + printf 'rustc=%s\n' "$rustc_version" + printf 'cargo=%s\n' "$cargo_version" + printf 'cargo_path=%s\n' "$cargo_bin" + printf 'rustc_path=%s\n' "$rustc_bin" + printf 'xcode=%s\n' "$xcode_version" + printf 'clang=%s\n' "$clang_version" + printf 'sdk=%s\n' "$sdk" + printf 'cargo_config=isolated-home-and-no-project-config\n' + printf 'target=aarch64-apple-darwin\n' + printf 'deployment_target=14.0\n' + printf 'source_commit=%s\n' "$source_commit" + printf 'source_epoch=%s\n' "$source_epoch" + printf 'rustflags=--remap-path-prefix==. --remap-path-prefix==/cargo-target\n' + printf 'cflags=-fdebug-prefix-map/-ffile-prefix-map for and \n' +} >"$output/build-environment.txt" +touch -t "$timestamp" "$output/build-environment.txt" "$output" diff --git a/core/tests/packaging.sh b/core/tests/packaging.sh index 8d2c500..2f23390 100755 --- a/core/tests/packaging.sh +++ b/core/tests/packaging.sh @@ -18,24 +18,92 @@ test -x "$BUILDER" WORK=$(mktemp -d "${TMPDIR:-/tmp}/versevdi-core-package.XXXXXX") trap 'rm -rf "$WORK"' EXIT HUP INT TERM +expect_path_rejected() { + error=$1 + shift + if "$BUILDER" "$@" >"$WORK/path.stdout" 2>"$WORK/path.stderr"; then + echo "unsafe packaging path was accepted" >&2 + exit 1 + fi + grep -F "$error" "$WORK/path.stderr" >/dev/null +} + +mkdir "$WORK/parents" "$WORK/physical-parent" +mkdir "$WORK/existing-output" +expect_path_rejected "must not exist" \ + --output "$WORK/existing-output" --target-dir "$WORK/parents/existing-target" +ln -s "$WORK/physical-parent" "$WORK/parent-alias" +ln -s "$WORK/physical-parent" "$WORK/second-parent-alias" +ln -s "$WORK/missing" "$WORK/dangling-output" +expect_path_rejected "must not exist" \ + --output "$WORK/dangling-output" --target-dir "$WORK/parents/dangling-target" +expect_path_rejected "must be separate" \ + --output "$WORK/parents/same" --target-dir "$WORK/parents/same" +expect_path_rejected "must be separate" \ + --output "$WORK/parent-alias/aliased-same" \ + --target-dir "$WORK/second-parent-alias/aliased-same" +ln -s "$ROOT" "$WORK/repository-alias" +expect_path_rejected "outside the repository" \ + --output "$WORK/repository-alias/core/forbidden-output" \ + --target-dir "$WORK/parents/repository-alias-target" + "$BUILDER" --output "$WORK/one" --target-dir "$WORK/target-one" -"$BUILDER" --output "$WORK/two" --target-dir "$WORK/target-two" +mkdir "$WORK/hostile-bin" +mkdir -p "$WORK/hostile-cargo-home" "$WORK/hostile-home/.cargo" +ln -s /usr/bin/false "$WORK/hostile-bin/cargo" +ln -s /usr/bin/false "$WORK/hostile-bin/rustup" +ln -s /usr/bin/false "$WORK/hostile-bin/xcodebuild" +cat >"$WORK/hostile-cargo-home/config.toml" <<'EOF' +[build] +rustc-wrapper = "/usr/bin/false" +EOF +cat >"$WORK/hostile-home/.cargo/config.toml" <<'EOF' +[build] +rustc = "/usr/bin/false" +EOF +env \ + AR=/usr/bin/false \ + CARGO_BUILD_RUSTC=/usr/bin/false \ + CARGO_BUILD_RUSTC_WRAPPER=/usr/bin/false \ + CARGO_ENCODED_RUSTFLAGS=--cfghostile \ + CARGO_HOME="$WORK/hostile-cargo-home" \ + CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER=/usr/bin/false \ + CC=/usr/bin/false \ + DEVELOPER_DIR="$WORK/hostile-xcode" \ + HOME="$WORK/hostile-home" \ + PATH="$WORK/hostile-bin:/usr/bin:/bin" \ + RUSTC=/usr/bin/false \ + RUSTC_WRAPPER=/usr/bin/false \ + RUSTFLAGS=--cfg=hostile \ + RUSTUP_HOME="$WORK/hostile-rustup-home" \ + RUSTUP_TOOLCHAIN=bogus \ + "$BUILDER" \ + --output "$WORK/parent-alias/two" \ + --target-dir "$WORK/parents/target-two" framework_one="$WORK/one/VerseVDICore.xcframework" -framework_two="$WORK/two/VerseVDICore.xcframework" +framework_two="$WORK/physical-parent/two/VerseVDICore.xcframework" library_one=$(find "$framework_one" -type f -name libversevdi_core.a -print) library_two=$(find "$framework_two" -type f -name libversevdi_core.a -print) test "$(printf '%s\n' "$library_one" | grep -c .)" -eq 1 test "$(printf '%s\n' "$library_two" | grep -c .)" -eq 1 -test "$(/usr/libexec/PlistBuddy -c 'Print :AvailableLibraries:0:SupportedPlatform' "$framework_one/Info.plist")" = macos -test "$(/usr/libexec/PlistBuddy -c 'Print :AvailableLibraries:0:SupportedArchitectures:0' "$framework_one/Info.plist")" = arm64 -test "$(/usr/libexec/PlistBuddy -c 'Print :AvailableLibraries' "$framework_one/Info.plist" | grep -c 'Dict {')" -eq 1 -test "$(xcrun lipo -archs "$library_one")" = arm64 -file "$library_one" | grep -F 'current ar archive' >/dev/null +platform=$(/usr/libexec/PlistBuddy -c 'Print :AvailableLibraries:0:SupportedPlatform' "$framework_one/Info.plist") +architecture=$(/usr/libexec/PlistBuddy -c 'Print :AvailableLibraries:0:SupportedArchitectures:0' "$framework_one/Info.plist") +available_libraries=$(/usr/libexec/PlistBuddy -c 'Print :AvailableLibraries' "$framework_one/Info.plist") +test "$platform" = macos +test "$architecture" = arm64 +test "$(printf '%s\n' "$available_libraries" | grep -c 'Dict {')" -eq 1 +library_archs=$(xcrun lipo -archs "$library_one") +library_identity=$(file "$library_one") +test "$library_archs" = arm64 +printf '%s\n' "$library_identity" | grep -F 'current ar archive' >/dev/null consumer="$WORK/consumer" -xcrun clang \ +link_consumer() { + output=$1 + library=$2 + xcrun clang \ -arch arm64 \ -mmacosx-version-min=14.0 \ -std=c11 \ @@ -44,19 +112,37 @@ xcrun clang \ -fmodules-cache-path="$WORK/module-cache" \ -I"$(dirname "$library_one")/Headers" \ "$ROOT/core/tests/ffi/abi_smoke.c" \ - "$library_one" \ + -Wl,-force_load,"$library" \ -framework Security \ -framework SystemConfiguration \ -framework CoreFoundation \ -lresolv \ - -o "$consumer" -test "$(xcrun lipo -archs "$consumer")" = arm64 -file "$consumer" | grep -F 'Mach-O 64-bit executable arm64' >/dev/null + -o "$output" +} + +link_consumer "$consumer" "$library_one" +consumer_archs=$(xcrun lipo -archs "$consumer") +consumer_identity=$(file "$consumer") +test "$consumer_archs" = arm64 +printf '%s\n' "$consumer_identity" | grep -F 'Mach-O 64-bit executable arm64' >/dev/null symbols=$(xcrun nm -gjU "$consumer") actual_exports=$(printf '%s\n' "$symbols" | grep '^_verse_core_' | LC_ALL=C sort -u) test "$actual_exports" = "$EXPECTED_EXPORTS" +cat >"$WORK/unexpected.c" <<'EOF' +void verse_core_unexpected_v1(void) {} +EOF +xcrun clang -arch arm64 -mmacosx-version-min=14.0 -c "$WORK/unexpected.c" -o "$WORK/unexpected.o" +cp "$library_one" "$WORK/libunexpected.a" +ZERO_AR_DATE=1 xcrun ar -r "$WORK/libunexpected.a" "$WORK/unexpected.o" +link_consumer "$WORK/unexpected-consumer" "$WORK/libunexpected.a" +unexpected_symbols=$(xcrun nm -gjU "$WORK/unexpected-consumer") +if test "$(printf '%s\n' "$unexpected_symbols" | grep '^_verse_core_' | LC_ALL=C sort -u)" = "$EXPECTED_EXPORTS"; then + echo "force-loaded export inspection missed an unreferenced ABI symbol" >&2 + exit 1 +fi + dependencies=$(xcrun otool -L "$consumer") unexpected_dependencies=$(printf '%s\n' "$dependencies" | tail -n +2 | awk '{print $1}' | grep -Ev '^(/usr/lib/(libSystem\.B|libresolv\.9)\.dylib|/System/Library/Frameworks/(CoreFoundation|Security|SystemConfiguration)\.framework/Versions/A/[^/]+)$' || true) test -z "$unexpected_dependencies" @@ -76,31 +162,58 @@ done canonical_tree() { ( cd "$1" - find . -type f -print | LC_ALL=C sort | while IFS= read -r path; do - digest=$(shasum -a 256 "$path" | awk '{print $1}') - printf '%s %s\n' "$digest" "$path" - done + find . -print >"$WORK/tree.entries" + LC_ALL=C sort "$WORK/tree.entries" >"$WORK/tree.sorted" + while IFS= read -r path; do + metadata=$(stat -f '%HT|%Sp' "$path") + case "$metadata" in + 'Regular File|'*) digest=$(sha256_file "$path") ;; + 'Symbolic Link|'*) digest=$(readlink "$path") ;; + *) digest=- ;; + esac + printf '%s|%s|%s\n' "$metadata" "$digest" "$path" + done <"$WORK/tree.sorted" ) } +sha256_file() { + checksum=$(shasum -a 256 "$1") + set -- $checksum + printf '%s\n' "$1" +} + canonical_tree "$framework_one" >"$WORK/one.tree" canonical_tree "$framework_two" >"$WORK/two.tree" cmp "$WORK/one.tree" "$WORK/two.tree" -test "$(shasum -a 256 "$library_one" | awk '{print $1}')" = "$(shasum -a 256 "$library_two" | awk '{print $1}')" +test "$(sha256_file "$library_one")" = "$(sha256_file "$library_two")" source_epoch=$(git -C "$ROOT" show -s --format=%ct HEAD) -find "$framework_one" -exec stat -f '%m' {} \; | while IFS= read -r epoch; do +find "$WORK/one" "$WORK/physical-parent/two" -exec stat -f '%m' {} \; >"$WORK/mtimes" +while IFS= read -r epoch; do test "$epoch" = "$source_epoch" -done +done <"$WORK/mtimes" + +expected_rustc='rustc 1.97.1 (8bab26f4f 2026-07-14)' +expected_cargo='cargo 1.97.1 (c980f4866 2026-06-30)' +expected_xcode='Xcode 26.6 +Build version 17F113' +grep -Fx "rustc=$expected_rustc" "$WORK/one/build-environment.txt" >/dev/null +grep -Fx "cargo=$expected_cargo" "$WORK/one/build-environment.txt" >/dev/null +test "$(sed -n 's/^xcode=//p; /^Build version /p' "$WORK/one/build-environment.txt")" = "$expected_xcode" +cmp "$WORK/one/build-environment.txt" "$WORK/physical-parent/two/build-environment.txt" strings "$library_one" >"$WORK/library.strings" if grep -F "$ROOT" "$WORK/library.strings" >/dev/null; then echo "repository path leaked into static archive" >&2 exit 1 fi +if grep -F "$WORK/target-one" "$WORK/library.strings" >/dev/null; then + echo "target directory path leaked into static archive" >&2 + exit 1 +fi archive_members=$(xcrun ar -tv "$library_one") printf '%s\n' "$archive_members" | awk '$7 != "1970" { exit 1 }' -printf 'framework_sha256=%s\n' "$(shasum -a 256 "$WORK/one.tree" | awk '{print $1}')" -printf 'library_sha256=%s\n' "$(shasum -a 256 "$library_one" | awk '{print $1}')" +printf 'framework_sha256=%s\n' "$(sha256_file "$WORK/one.tree")" +printf 'library_sha256=%s\n' "$(sha256_file "$library_one")"