diff --git a/Makefile b/Makefile index 5f9afa37..d1415cfe 100644 --- a/Makefile +++ b/Makefile @@ -243,131 +243,136 @@ $(BUILD_DIR)/test-hello: tests/hello.S tests/simple.ld | $(BUILD_DIR) # Pattern rule: cross-compile tests/*.c to static aarch64-linux binaries # -D_GNU_SOURCE exposes pipe2/dup3/O_DIRECT/etc. on glibc (musl exposes them by default) +# -MMD tracks the shared test headers, so editing one rebuilds every guest +# binary that includes it. Compiling and linking in one step, the driver +# injects -MQ with the -o argument unless -MT or -MQ is given, so the rule +# target is the binary and the .d lands where mk/common.mk -includes it. +CROSS_TEST_CFLAGS = -D_GNU_SOURCE -static -O2 -MMD -MP $(BUILD_DIR)/%: tests/%.c | $(BUILD_DIR) @echo " CROSS $<" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< # test-pthread needs -lpthread $(BUILD_DIR)/test-pthread: tests/test-pthread.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread # test-sysroot-name-soak churns from worker threads plus forked children $(BUILD_DIR)/test-sysroot-name-soak: tests/test-sysroot-name-soak.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread # test-process-lifecycle creates a worker to verify that process PIDs and # thread TIDs share one namespace-wide allocator across fork children. $(BUILD_DIR)/test-process-lifecycle: tests/test-process-lifecycle.c src/utils.h | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -Isrc -o $@ $< -lpthread + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -Isrc -o $@ $< -lpthread # test-sigsuspend parks two threads on one process-directed signal. $(BUILD_DIR)/test-sigsuspend: tests/test-sigsuspend.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -Itests -o $@ $< -lpthread + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -Itests -o $@ $< -lpthread # test-thread-churn creates >64 threads to force thread-table slot reuse. $(BUILD_DIR)/test-thread-churn: tests/test-thread-churn.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread # test-cntvct-thread verifies cloned vCPUs inherit EL0 timer access. $(BUILD_DIR)/test-cntvct-thread: tests/test-cntvct-thread.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread # test-poll uses pthread_kill to verify blocked read signal delivery. $(BUILD_DIR)/test-poll: tests/test-poll.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread # test-tgkill-directed verifies thread-directed (pthread_kill/tgkill) routing. $(BUILD_DIR)/test-tgkill-directed: tests/test-tgkill-directed.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread # test-sigtimedwait uses a sender pthread to verify thread-directed waits. $(BUILD_DIR)/test-sigtimedwait: tests/test-sigtimedwait.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread # test-osync-requeue drives a raw FUTEX_REQUEUE against a plain-FUTEX_WAIT # waiter (musl unlock_requeue pattern) to guard the os_sync wake-at-source # degradation; needs -lpthread. $(BUILD_DIR)/test-osync-requeue: tests/test-osync-requeue.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread # test-scm-creds blocks accept in a pthread while the listener option changes. $(BUILD_DIR)/test-scm-creds: tests/test-scm-creds.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread # test-fault-signal-mt spawns pthreads that each take recoverable SIGSEGVs to # stress synchronous-fault delivery routing in a multi-threaded guest. $(BUILD_DIR)/test-fault-signal-mt: tests/test-fault-signal-mt.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread # test-exit-group-worker has a non-main pthread issue exit_group while # spinner threads hammer memory, guarding the join-before-teardown order. $(BUILD_DIR)/test-exit-group-worker: tests/test-exit-group-worker.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread # test-shim-cred-race spawns a pthread reader while the main thread # toggles setresuid; the reader spins on the identity fast path. $(BUILD_DIR)/test-shim-cred-race: tests/test-shim-cred-race.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread # test-mprotect-mt stresses multi-vCPU mprotect under concurrent reader # threads to surface stale-TLB regressions. $(BUILD_DIR)/test-mprotect-mt: tests/test-mprotect-mt.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread # test-shim-urandom-smp spawns N pthreads racing on a shared FD_URANDOM # slot to exercise the shim's LDXR/STXR head-advance under contention. $(BUILD_DIR)/test-shim-urandom-smp: tests/test-shim-urandom-smp.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread # test-shim-urandom-toctou races mprotect(PROT_NONE) against urandom # reads to exercise the EL1 data abort recovery path. Needs pthreads. $(BUILD_DIR)/test-shim-urandom-toctou: tests/test-shim-urandom-toctou.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread # test-fuse-basic runs a guest daemon thread and consumer in one process $(BUILD_DIR)/test-fuse-basic: tests/test-fuse-basic.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread # test-sched-policy spawns a pthread to verify per-thread TID lookup $(BUILD_DIR)/test-sched-policy: tests/test-sched-policy.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread # test-signalfd-hardening needs -lpthread for the worker-thread tid # regression case in test_rt_sigqueueinfo_rejects_thread_tid. $(BUILD_DIR)/test-signalfd-hardening: tests/test-signalfd-hardening.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread # test-futex-waitv needs -lpthread for the host wake-thread used to unblock # the main thread's futex_waitv. $(BUILD_DIR)/test-futex-waitv: tests/test-futex-waitv.c | $(BUILD_DIR) @echo " CROSS $< (with -lpthread)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -o $@ $< -lpthread + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -o $@ $< -lpthread # test-fork-lowbase must be a non-PIE ET_EXEC linked below ELF_DEFAULT_BASE so # nested forks exercise elf_load_min preservation across fork IPC. $(BUILD_DIR)/test-fork-lowbase: tests/test-fork-lowbase.c | $(BUILD_DIR) @echo " CROSS $< (low-base ET_EXEC)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -no-pie \ + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -no-pie \ -Wl,-Ttext-segment=0x200000 -o $@ $< # test-lowbase-mem variants must be non-PIE ET_EXEC binaries linked below @@ -375,12 +380,12 @@ $(BUILD_DIR)/test-fork-lowbase: tests/test-fork-lowbase.c | $(BUILD_DIR) # window at two offsets. $(BUILD_DIR)/test-lowbase-mem-200000: tests/test-lowbase-mem.c | $(BUILD_DIR) @echo " CROSS $< (low-base ET_EXEC @0x200000)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -no-pie \ + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -no-pie \ -Wl,-Ttext-segment=0x200000 -o $@ $< $(BUILD_DIR)/test-lowbase-mem-300000: tests/test-lowbase-mem.c | $(BUILD_DIR) @echo " CROSS $< (low-base ET_EXEC @0x300000)" - $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -no-pie \ + $(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -no-pie \ -Wl,-Ttext-segment=0x300000 -o $@ $< # bench-hot-guard-glibc is the dynamic-glibc twin of bench-hot-guard. diff --git a/docs/internals.md b/docs/internals.md index b88f9b76..e7259b1c 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -851,10 +851,7 @@ drops out of the resolution entirely, which is the point. A climbed path is the one case where the recheck's answer is the resolution itself rather than a discarded verdict, so it also honors the caller's create intent: missing sysroot parents are materialized exactly as they would be for -the absolute spelling. The case-index sidecar declines a climbing relative -create for the same reason (`sidecar_walk_parent_at()` in -`src/syscall/sidecar.c`): its own walk from the descriptor has no clamp, and -the clamped absolute spelling already bypasses sidecar lookup. +the absolute spelling. ### Why The No-Symlinks Precheck Has No Component Budget diff --git a/mk/tests.mk b/mk/tests.mk index 4be85651..dcf96628 100644 --- a/mk/tests.mk +++ b/mk/tests.mk @@ -90,147 +90,124 @@ check-tsan: clean # the remaining compat sections (rseq, /proc, sockets) stay on the release lane. SANITIZER_SECTIONS := Threading|Stress|Signal.*thread|Fork edge|CoW fork|Guard page|mremap|MAP_SHARED|madvise|futex|FD table race|Multithreaded|SysV shared|membarrier|I/O subsystem +# One banner-plus-run pair, for a host unit binary run directly and for a +# lane reached through its own make target. Canned recipes (make manual 5.8) +# rather than 59 hand-kept printf/invoke pairs, so a lane list edit cannot +# leave a banner naming the wrong lane. Banner text arrives through $(call), +# so a comma in it silently splits into $(3) and truncates the banner. +define run-host-unit +@printf "\n$(BLUE)━━━ $(2) ━━━$(RESET)\n" +@$(BUILD_DIR)/$(1) +endef + +# The lane is a single '+'-marked recipe line. The '+' is needed because make +# decides what still runs under -n by looking for a literal $(MAKE) in the +# unexpanded recipe line, which a canned recipe hides (make manual 9.3); +# without it, "make -n check" prints the sub-make instead of expanding the +# lane it would run. One line rather than a banner-then-invoke pair because +# once a '+' line appears in an expansion, every later line of that same +# expansion also runs for real under -n, so split banners would behave +# unlike each other, and a non-lane line appended after a run-lane call +# would execute during a dry build. +define run-lane ++@printf "\n$(BLUE)━━━ $(2) ━━━$(RESET)\n"; \ +$(MAKE) --no-print-directory $(1) +endef + +# Open a lane recipe with a scratch directory in $$tmpdir, removed however +# the recipe exits. Recipes that clean up more than the scratch directory +# replace the trap once the extra paths are named. +define SYSROOT_SCRATCH +set -e; \ +tmpdir=$$(mktemp -d); \ +trap 'rm -rf "$$tmpdir"' EXIT +endef + +# Count the absock namespace dirs before the lane runs, then require the +# count not to have grown: the exit sweeps must leave nothing behind, and a +# grown count is a leaked /tmp/elfuse-absock- no later process owns. +define ABSOCK_LEAK_BEFORE +before=$$(ls -d /tmp/elfuse-absock-* 2>/dev/null | wc -l | tr -d ' ') +endef + +define ASSERT_NO_ABSOCK_LEAK +after=$$(ls -d /tmp/elfuse-absock-* 2>/dev/null | wc -l | tr -d ' '); \ +if [ "$$after" -gt "$$before" ]; then \ + printf "$(RED)FAIL$(RESET) absock namespace dir leaked ($$before -> $$after)\n"; \ + exit 1; \ +fi +endef + +# The host unit binaries both suites run directly, as prerequisites. +CHECK_HOST_UNIT_BINS := $(addprefix $(BUILD_DIR)/, \ + test-tlbi-encoder-host test-fork-ipc-protocol-host \ + test-vcpu-run-hooks-host test-identity-override-host \ + test-teardown-live-vcpu-host test-casefold-host \ + test-casefold-walk-host test-absock-names-host) + +# Lanes shared by check and check-sanitizer, in execution order: the host +# unit binaries, then the name-contract lanes cheap enough for a sanitizer +# build. check-sanitizer's coverage is exactly this prefix, kept as one +# explicit sequence rather than "whatever check happens to run". +define CHECK_SHARED_LANES +$(call run-host-unit,test-tlbi-encoder-host,TLBI RVAE1IS encoder unit test) +$(call run-host-unit,test-fork-ipc-protocol-host,fork IPC protocol identity unit test) +$(call run-host-unit,test-vcpu-run-hooks-host,vCPU run-loop hook API unit test) +$(call run-host-unit,test-identity-override-host,identity override unit test) +$(call run-host-unit,test-teardown-live-vcpu-host,teardown live-worker accounting unit test) +$(call run-host-unit,test-casefold-host,filename codec unit test) +$(call run-host-unit,test-casefold-walk-host,case-exact path resolution unit test) +$(call run-host-unit,test-absock-names-host,absock derived-name unit test) +$(call run-lane,test-sysroot-name-unique,one on-disk name per guest name) +$(call run-lane,test-sysroot-name-relative,relative and dirfd-relative names) +$(call run-lane,test-sysroot-name-i18n,non-ASCII guest filenames) +$(call run-lane,test-sysroot-name-length,guest filenames at full length) +$(call run-lane,test-sysroot-name-staged,host-staged escape-shaped names) +endef + ## Run the representative internal-implementation subset (for sanitizer builds) -check-sanitizer: $(ELFUSE_BIN) $(TEST_DEPS) \ - $(BUILD_DIR)/test-tlbi-encoder-host \ - $(BUILD_DIR)/test-fork-ipc-protocol-host \ - $(BUILD_DIR)/test-vcpu-run-hooks-host \ - $(BUILD_DIR)/test-identity-override-host \ - $(BUILD_DIR)/test-teardown-live-vcpu-host \ - $(BUILD_DIR)/test-casefold-host \ - $(BUILD_DIR)/test-casefold-walk-host \ - $(BUILD_DIR)/test-absock-names-host +check-sanitizer: $(ELFUSE_BIN) $(TEST_DEPS) $(CHECK_HOST_UNIT_BINS) @bash tests/driver.sh -e $(ELFUSE_BIN) -d $(TEST_DIR) -v -s '$(SANITIZER_SECTIONS)' - @printf "\n$(BLUE)━━━ TLBI RVAE1IS encoder unit test ━━━$(RESET)\n" - @$(BUILD_DIR)/test-tlbi-encoder-host - @printf "\n$(BLUE)━━━ fork IPC protocol identity unit test ━━━$(RESET)\n" - @$(BUILD_DIR)/test-fork-ipc-protocol-host - @printf "\n$(BLUE)━━━ vCPU run-loop hook API unit test ━━━$(RESET)\n" - @$(BUILD_DIR)/test-vcpu-run-hooks-host - @printf "\n$(BLUE)━━━ identity override unit test ━━━$(RESET)\n" - @$(BUILD_DIR)/test-identity-override-host - @printf "\n$(BLUE)━━━ teardown live-worker accounting unit test ━━━$(RESET)\n" - @$(BUILD_DIR)/test-teardown-live-vcpu-host - @printf "\n$(BLUE)━━━ filename codec unit test ━━━$(RESET)\n" - @$(BUILD_DIR)/test-casefold-host - @printf "\n$(BLUE)━━━ case-exact path resolution unit test ━━━$(RESET)\n" - @$(BUILD_DIR)/test-casefold-walk-host - @printf "\n$(BLUE)━━━ absock derived-name unit test ━━━$(RESET)\n" - @$(BUILD_DIR)/test-absock-names-host - @printf "\n$(BLUE)━━━ one on-disk name per guest name ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-name-unique - @printf "\n$(BLUE)━━━ relative and dirfd-relative names ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-name-relative - @printf "\n$(BLUE)━━━ non-ASCII guest filenames ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-name-i18n - @printf "\n$(BLUE)━━━ guest filenames at full length ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-name-length - @printf "\n$(BLUE)━━━ host-staged escape-shaped names ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-name-staged + $(CHECK_SHARED_LANES) ## Run the unit test suite plus busybox applet validation check: $(ELFUSE_BIN) $(TEST_DEPS) check-syscall-coverage \ - $(BUILD_DIR)/test-tlbi-encoder-host \ - $(BUILD_DIR)/test-fork-ipc-protocol-host \ - $(BUILD_DIR)/test-vcpu-run-hooks-host \ - $(BUILD_DIR)/test-identity-override-host \ - $(BUILD_DIR)/test-teardown-live-vcpu-host \ - $(BUILD_DIR)/test-casefold-host \ - $(BUILD_DIR)/test-casefold-walk-host \ - $(BUILD_DIR)/test-absock-names-host + $(CHECK_HOST_UNIT_BINS) @bash tests/driver.sh -e $(ELFUSE_BIN) -d $(TEST_DIR) -v - @printf "\n$(BLUE)━━━ TLBI RVAE1IS encoder unit test ━━━$(RESET)\n" - @$(BUILD_DIR)/test-tlbi-encoder-host - @printf "\n$(BLUE)━━━ fork IPC protocol identity unit test ━━━$(RESET)\n" - @$(BUILD_DIR)/test-fork-ipc-protocol-host - @printf "\n$(BLUE)━━━ vCPU run-loop hook API unit test ━━━$(RESET)\n" - @$(BUILD_DIR)/test-vcpu-run-hooks-host - @printf "\n$(BLUE)━━━ identity override unit test ━━━$(RESET)\n" - @$(BUILD_DIR)/test-identity-override-host - @printf "\n$(BLUE)━━━ teardown live-worker accounting unit test ━━━$(RESET)\n" - @$(BUILD_DIR)/test-teardown-live-vcpu-host - @printf "\n$(BLUE)━━━ filename codec unit test ━━━$(RESET)\n" - @$(BUILD_DIR)/test-casefold-host - @printf "\n$(BLUE)━━━ case-exact path resolution unit test ━━━$(RESET)\n" - @$(BUILD_DIR)/test-casefold-walk-host - @printf "\n$(BLUE)━━━ absock derived-name unit test ━━━$(RESET)\n" - @$(BUILD_DIR)/test-absock-names-host - @printf "\n$(BLUE)━━━ one on-disk name per guest name ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-name-unique - @printf "\n$(BLUE)━━━ relative and dirfd-relative names ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-name-relative - @printf "\n$(BLUE)━━━ non-ASCII guest filenames ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-name-i18n - @printf "\n$(BLUE)━━━ guest filenames at full length ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-name-length - @printf "\n$(BLUE)━━━ host-staged escape-shaped names ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-name-staged - @printf "\n$(BLUE)━━━ concurrent creation of colliding names ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-name-race - @printf "\n$(BLUE)━━━ guest paths at the host path ceiling ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-pathmax - @printf "\n$(BLUE)━━━ frozen on-disk spelling corpus ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-corpus - @printf "\n$(BLUE)━━━ addressing modes agree across the path matrix ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-path-matrix - @printf "\n$(BLUE)━━━ shebang parser unit test ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-shebang-host - @printf "\n$(BLUE)━━━ gva-math.h call-site contract checks ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-gva-contracts - @printf "\n$(BLUE)━━━ proctitle argv-tail regression ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-proctitle-host - @printf "\n$(BLUE)━━━ proctitle low-stack regression ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-proctitle-low-stack - @printf "\n$(BLUE)━━━ busybox applet validation ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-busybox - @printf "\n$(BLUE)━━━ sysroot procfs exec validation ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-procfs-exec - @printf "\n$(BLUE)━━━ getdents64 overlong-UTF-8 dirent skip ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-getdents64-overlong - @printf "\n$(BLUE)━━━ sysroot host-fallback validation ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-host-fallback - @printf "\n$(BLUE)━━━ sysroot /tmp remove/rename consistency ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-tmp-remove - @printf "\n$(BLUE)━━━ sysroot byte-exact lookup validation ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-case-exact - @printf "\n$(BLUE)━━━ sysroot relative-dirfd symlink escape validation ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-symlink-escape - @printf "\n$(BLUE)━━━ no-symlinks path walking ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-openat2-walk - @printf "\n$(BLUE)━━━ sysroot '..' resolution ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-dotdot - @printf "\n$(BLUE)━━━ escaped symlink-target resolution ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-symlink-target - @printf "\n$(BLUE)━━━ inotify names across the escape boundary ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-inotify-names - @printf "\n$(BLUE)━━━ exec identity across the escape boundary ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-exec-names - @printf "\n$(BLUE)━━━ PT_INTERP /lib fallback ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-interp-fallback - @printf "\n$(BLUE)━━━ PT_INTERP through an escaped path ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-interp-cased - @printf "\n$(BLUE)━━━ pathname sockets across the escape boundary ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-absock-names - @printf "\n$(BLUE)━━━ absock namespace lifecycle ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-absock-cleanup - @printf "\n$(BLUE)━━━ sysroot mounted at / ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-root - @printf "\n$(BLUE)━━━ literal names without a sysroot ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-nosysroot-literal-names - @printf "\n$(BLUE)━━━ literal names outside the sysroot ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-outside-names - @printf "\n$(BLUE)━━━ guest-visible working directory ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-sysroot-chdir - @printf "\n$(BLUE)━━━ case collisions on a folding sysroot ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-case-collision-fallback - @printf "\n$(BLUE)━━━ Alpine sysroot FUSE validation ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-fuse-alpine - @printf "\n$(BLUE)━━━ timeout=0 validation ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-timeout-disable - @printf "\n$(BLUE)━━━ rosetta CLI gating ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-rosetta-cli - @printf "\n$(BLUE)━━━ hot-syscall guardrail ━━━$(RESET)\n" - @$(MAKE) --no-print-directory test-bench-guardrail + $(CHECK_SHARED_LANES) + $(call run-lane,test-sysroot-name-race,concurrent creation of colliding names) + $(call run-lane,test-sysroot-pathmax,guest paths at the host path ceiling) + $(call run-lane,test-sysroot-corpus,frozen on-disk spelling corpus) + $(call run-lane,test-sysroot-path-matrix,addressing modes agree across the path matrix) + $(call run-lane,test-shebang-host,shebang parser unit test) + $(call run-lane,test-gva-contracts,gva-math.h call-site contract checks) + $(call run-lane,test-proctitle-host,proctitle argv-tail regression) + $(call run-lane,test-proctitle-low-stack,proctitle low-stack regression) + $(call run-lane,test-busybox,busybox applet validation) + $(call run-lane,test-sysroot-procfs-exec,sysroot procfs exec validation) + $(call run-lane,test-getdents64-overlong,getdents64 overlong-UTF-8 dirent skip) + $(call run-lane,test-sysroot-host-fallback,sysroot host-fallback validation) + $(call run-lane,test-sysroot-tmp-remove,sysroot /tmp remove/rename consistency) + $(call run-lane,test-sysroot-case-exact,sysroot byte-exact lookup validation) + $(call run-lane,test-sysroot-symlink-escape,sysroot relative-dirfd symlink escape validation) + $(call run-lane,test-sysroot-openat2-walk,no-symlinks path walking) + $(call run-lane,test-sysroot-dotdot,sysroot '..' resolution) + $(call run-lane,test-sysroot-symlink-target,escaped symlink-target resolution) + $(call run-lane,test-sysroot-inotify-names,inotify names across the escape boundary) + $(call run-lane,test-sysroot-exec-names,exec identity across the escape boundary) + $(call run-lane,test-sysroot-interp-fallback,PT_INTERP /lib fallback) + $(call run-lane,test-sysroot-interp-cased,PT_INTERP through an escaped path) + $(call run-lane,test-sysroot-absock-names,pathname sockets across the escape boundary) + $(call run-lane,test-absock-cleanup,absock namespace lifecycle) + $(call run-lane,test-sysroot-root,sysroot mounted at /) + $(call run-lane,test-nosysroot-literal-names,literal names without a sysroot) + $(call run-lane,test-sysroot-outside-names,literal names outside the sysroot) + $(call run-lane,test-sysroot-chdir,guest-visible working directory) + $(call run-lane,test-case-collision-fallback,case collisions on a folding sysroot) + $(call run-lane,test-fuse-alpine,Alpine sysroot FUSE validation) + $(call run-lane,test-timeout-disable,timeout=0 validation) + $(call run-lane,test-rosetta-cli,rosetta CLI gating) + $(call run-lane,test-bench-guardrail,hot-syscall guardrail) ## Hot-syscall performance guardrail: ensure getpid, libc clock_gettime, ## and 1-byte /dev/urandom reads stay under their TODO ns/op ceilings. @@ -395,9 +372,7 @@ test-case-collision-fallback: $(ELFUSE_BIN) $(BUILD_DIR)/test-case-collision ## resolver's host-literal fallback instead of vetoing it with ENOENT. ## Regression test for the test-matrix "musl dyn" coreutils failures. test-sysroot-host-fallback: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-host-fallback - @set -e; \ - tmpdir=$$(mktemp -d); \ - trap 'rm -rf "$$tmpdir"' EXIT; \ + @$(SYSROOT_SCRATCH); \ sysroot="$$tmpdir/sysroot"; \ hostdir="$$tmpdir/host-data"; \ mkdir -p "$$sysroot" "$$hostdir"; \ @@ -454,9 +429,7 @@ test-sysroot-tmp-remove: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-tmp-remove ## spellings do not resolve. The normalization probes require the walk, ## so the guest skips them when the staging volume is case-sensitive. test-sysroot-case-exact: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-case-exact - @set -e; \ - tmpdir=$$(mktemp -d); \ - trap 'rm -rf "$$tmpdir"' EXIT; \ + @$(SYSROOT_SCRATCH); \ sysroot="$$tmpdir/sysroot"; \ mkdir -p "$$sysroot/data/sub"; \ printf 'exact\n' > "$$sysroot/data/Makefile"; \ @@ -480,9 +453,7 @@ test-sysroot-case-exact: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-case-exact # name. ## Each guest name has exactly one on-disk representation test-sysroot-name-unique: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-name-unique - @set -e; \ - tmpdir=$$(mktemp -d); \ - trap 'rm -rf "$$tmpdir"' EXIT; \ + @$(SYSROOT_SCRATCH); \ $(ELFUSE_BIN) --sysroot "$$tmpdir" \ $(BUILD_DIR)/test-sysroot-name-unique; \ left=$$(ls -A "$$tmpdir/name-unique" 2>/dev/null | wc -l | tr -d ' '); \ @@ -499,9 +470,7 @@ test-sysroot-name-unique: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-name-unique # it is measured against. Tree walkers reach every name this way. ## Relative and dirfd-relative names resolve to the same file as absolute ones test-sysroot-name-relative: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-name-relative - @set -e; \ - tmpdir=$$(mktemp -d); \ - trap 'rm -rf "$$tmpdir"' EXIT; \ + @$(SYSROOT_SCRATCH); \ outside="$$tmpdir-outside"; \ mkdir -p "$$outside"; \ trap 'rm -rf "$$tmpdir" "$$tmpdir-outside"' EXIT; \ @@ -529,9 +498,7 @@ test-sysroot-name-relative: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-name-relativ ## Symlink targets are resolved in the guest namespace, not the host's test-sysroot-symlink-target: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-symlink-target - @set -e; \ - tmpdir=$$(mktemp -d); \ - trap 'rm -rf "$$tmpdir"' EXIT; \ + @$(SYSROOT_SCRATCH); \ mkdir -p "$$tmpdir/host-downloads"; \ ln -s "$$HOME" "$$tmpdir/host-home-link"; \ mkdir -p "$$tmpdir/home/muplar"; \ @@ -556,10 +523,8 @@ test-sysroot-symlink-target: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-symlink-tar # exit. ## pathname AF_UNIX socket addresses resolve through the sysroot test-sysroot-absock-names: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-absock-names - @set -e; \ - tmpdir=$$(mktemp -d); \ - trap 'rm -rf "$$tmpdir"' EXIT; \ - before=$$(ls -d /tmp/elfuse-absock-* 2>/dev/null | wc -l | tr -d ' '); \ + @$(SYSROOT_SCRATCH); \ + $(ABSOCK_LEAK_BEFORE); \ $(ELFUSE_BIN) --sysroot "$$tmpdir" \ $(BUILD_DIR)/test-sysroot-absock-names; \ for leaf in My.Sock Recv.Sock Sender.Sock sock Sock; do \ @@ -573,21 +538,15 @@ test-sysroot-absock-names: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-absock-names ls -Ab "$$tmpdir/sockdir"; \ exit 1; \ fi; \ - after=$$(ls -d /tmp/elfuse-absock-* 2>/dev/null | wc -l | tr -d ' '); \ - if [ "$$after" -gt "$$before" ]; then \ - printf "$(RED)FAIL$(RESET) absock namespace dir leaked ($$before -> $$after)\n"; \ - exit 1; \ - fi + $(ASSERT_NO_ABSOCK_LEAK) # The absock namespace dir is shared across a forked guest tree, so neither # exit order may destroy state the other side still needs; the recipe also # asserts the dir itself does not leak. ## absock namespace lifecycle across fork and exit order test-absock-cleanup: $(ELFUSE_BIN) $(BUILD_DIR)/test-absock-cleanup - @set -e; \ - tmpdir=$$(mktemp -d); \ - trap 'rm -rf "$$tmpdir"' EXIT; \ - before=$$(ls -d /tmp/elfuse-absock-* 2>/dev/null | wc -l | tr -d ' '); \ + @$(SYSROOT_SCRATCH); \ + $(ABSOCK_LEAK_BEFORE); \ $(ELFUSE_BIN) --sysroot "$$tmpdir" $(BUILD_DIR)/test-absock-cleanup; \ printf " %-30s " "owner sweep spares live child"; \ out=$$($(ELFUSE_BIN) --sysroot "$$tmpdir" \ @@ -609,11 +568,7 @@ test-absock-cleanup: $(ELFUSE_BIN) $(BUILD_DIR)/test-absock-cleanup printf "FAIL: late bind %s\n" "$${verdict:-unreported}"; \ exit 1; \ fi; \ - after=$$(ls -d /tmp/elfuse-absock-* 2>/dev/null | wc -l | tr -d ' '); \ - if [ "$$after" -gt "$$before" ]; then \ - printf "$(RED)FAIL$(RESET) absock namespace dir leaked ($$before -> $$after)\n"; \ - exit 1; \ - fi + $(ASSERT_NO_ABSOCK_LEAK) # PT_INTERP names the loader by the guest's spelling, and a rootfs may ship # it somewhere other than where the binary asks (store-style paths). The @@ -684,9 +639,7 @@ test-sysroot-interp-cased: $(ELFUSE_BIN) $(BUILD_DIR)/mkdir-arg \ # so the passing exec lanes prove the resolution actually crossed it. ## exec paths and the reported exec identity stay in the guest namespace test-sysroot-exec-names: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-exec-names - @set -e; \ - tmpdir=$$(mktemp -d); \ - trap 'rm -rf "$$tmpdir"' EXIT; \ + @$(SYSROOT_SCRATCH); \ $(ELFUSE_BIN) --sysroot "$$tmpdir" \ $(BUILD_DIR)/test-sysroot-exec-names; \ if [ -e "$$tmpdir/Apps" ] && \ @@ -721,13 +674,13 @@ test-sysroot-root: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-root $(ELFUSE_BIN) --sysroot / $(BUILD_DIR)/test-sysroot-root ## Escape-shaped host names must mean themselves when there is no sysroot -test-nosysroot-literal-names: $(ELFUSE_BIN) $(BUILD_DIR)/test-nosysroot-literal-names - @set -e; \ - tmpdir=$$(mktemp -d); \ - trap 'rm -rf "$$tmpdir"' EXIT; \ +# Same binary as test-sysroot-outside-names: one set of assertions, two +# invocation modes, so the two halves of the scoping contract cannot drift. +test-nosysroot-literal-names: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-outside-names + @$(SYSROOT_SCRATCH); \ printf 'literal\n' > "$$tmpdir/.ef=464f4f"; \ printf 'other\n' > "$$tmpdir/plain"; \ - $(ELFUSE_BIN) $(BUILD_DIR)/test-nosysroot-literal-names "$$tmpdir" + $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-outside-names "$$tmpdir" nosysroot ## Escape-shaped host names must mean themselves outside the sysroot test-sysroot-outside-names: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-outside-names @@ -750,9 +703,7 @@ test-sysroot-outside-names: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-outside-name # equal and the guest must see as two files. ## Non-ASCII, normalization and case-folding guest filenames test-sysroot-name-i18n: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-name-i18n - @set -e; \ - tmpdir=$$(mktemp -d); \ - trap 'rm -rf "$$tmpdir"' EXIT; \ + @$(SYSROOT_SCRATCH); \ printf 'staged\n' > "$$tmpdir/$$(printf '\346\226\207\346\241\243')-host.txt"; \ $(ELFUSE_BIN) --sysroot "$$tmpdir" $(BUILD_DIR)/test-sysroot-name-i18n; \ if [ ! -e "$$tmpdir/$$(printf '\346\226\207\346\241\243')-host.txt" ]; then \ @@ -765,9 +716,7 @@ test-sysroot-name-i18n: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-name-i18n # stands for. Nothing below the Linux maximum may be refused. ## Guest filenames at their full length, both stored forms test-sysroot-name-length: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-name-length - @set -e; \ - tmpdir=$$(mktemp -d); \ - trap 'rm -rf "$$tmpdir"' EXIT; \ + @$(SYSROOT_SCRATCH); \ $(ELFUSE_BIN) --sysroot "$$tmpdir" $(BUILD_DIR)/test-sysroot-name-length # Component length is a guest budget; whole-path length is a host one, and the @@ -778,9 +727,7 @@ test-sysroot-name-length: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-name-length # there. ## Guest paths that cross the host path ceiling report ENAMETOOLONG test-sysroot-pathmax: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-pathmax - @set -e; \ - tmpdir=$$(mktemp -d); \ - trap 'rm -rf "$$tmpdir"' EXIT; \ + @$(SYSROOT_SCRATCH); \ printf 'probe\n' > "$$tmpdir/pathmax-probe"; \ mode=exact; \ if [ -e "$$tmpdir/PATHMAX-PROBE" ]; then mode=fold; fi; \ @@ -797,9 +744,7 @@ test-sysroot-pathmax: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-pathmax # uses a payload whose lowercase form is not also staged. ## Host-staged escape-shaped names in a sysroot test-sysroot-name-staged: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-name-staged - @set -e; \ - tmpdir=$$(mktemp -d); \ - trap 'rm -rf "$$tmpdir"' EXIT; \ + @$(SYSROOT_SCRATCH); \ d="$$tmpdir/staged"; \ mkdir -p "$$d"; \ printf 'plain\n' > "$$d/Plain.Host"; \ @@ -840,9 +785,7 @@ test-sysroot-name-staged: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-name-staged # scratch volume is a SKIP, not a pass. ## Read a corpus of frozen on-disk spellings staged host-side test-sysroot-corpus: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-corpus - @set -e; \ - tmpdir=$$(mktemp -d); \ - trap 'rm -rf "$$tmpdir"' EXIT; \ + @$(SYSROOT_SCRATCH); \ printf 'p\n' > "$$tmpdir/CaseProbe"; \ if [ ! -e "$$tmpdir/caseprobe" ]; then \ printf "$(YELLOW)SKIP$(RESET) test-sysroot-corpus (scratch volume is case-sensitive)\n"; \ @@ -873,9 +816,7 @@ test-sysroot-corpus: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-corpus # of tests/test-sysroot-name-soak.c states the invariants. ## Soak colliding-name churn for SECS seconds (default 120) test-sysroot-name-soak: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-name-soak - @set -e; \ - tmpdir=$$(mktemp -d); \ - trap 'rm -rf "$$tmpdir"' EXIT; \ + @$(SYSROOT_SCRATCH); \ $(ELFUSE_BIN) --timeout 0 --sysroot "$$tmpdir" \ $(BUILD_DIR)/test-sysroot-name-soak $(or $(SECS),120) @@ -923,9 +864,7 @@ test-sysroot-name-race: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-name-race # case-sensitive volume. ## Addressing modes agree over operation x shape x name class test-sysroot-path-matrix: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-path-matrix - @set -e; \ - tmpdir=$$(mktemp -d); \ - trap 'rm -rf "$$tmpdir"' EXIT; \ + @$(SYSROOT_SCRATCH); \ $(ELFUSE_BIN) --sysroot "$$tmpdir" \ $(BUILD_DIR)/test-sysroot-path-matrix @@ -1000,9 +939,7 @@ check-name-caseexact: $(ELFUSE_BIN) $(BUILD_DIR)/test-sysroot-name-unique \ # any APFS hash ordering puts an overlong entry in a position where # pre-fix code returned -ENAMETOOLONG to userspace. test-getdents64-overlong: $(ELFUSE_BIN) $(BUILD_DIR)/test-getdents64-overlong - @set -e; \ - tmpdir=$$(mktemp -d); \ - trap 'rm -rf "$$tmpdir"' EXIT; \ + @$(SYSROOT_SCRATCH); \ mkdir -p "$$tmpdir/fixture"; \ : > "$$tmpdir/fixture/expected.txt"; \ for tag in a b c d e; do \ diff --git a/tests/host-test-util.h b/tests/host-test-util.h index eb107dd7..99844a34 100644 --- a/tests/host-test-util.h +++ b/tests/host-test-util.h @@ -16,12 +16,71 @@ #include #include #include +#include #include +#include #include #include #include #include +/* Pass/fail bookkeeping for the host lanes. Counters in a header are safe + * here because each host binary is a single translation unit. + */ +static int host_passes; +static int host_fails; + +static inline void host_ok(void) +{ + host_passes++; +} + +static inline void host_fail(const char *label, const char *detail) +{ + host_fails++; + fprintf(stderr, "FAIL %s: %s\n", label, detail); +} + +static inline void host_check(bool ok, const char *label, const char *detail) +{ + if (ok) + host_ok(); + else + host_fail(label, detail); +} + +/* Print the lane's verdict line and return its exit status. */ +static inline int host_summary(const char *name) +{ + printf("%s: %d passed, %d failed - %s\n", name, host_passes, host_fails, + host_fails ? "FAIL" : "PASS"); + return host_fails ? 1 : 0; +} + +/* Make a scratch root named after @tag under @argv1, $TMPDIR, or /tmp, in + * that order. Returns 0 with the path in @out, or -1 after reporting why + * under @lane, the caller's argv[0]: in a CI log the failure line is the + * only thing naming which test died, and the tag does not. + */ +static inline int host_scratch_root(const char *lane, + const char *tag, + const char *argv1, + char *out, + size_t outsz) +{ + const char *base = argv1 ? argv1 : getenv("TMPDIR"); + + if (!base || base[0] == '\0') + base = "/tmp"; + snprintf(out, outsz, "%s/%s-XXXXXX", base, tag); + if (!mkdtemp(out)) { + fprintf(stderr, "%s: cannot create a scratch directory in %s: %s\n", + lane, base, strerror(errno)); + return -1; + } + return 0; +} + /* Encode one code point as UTF-8 into @o, which needs four bytes of room. * Returns the number written. Hand-rolled because the tests build names from * code points chosen for their folding behavior, and iconv would drag a locale @@ -58,6 +117,13 @@ static inline int utf8_put(char *o, unsigned cp) * tell "exists as spelled" from "exists under a spelling that folded onto it". * FSOPT_NOFOLLOW because a symlink's own name is the question, not its * target's. Returns a pointer to static storage, valid until the next call. + * + * The reply is bounds-checked before the reference and the name are read: + * attr_dataoffset and attr_length come from the filesystem, and --sysroot may + * name an SMB or NFS mount that need not fill them the way APFS does. This + * mirrors casefold_attr_stored_name rather than calling it: of the three + * lanes including this header, only test-casefold-walk-host links + * casefold-walk.o. */ static inline const char *disk_name(const char *dir, const char *name) { @@ -72,7 +138,7 @@ static inline const char *disk_name(const char *dir, const char *name) attribute_set_t returned; attrreference_t name_ref; char name[1024]; - } __attribute__((aligned(4), packed)) buf; + } __attribute__((aligned(4), packed)) buf = {0}; if (snprintf(path, sizeof(path), "%s/%s", dir, name) >= (int) sizeof(path)) return NULL; @@ -81,8 +147,33 @@ static inline const char *disk_name(const char *dir, const char *name) return NULL; if (!(buf.returned.commonattr & ATTR_CMN_NAME)) return NULL; - snprintf(out, sizeof(out), "%s", - (const char *) &buf.name_ref + buf.name_ref.attr_dataoffset); + + /* The reference itself is checked against the reply before its fields are + * read: a volume can set ATTR_CMN_NAME in returned yet write a reply too + * short to hold the reference, and buf past buf.length is only zeros. + */ + size_t usable = buf.length < sizeof(buf) ? buf.length : sizeof(buf); + size_t ref_off = + (size_t) ((const char *) &buf.name_ref - (const char *) &buf); + if (ref_off > usable || usable - ref_off < sizeof(buf.name_ref)) + return NULL; + + /* attr_dataoffset is signed; a negative one points before the reference, + * outside anything the kernel wrote for this attribute. + */ + if (buf.name_ref.attr_dataoffset <= 0 || buf.name_ref.attr_length == 0) + return NULL; + size_t name_off = ref_off + (size_t) buf.name_ref.attr_dataoffset; + if (name_off >= usable || buf.name_ref.attr_length > usable - name_off) + return NULL; + + const char *stored = (const char *) &buf + name_off; + /* No NUL inside the declared length means the name is not a C string, and + * "%s" would read past what the volume wrote. + */ + if (!memchr(stored, '\0', buf.name_ref.attr_length)) + return NULL; + snprintf(out, sizeof(out), "%s", stored); return out; } diff --git a/tests/probe-volume-naming.c b/tests/probe-volume-naming.c index f2f644f5..1361f8aa 100644 --- a/tests/probe-volume-naming.c +++ b/tests/probe-volume-naming.c @@ -362,17 +362,11 @@ static void section_alphabet(void) int main(int argc, char **argv) { - const char *base = argc > 1 ? argv[1] : getenv("TMPDIR"); char tmpl[4096]; - if (!base || base[0] == '\0') - base = "/tmp"; - snprintf(tmpl, sizeof(tmpl), "%s/elfuse-probe-XXXXXX", base); - if (!mkdtemp(tmpl)) { - fprintf(stderr, "cannot create a scratch directory in %s: %s\n", base, - strerror(errno)); + if (host_scratch_root(argv[0], "elfuse-probe", argc > 1 ? argv[1] : NULL, + tmpl, sizeof(tmpl)) < 0) return 1; - } root = tmpl; printf("probing %s\n\n", root); diff --git a/tests/test-absock-cleanup.c b/tests/test-absock-cleanup.c index d635c878..990b734b 100644 --- a/tests/test-absock-cleanup.c +++ b/tests/test-absock-cleanup.c @@ -53,22 +53,6 @@ int passes = 0, fails = 0; #define DEEP_DIR "/Deep.A/Deep.B/Deep.C/Deep.D" -static int bind_pathname(const char *path) -{ - int fd = socket(AF_UNIX, SOCK_STREAM, 0); - if (fd < 0) - return -1; - struct sockaddr_un un = {0}; - un.sun_family = AF_UNIX; - strncpy(un.sun_path, path, sizeof(un.sun_path) - 1); - if (bind(fd, (struct sockaddr *) &un, sizeof(un)) < 0 || - listen(fd, 1) < 0) { - close(fd); - return -1; - } - return fd; -} - static bool getsockname_is(int fd, const char *expect) { struct sockaddr_un got = {0}; @@ -208,7 +192,7 @@ static int child_after_owner_mode(void) (mkdir("/Deep.A/Deep.B", 0755) == 0 || errno == EEXIST) && (mkdir("/Deep.A/Deep.B/Deep.C", 0755) == 0 || errno == EEXIST) && (mkdir(DEEP_DIR, 0755) == 0 || errno == EEXIST); - int fd = ok ? bind_pathname(DEEP_DIR "/Late.Sock") : -1; + int fd = ok ? unix_bind(DEEP_DIR "/Late.Sock", SOCK_STREAM, 1) : -1; ok = fd >= 0 && getsockname_is(fd, DEEP_DIR "/Late.Sock"); const char *msg = ok ? "CHILD_AFTER=ok\n" : "CHILD_AFTER=failed\n"; @@ -247,7 +231,7 @@ int main(int argc, char **argv) EXPECT_TRUE(deep, "mkdir deep chain"); TEST("parent over-long bind"); - int pfd = bind_pathname(DEEP_DIR "/Parent.Sock"); + int pfd = unix_bind(DEEP_DIR "/Parent.Sock", SOCK_STREAM, 1); EXPECT_TRUE(pfd >= 0, "parent bind+listen"); TEST("parent getsockname before fork"); @@ -261,7 +245,7 @@ int main(int argc, char **argv) TEST("child binds over-long and exits"); pid_t pid = fork(); if (pid == 0) { - int cfd = bind_pathname(DEEP_DIR "/Child.Sock"); + int cfd = unix_bind(DEEP_DIR "/Child.Sock", SOCK_STREAM, 1); _exit(cfd >= 0 ? 0 : 1); } int status = 0; diff --git a/tests/test-absock-names-host.c b/tests/test-absock-names-host.c index 32b21f29..e294af3f 100644 --- a/tests/test-absock-names-host.c +++ b/tests/test-absock-names-host.c @@ -34,6 +34,7 @@ #include #include +#include "host-test-util.h" #include "syscall/net-abi.h" #include "syscall/net-absock.h" #include "syscall/path.h" @@ -84,19 +85,6 @@ int path_host_to_guest(const char *host_path, char *out, size_t outsz) return -1; } -static int passes; -static int fails; - -static void check(bool ok, const char *label, const char *detail) -{ - if (ok) { - passes++; - } else { - fails++; - fprintf(stderr, "FAIL %s: %s\n", label, detail); - } -} - /* The 32-bit FNV-1a the old tail used, kept here as the collision oracle. */ static uint32_t fnv1a32(const char *s) { @@ -189,10 +177,10 @@ int main(void) printf("test-absock-names-host: derived socket names\n"); - check(find_fnv32_collision(a, b, sizeof(a)), "collision search", - "no 32-bit collision in 400k candidates"); - check(!strncmp(a, b, 20) && strcmp(a, b), "collision pair", - "pair must share 20 bytes and differ"); + host_check(find_fnv32_collision(a, b, sizeof(a)), "collision search", + "no 32-bit collision in 400k candidates"); + host_check(!strncmp(a, b, 20) && strcmp(a, b), "collision pair", + "pair must share 20 bytes and differ"); /* The pair defeats a 32-bit tail by construction; the encoded names must * still differ. @@ -201,17 +189,18 @@ int main(void) sizeof(name_a)); absock_encode_name(dir, (const uint8_t *) b, (uint32_t) strlen(b), name_b, sizeof(name_b)); - check(strcmp(name_a, name_b), "encode collision", - "32-bit-colliding names must encode differently"); - check(strlen(name_a) < 104, "encode budget", - "encoded name must fit sun_path"); - - check(absock_link_name(dir, a, name_a, sizeof(name_a)) == 0 && - absock_link_name(dir, b, name_b, sizeof(name_b)) == 0, - "link naming", "link name derivation failed"); - check(strcmp(name_a, name_b), "link collision", - "32-bit-colliding paths must derive distinct links"); - check(strlen(name_a) < 104, "link budget", "link name must fit sun_path"); + host_check(strcmp(name_a, name_b), "encode collision", + "32-bit-colliding names must encode differently"); + host_check(strlen(name_a) < 104, "encode budget", + "encoded name must fit sun_path"); + + host_check(absock_link_name(dir, a, name_a, sizeof(name_a)) == 0 && + absock_link_name(dir, b, name_b, sizeof(name_b)) == 0, + "link naming", "link name derivation failed"); + host_check(strcmp(name_a, name_b), "link collision", + "32-bit-colliding paths must derive distinct links"); + host_check(strlen(name_a) < 104, "link budget", + "link name must fit sun_path"); /* Pid scoping: the marker that makes every exit-time unlink provably of * the process's own property. @@ -220,8 +209,8 @@ int main(void) char marker[32]; snprintf(marker, sizeof(marker), "/p%d-", (int) getpid()); - check(strstr(name_a, marker) != NULL, "pid scope", - "link name must embed the minting pid"); + host_check(strstr(name_a, marker) != NULL, "pid scope", + "link name must embed the minting pid"); } /* The digest never truncates: with a tight buffer the literal prefix @@ -230,14 +219,12 @@ int main(void) { char tight[64]; - check(absock_link_name(dir, a, tight, sizeof(tight)) == 0 && - strlen(tight) >= 16, - "tight budget", "digest must survive a tight buffer"); - check(strspn(tight + strlen(tight) - 16, "0123456789abcdef") == 16, - "digest tail", "last 16 chars must be the hex digest"); + host_check(absock_link_name(dir, a, tight, sizeof(tight)) == 0 && + strlen(tight) >= 16, + "tight budget", "digest must survive a tight buffer"); + host_check(strspn(tight + strlen(tight) - 16, "0123456789abcdef") == 16, + "digest tail", "last 16 chars must be the hex digest"); } - printf("test-absock-names-host: %d passed, %d failed - %s\n", passes, fails, - fails ? "FAIL" : "PASS"); - return fails ? 1 : 0; + return host_summary("test-absock-names-host"); } diff --git a/tests/test-case-collision.c b/tests/test-case-collision.c index 8f759ca0..7b49e0db 100644 --- a/tests/test-case-collision.c +++ b/tests/test-case-collision.c @@ -60,35 +60,6 @@ typedef struct { char d_name[]; } linux_dirent64_t; -static int create_file(const char *path, const char *contents) -{ - int fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0644); - if (fd < 0) - return -1; - size_t len = strlen(contents); - int rc = write_fd_all(fd, contents, len); - close(fd); - return rc; -} - -static int dir_has_entry(const char *path, const char *needle) -{ - DIR *dir = opendir(path); - if (!dir) - return -1; - - int found = 0; - struct dirent *de; - while ((de = readdir(dir)) != NULL) { - if (!strcmp(de->d_name, needle)) { - found = 1; - break; - } - } - closedir(dir); - return found; -} - static void build_long_name(char *out, size_t outsz, char first) { memset(out, 'a', outsz - 1); @@ -100,7 +71,7 @@ static int xattr_supported(void) { const char *probe = "/tmp/elfuse-case-collision-xattr-probe"; unlink(probe); - if (create_file(probe, "probe\n") < 0) + if (file_write(probe, "probe\n") < 0) return 0; int rc = setxattr(probe, "user.elfuse_probe", "x", 1, 0); @@ -202,7 +173,7 @@ static void check_linkat(const char *base, unlink(link_path); unlink(target); - if (create_file(target, "linkat\n") < 0) { + if (file_write(target, "linkat\n") < 0) { FAIL("failed to create link target"); } else if (symlink(absolute_target ? target : target_name, link_path) < 0) { FAIL("failed to create symlink"); @@ -246,11 +217,10 @@ int main(void) unlink(upper); unlink(lower); - if (create_file(upper, "upper\n") < 0 || - create_file(lower, "lower\n") < 0) { + if (file_write(upper, "upper\n") < 0 || + file_write(lower, "lower\n") < 0) { FAIL("failed to create colliding files"); - } else if (dir_has_entry(base, "Foo") != 1 || - dir_has_entry(base, "foo") != 1) { + } else if (!dir_contains(base, "Foo") || !dir_contains(base, "foo")) { FAIL("readdir collapsed colliding names"); } else { PASS(); @@ -291,8 +261,8 @@ int main(void) unlink(left); unlink(right); - if (create_file(left, "left\n") < 0 || - create_file(right, "right\n") < 0) { + if (file_write(left, "left\n") < 0 || + file_write(right, "right\n") < 0) { FAIL("failed to create cross-directory colliding files"); } else if (syscall(SYS_renameat2, AT_FDCWD, left, AT_FDCWD, right, LINUX_RENAME_EXCHANGE) < 0) { @@ -319,7 +289,7 @@ int main(void) unlink(src); unlink(alias); - if (create_file(src, "inode\n") < 0) { + if (file_write(src, "inode\n") < 0) { FAIL("failed to create hardlink source"); } else if (link(src, alias) < 0) { FAIL("linkat failed"); @@ -328,8 +298,8 @@ int main(void) } else if (st_src.st_ino != st_alias.st_ino || st_src.st_nlink < 2) { FAIL("colliding hardlinks do not share inode"); } else if (unlink(src) < 0 || stat(alias, &st_alias) < 0 || - dir_has_entry(base, "hardlink") != 0 || - dir_has_entry(base, "HARDLINK") != 1) { + dir_contains(base, "hardlink") || + !dir_contains(base, "HARDLINK")) { FAIL("unlink removed wrong hardlink entry"); } else { PASS(); @@ -426,8 +396,7 @@ int main(void) FAIL("renamed colliding spelling has unexpected contents"); } else if (raw_open_rdonly(untouched_path) < 0) { FAIL("rename disturbed untouched colliding entry"); - } else if (dir_has_entry(base, "foo") != 0 || - dir_has_entry(base, "bar") != 1) { + } else if (dir_contains(base, "foo") || !dir_contains(base, "bar")) { FAIL("directory listing did not reflect the rename"); } else { PASS(); @@ -538,8 +507,8 @@ int main(void) unlink(path_a); unlink(path_b); - if (create_file(path_a, "long-a\n") < 0 || - create_file(path_b, "long-b\n") < 0) { + if (file_write(path_a, "long-a\n") < 0 || + file_write(path_b, "long-b\n") < 0) { FAIL("failed to create long colliding names"); } else if (raw_open_rdonly(path_a) < 0 || raw_open_rdonly(path_b) < 0) { FAIL("failed to reopen long colliding names"); diff --git a/tests/test-casefold-host.c b/tests/test-casefold-host.c index 5d9809c2..c2709cac 100644 --- a/tests/test-casefold-host.c +++ b/tests/test-casefold-host.c @@ -43,20 +43,6 @@ #include "host-test-util.h" #include "syscall/casefold.h" -static int passes; -static int fails; - -static void ok(void) -{ - passes++; -} - -static void fail(const char *label, const char *detail) -{ - fails++; - fprintf(stderr, "FAIL %s: %s\n", label, detail); -} - /* Print a name so a failure is diagnosable when the bytes are not printable. */ static void dump(const char *label, const char *s) { @@ -83,33 +69,33 @@ static void check_roundtrip(const char *label, const char *guest) size_t units; if (casefold_escape(guest, enc, sizeof(enc)) < 0) { - fail(label, "escape failed"); + host_fail(label, "escape failed"); dump("guest", guest); return; } if (!casefold_is_escaped(enc)) { - fail(label, "encoded form is not recognized as an escape"); + host_fail(label, "encoded form is not recognized as an escape"); dump("encoded", enc); return; } units = casefold_utf16_units(enc); if (units == 0 || units > CASEFOLD_UNIT_MAX) { - fail(label, "encoded form exceeds the host per-name budget"); + host_fail(label, "encoded form exceeds the host per-name budget"); fprintf(stderr, " units = %zu\n", units); return; } if (casefold_to_guest(enc, dec, sizeof(dec)) < 0) { - fail(label, "decode failed"); + host_fail(label, "decode failed"); dump("encoded", enc); return; } if (strcmp(dec, guest)) { - fail(label, "round trip changed the name"); + host_fail(label, "round trip changed the name"); dump("guest", guest); dump("decoded", dec); return; } - ok(); + host_ok(); } static void check_not_escaped(const char *label, const char *host) @@ -117,7 +103,7 @@ static void check_not_escaped(const char *label, const char *host) char out[CASEFOLD_HOST_NAME_MAX + 1]; if (casefold_is_escaped(host)) { - fail(label, "malformed escape was accepted"); + host_fail(label, "malformed escape was accepted"); dump("host", host); return; } @@ -125,20 +111,21 @@ static void check_not_escaped(const char *label, const char *host) * report a name the guest cannot open. */ if (casefold_to_guest(host, out, sizeof(out)) < 0 || strcmp(out, host)) { - fail(label, "unrecognized name did not pass through unchanged"); + host_fail(label, "unrecognized name did not pass through unchanged"); return; } - ok(); + host_ok(); } static void check_needs_escape(const char *label, const char *name, bool want) { if (casefold_needs_escape(name) != want) { - fail(label, want ? "should need escaping" : "should not need escaping"); + host_fail(label, + want ? "should need escaping" : "should not need escaping"); dump("name", name); return; } - ok(); + host_ok(); } /* casefold_utf16_units spends no budget on a name that is not well-formed @@ -149,11 +136,12 @@ static void check_needs_escape(const char *label, const char *name, bool want) static void check_utf8(const char *label, const char *s, bool want) { if ((casefold_utf16_units(s) > 0) != want) { - fail(label, want ? "should be valid UTF-8" : "should be invalid UTF-8"); + host_fail(label, + want ? "should be valid UTF-8" : "should be invalid UTF-8"); dump("name", s); return; } - ok(); + host_ok(); } static void check_units(const char *label, const char *s, size_t want) @@ -161,11 +149,11 @@ static void check_units(const char *label, const char *s, size_t want) size_t got = casefold_utf16_units(s); if (got != want) { - fail(label, "wrong UTF-16 unit count"); + host_fail(label, "wrong UTF-16 unit count"); fprintf(stderr, " got %zu, expected %zu\n", got, want); return; } - ok(); + host_ok(); } static void fill(char *buf, size_t n, char c) @@ -192,36 +180,39 @@ static void section_golden(void) bool literal = !strcmp(v->guest, v->host); if (casefold_needs_escape(v->guest) == literal) { - fail(v->label, - literal ? "should be stored literally" : "should be escaped"); + host_fail(v->label, literal ? "should be stored literally" + : "should be escaped"); dump("guest", v->guest); continue; } if (!literal) { if (casefold_escape(v->guest, host, sizeof(host)) < 0) { - fail(v->label, "escape failed"); + host_fail(v->label, "escape failed"); dump("guest", v->guest); continue; } if (strcmp(host, v->host)) { - fail(v->label, "on-disk spelling moved off the frozen bytes"); + host_fail(v->label, + "on-disk spelling moved off the frozen bytes"); dump("expected", v->host); dump("got", host); continue; } if (!casefold_is_escaped(v->host)) { - fail(v->label, "frozen spelling not recognized as an escape"); + host_fail(v->label, + "frozen spelling not recognized as an escape"); continue; } } if (casefold_to_guest(v->host, guest, sizeof(guest)) < 0 || strcmp(guest, v->guest)) { - fail(v->label, "frozen spelling did not decode to the guest name"); + host_fail(v->label, + "frozen spelling did not decode to the guest name"); dump("host", v->host); dump("decoded", guest); continue; } - ok(); + host_ok(); } /* The long-tier budget arithmetic, pinned on the frozen strings: symbols @@ -260,10 +251,10 @@ static void section_boundaries(void) fill(name, CASEFOLD_GUEST_NAME_MAX, 'X'); if (casefold_escape(name, enc, sizeof(enc)) == 0 && casefold_utf16_units(enc) == 175 && CASEFOLD_UNIT_MAX - 175 == 80) - ok(); + host_ok(); else - fail("worst-case escape costs 175 of 255 units", - "the longest guest name no longer costs 175 units"); + host_fail("worst-case escape costs 175 of 255 units", + "the longest guest name no longer costs 175 units"); /* The tier is picked by length alone, so a short name is never spelled with * symbols and a long one never with hex. That is half of what makes each @@ -272,16 +263,16 @@ static void section_boundaries(void) fill(name, CASEFOLD_HEX_MAX, 'x'); if (casefold_escape(name, enc, sizeof(enc)) == 0 && (unsigned char) enc[CASEFOLD_PREFIX_LEN] < 0x80) - ok(); + host_ok(); else - fail("hex tier uses hex", "short name did not use the hex tier"); + host_fail("hex tier uses hex", "short name did not use the hex tier"); fill(name, CASEFOLD_HEX_MAX + 1, 'x'); if (casefold_escape(name, enc, sizeof(enc)) == 0 && (unsigned char) enc[CASEFOLD_PREFIX_LEN] >= 0x80) - ok(); + host_ok(); else - fail("symbol tier uses symbols", - "long name did not use the symbol tier"); + host_fail("symbol tier uses symbols", + "long name did not use the symbol tier"); /* "." and ".." name no entry, so no escape can stand for one. Rejecting * them keeps every name the codec accepts one that has a slot to live in; @@ -289,13 +280,13 @@ static void section_boundaries(void) * directory can hold. */ if (casefold_escape(".", enc, sizeof(enc)) < 0 && errno == EINVAL) - ok(); + host_ok(); else - fail("dot is refused", "\".\" was escaped"); + host_fail("dot is refused", "\".\" was escaped"); if (casefold_escape("..", enc, sizeof(enc)) < 0 && errno == EINVAL) - ok(); + host_ok(); else - fail("dotdot is refused", "\"..\" was escaped"); + host_fail("dotdot is refused", "\"..\" was escaped"); /* A guest name over NAME_MAX cannot reach a syscall, but the codec must * reject it rather than truncate. @@ -305,31 +296,33 @@ static void section_boundaries(void) memset(over, 'x', sizeof(over) - 1); over[sizeof(over) - 1] = '\0'; if (casefold_escape(over, enc, sizeof(enc)) < 0 && errno == EINVAL) - ok(); + host_ok(); else - fail("over-long guest name", "should be rejected with EINVAL"); + host_fail("over-long guest name", "should be rejected with EINVAL"); /* A caller buffer too small is ENAMETOOLONG, which is distinct from "this * name has no representation"; the latter cannot happen. */ fill(name, 8, 'x'); if (casefold_escape(name, enc, 4) < 0 && errno == ENAMETOOLONG) - ok(); + host_ok(); else - fail("short output buffer", "should be rejected with ENAMETOOLONG"); + host_fail("short output buffer", + "should be rejected with ENAMETOOLONG"); /* '/' separates components and so never appears inside one. Encoding it * would produce a name no directory could hold, so it is refused outright * rather than escaped. */ if (casefold_escape("a/z", enc, sizeof(enc)) < 0 && errno == EINVAL) - ok(); + host_ok(); else - fail("component containing a slash", "should be rejected with EINVAL"); + host_fail("component containing a slash", + "should be rejected with EINVAL"); if (casefold_escape("", enc, sizeof(enc)) < 0 && errno == EINVAL) - ok(); + host_ok(); else - fail("empty component", "should be rejected with EINVAL"); + host_fail("empty component", "should be rejected with EINVAL"); } static void section_shapes(void) @@ -378,10 +371,10 @@ static void section_shapes(void) if (casefold_escape(".ef=464f4f", enc, sizeof(enc)) == 0 && casefold_to_guest(enc, dec, sizeof(dec)) == 0 && !strcmp(dec, ".ef=464f4f") && strcmp(dec, "FOO")) - ok(); + host_ok(); else - fail("escape-shaped name is not confused with FOO", - "decoded to the wrong name"); + host_fail("escape-shaped name is not confused with FOO", + "decoded to the wrong name"); /* Which names must be escaped at all. Lowercase ASCII is the fixed point * that can be stored literally; everything else cannot. @@ -499,8 +492,9 @@ static void section_i18n(void) if (casefold_needs_escape(i18n_corpus[i])) check_roundtrip("i18n round trip", i18n_corpus[i]); else - ok(); /* fold-stable names are stored literally, nothing to encode - */ + host_ok(); /* fold-stable names are stored literally, nothing to + * encode + */ } /* Distinct names must encode distinctly, or two files would share a slot. @@ -512,11 +506,11 @@ static void section_i18n(void) if (!casefold_needs_escape(i18n_corpus[i])) continue; /* An encoder that refused these names would skip every comparison and - * reach the ok() below having proved nothing, so a refusal is the + * reach the host_ok() below having proved nothing, so a refusal is the * failure rather than a reason to move on. */ if (casefold_escape(i18n_corpus[i], a, sizeof(a)) < 0) { - fail("distinct names encode distinctly", "escape failed"); + host_fail("distinct names encode distinctly", "escape failed"); dump("name", i18n_corpus[i]); return; } @@ -527,20 +521,20 @@ static void section_i18n(void) if (!casefold_needs_escape(i18n_corpus[j])) continue; if (casefold_escape(i18n_corpus[j], b, sizeof(b)) < 0) { - fail("distinct names encode distinctly", "escape failed"); + host_fail("distinct names encode distinctly", "escape failed"); dump("name", i18n_corpus[j]); return; } if (!strcmp(a, b)) { - fail("distinct names encode distinctly", - "two names share an encoding"); + host_fail("distinct names encode distinctly", + "two names share an encoding"); dump("first", i18n_corpus[i]); dump("second", i18n_corpus[j]); return; } } } - ok(); + host_ok(); } /* ---------------------------------------------------------- filesystem arm */ @@ -570,7 +564,7 @@ static void section_alphabet(const char *root) snprintf(dir, sizeof(dir), "%s/alphabet", root); if (mkdir(dir, 0755) < 0) { - fail("alphabet directory", strerror(errno)); + host_fail("alphabet directory", strerror(errno)); return; } for (unsigned v = 0; v < 4096; v++) { @@ -582,14 +576,14 @@ static void section_alphabet(const char *root) name[len] = '\0'; rc = create_in(dir, name); if (rc < 0) { - fail("payload alphabet is fold-free", strerror(-rc)); + host_fail("payload alphabet is fold-free", strerror(-rc)); fprintf(stderr, " symbol %u (U+%04X)\n", v, cp); return; } created++; } if (created == 4096) - ok(); + host_ok(); int seen = 0; DIR *d = opendir(dir); @@ -601,9 +595,9 @@ static void section_alphabet(const char *root) if (d) closedir(d); if (seen == 4096) { - ok(); + host_ok(); } else { - fail("payload alphabet survives a listing", "wrong entry count"); + host_fail("payload alphabet survives a listing", "wrong entry count"); fprintf(stderr, " listed %d of 4096\n", seen); } } @@ -620,7 +614,7 @@ static void section_accept(const char *root) snprintf(dir, sizeof(dir), "%s/accept", root); if (mkdir(dir, 0755) < 0) { - fail("acceptance directory", strerror(errno)); + host_fail("acceptance directory", strerror(errno)); return; } @@ -652,7 +646,7 @@ static void section_accept(const char *root) if (casefold_needs_escape(names[k])) { if (casefold_escape(names[k], host, sizeof(host)) < 0) { - fail("encoder accepts every byte", "escape failed"); + host_fail("encoder accepts every byte", "escape failed"); dump("name", names[k]); bad = true; break; @@ -668,9 +662,9 @@ static void section_accept(const char *root) */ rc = create_in(dir, host); if (rc < 0) { - fail("encoder output is creatable", - rc == -EEXIST ? "two names share one entry" - : strerror(-rc)); + host_fail("encoder output is creatable", + rc == -EEXIST ? "two names share one entry" + : strerror(-rc)); dump("guest", names[k]); dump("host", host); bad = true; @@ -686,7 +680,7 @@ static void section_accept(const char *root) if (casefold_needs_escape(i18n_corpus[i])) { if (casefold_escape(i18n_corpus[i], host, sizeof(host)) < 0) { - fail("encoder accepts the i18n corpus", "escape failed"); + host_fail("encoder accepts the i18n corpus", "escape failed"); bad = true; break; } @@ -695,8 +689,9 @@ static void section_accept(const char *root) } rc = create_in(dir, host); if (rc < 0) { - fail("i18n encoder output is creatable", - rc == -EEXIST ? "two names share one entry" : strerror(-rc)); + host_fail( + "i18n encoder output is creatable", + rc == -EEXIST ? "two names share one entry" : strerror(-rc)); dump("guest", i18n_corpus[i]); dump("host", host); bad = true; @@ -720,7 +715,7 @@ static void section_accept(const char *root) if (casefold_needs_escape(guest)) { if (casefold_escape(guest, host, sizeof(host)) < 0) { - fail("encoder handles every length", "escape failed"); + host_fail("encoder handles every length", "escape failed"); fprintf(stderr, " length %zu\n", n); bad = true; break; @@ -730,7 +725,8 @@ static void section_accept(const char *root) } rc = create_in(dir, host); if (rc < 0 && rc != -EEXIST) { - fail("encoder output is creatable at every length", strerror(-rc)); + host_fail("encoder output is creatable at every length", + strerror(-rc)); fprintf(stderr, " length %zu\n", n); dump("host", host); bad = true; @@ -738,7 +734,7 @@ static void section_accept(const char *root) } if (!bad) - ok(); + host_ok(); } /* The three volume behaviors the resolver is built on. If a macOS release @@ -754,20 +750,20 @@ static void section_volume(const char *root) snprintf(dir, sizeof(dir), "%s/volume", root); if (mkdir(dir, 0755) < 0) { - fail("volume directory", strerror(errno)); + host_fail("volume directory", strerror(errno)); return; } if (create_in(dir, "Mixed.Case") < 0) { - fail("volume fixture", strerror(errno)); + host_fail("volume fixture", strerror(errno)); return; } spelling = disk_name(dir, "Mixed.Case"); if (spelling && !strcmp(spelling, "Mixed.Case")) - ok(); + host_ok(); else - fail("getattrlistat reports the on-disk spelling", - "probe did not return the name as stored"); + host_fail("getattrlistat reports the on-disk spelling", + "probe did not return the name as stored"); /* On a folding volume the probe is what separates "absent" from "present * under another spelling"; on a case-sensitive one the wrong case is simply @@ -775,21 +771,21 @@ static void section_volume(const char *root) */ spelling = disk_name(dir, "mixed.case"); if (!spelling || strcmp(spelling, "mixed.case")) - ok(); + host_ok(); else - fail("a wrong-case spelling is never reported as exact", - "probe accepted a folded spelling"); + host_fail("a wrong-case spelling is never reported as exact", + "probe accepted a folded spelling"); snprintf(path, sizeof(path), "%s/mixed.case", dir); if (realpath(path, real)) { const char *base = strrchr(real, '/'); if (base && !strcmp(base + 1, "Mixed.Case")) - ok(); + host_ok(); else - fail("realpath returns the true on-disk case", real); + host_fail("realpath returns the true on-disk case", real); } else { /* A case-sensitive volume has no such entry at all, which is fine. */ - ok(); + host_ok(); } /* The per-name budget is counted in UTF-16 units, not bytes: this is what @@ -801,20 +797,20 @@ static void section_volume(const char *root) len += utf8_put(wide + len, 0x6587); wide[len] = '\0'; if (create_in(dir, wide) == 0) - ok(); + host_ok(); else - fail("a 255-unit BMP name is creatable", - "the per-name limit is not counted in UTF-16 units"); + host_fail("a 255-unit BMP name is creatable", + "the per-name limit is not counted in UTF-16 units"); len = 0; for (int i = 0; i < CASEFOLD_UNIT_MAX + 1; i++) len += utf8_put(wide + len, 0x6587); wide[len] = '\0'; if (create_in(dir, wide) == -ENAMETOOLONG) - ok(); + host_ok(); else - fail("a 256-unit name is refused", - "the per-name limit is not 255 UTF-16 units"); + host_fail("a 256-unit name is refused", + "the per-name limit is not 255 UTF-16 units"); /* The worst case the encoder can produce, created for real. */ char guest[CASEFOLD_GUEST_NAME_MAX + 1]; @@ -822,15 +818,14 @@ static void section_volume(const char *root) fill(guest, CASEFOLD_GUEST_NAME_MAX, 'Q'); if (casefold_escape(guest, host, sizeof(host)) == 0 && create_in(dir, host) == 0) - ok(); + host_ok(); else - fail("the longest encoded name is creatable", - "the symbol tier does not fit the budget"); + host_fail("the longest encoded name is creatable", + "the symbol tier does not fit the budget"); } int main(int argc, char **argv) { - const char *base = argc > 1 ? argv[1] : getenv("TMPDIR"); char root[4096]; section_golden(); @@ -839,23 +834,14 @@ int main(int argc, char **argv) section_utf8(); section_i18n(); - if (!base || base[0] == '\0') - base = "/tmp"; - snprintf(root, sizeof(root), "%s/elfuse-casefold-XXXXXX", base); - if (!mkdtemp(root)) { - fprintf(stderr, - "test-casefold-host: cannot create a scratch directory in " - "%s: %s\n", - base, strerror(errno)); + if (host_scratch_root(argv[0], "elfuse-casefold", argc > 1 ? argv[1] : NULL, + root, sizeof(root)) < 0) return 1; - } section_alphabet(root); section_accept(root); section_volume(root); remove_tree(root); - printf("test-casefold-host: %d passed, %d failed - %s\n", passes, fails, - fails ? "FAIL" : "PASS"); - return fails ? 1 : 0; + return host_summary("test-casefold-host"); } diff --git a/tests/test-casefold-walk-host.c b/tests/test-casefold-walk-host.c index b9816cec..d8e478f9 100644 --- a/tests/test-casefold-walk-host.c +++ b/tests/test-casefold-walk-host.c @@ -58,22 +58,9 @@ bool proc_sysroot_casefold_enabled(void) return true; } -static int passes; -static int fails; static char root[PATH_MAX]; static bool volume_folds; -static void ok(void) -{ - passes++; -} - -static void fail(const char *label, const char *detail) -{ - fails++; - fprintf(stderr, "FAIL %s: %s\n", label, detail); -} - static void stage_file(const char *rel) { char p[PATH_MAX]; @@ -109,7 +96,7 @@ static void check(const char *label, got = casefold_resolve_at(AT_FDCWD, root, guest, false, out, sizeof(out), &walk); if (got != want_verdict) { - fail(label, "wrong verdict"); + host_fail(label, "wrong verdict"); fprintf(stderr, " guest %s -> verdict %d, expected %d (host %s)\n", guest, (int) got, (int) want_verdict, got == CASEFOLD_ERROR ? strerror(errno) : out); @@ -118,12 +105,12 @@ static void check(const char *label, snprintf(want, sizeof(want), "%s%s%s", root, want_rel[0] ? "/" : "", want_rel); if (strcmp(out, want)) { - fail(label, "wrong host spelling"); + host_fail(label, "wrong host spelling"); fprintf(stderr, " guest %s\n got %s\n expected %s\n", guest, out, want); return; } - ok(); + host_ok(); } static void check_parent_found(const char *label, const char *guest, bool want) @@ -133,15 +120,15 @@ static void check_parent_found(const char *label, const char *guest, bool want) if (casefold_resolve_at(AT_FDCWD, root, guest, false, out, sizeof(out), &walk) == CASEFOLD_ERROR) { - fail(label, strerror(errno)); + host_fail(label, strerror(errno)); return; } if (walk.parent_found != want) { - fail(label, want ? "parent should have been found" - : "parent should not have been found"); + host_fail(label, want ? "parent should have been found" + : "parent should not have been found"); return; } - ok(); + host_ok(); } /* Names the codec escapes, spelled here so the expectations read literally. */ @@ -278,10 +265,10 @@ static void section_symlink(void) "inner.txt") && !strcmp("/link.to.lowdir/inner.txt" + walk.link_guest_offset, "link.to.lowdir/inner.txt")) - ok(); + host_ok(); else - fail("an intermediate link stops the walk", - "expected CASEFOLD_SYMLINK"); + host_fail("an intermediate link stops the walk", + "expected CASEFOLD_SYMLINK"); /* A dangling link exists as a link. Asking about its target stops the walk * at the link too: whether the target is there is a question about a guest @@ -289,16 +276,16 @@ static void section_symlink(void) */ if (casefold_resolve_at(AT_FDCWD, root, "/dangling", false, out, sizeof(out), &walk) == CASEFOLD_FOUND) - ok(); + host_ok(); else - fail("dangling link exists without following", "expected found"); + host_fail("dangling link exists without following", "expected found"); if (casefold_resolve_at(AT_FDCWD, root, "/dangling", true, out, sizeof(out), &walk) == CASEFOLD_SYMLINK && walk.link_rest_offset == strlen("/dangling")) - ok(); + host_ok(); else - fail("following a dangling link stops at the link", - "expected CASEFOLD_SYMLINK with nothing left to resolve"); + host_fail("following a dangling link stops at the link", + "expected CASEFOLD_SYMLINK with nothing left to resolve"); /* A second hard link to a symlink is that same link under another name, * and it resolves by the name the caller used. The volume reports the @@ -347,15 +334,15 @@ static void section_limits(void) if (errno == ENAMETOOLONG) saw_toolong = true; else - fail("deep path", strerror(errno)); + host_fail("deep path", strerror(errno)); break; } if (casefold_escape(comp, want_leaf, sizeof(want_leaf)) < 0) { - fail("deep path", "could not spell the expected leaf"); + host_fail("deep path", "could not spell the expected leaf"); break; } if (strcmp(out + walk.leaf_offset, want_leaf)) { - fail("deep path", "last component was truncated"); + host_fail("deep path", "last component was truncated"); fprintf(stderr, " depth %d, got %s\n", depth, out + walk.leaf_offset); break; @@ -364,20 +351,20 @@ static void section_limits(void) } if (saw_toolong && last_ok > 0) - ok(); + host_ok(); else - fail("over-long host path", saw_toolong - ? "no depth resolved at all" - : "never reported ENAMETOOLONG"); + host_fail("over-long host path", saw_toolong + ? "no depth resolved at all" + : "never reported ENAMETOOLONG"); /* A caller buffer smaller than the prefix is the same class of failure. */ char tiny[8]; if (casefold_resolve_at(AT_FDCWD, root, "/plain.txt", false, tiny, sizeof(tiny), &walk) == CASEFOLD_ERROR && errno == ENAMETOOLONG) - ok(); + host_ok(); else - fail("caller buffer too small", "expected ENAMETOOLONG"); + host_fail("caller buffer too small", "expected ENAMETOOLONG"); } /* Forged getattrlist replies. No real volume produces these here, and @@ -412,34 +399,34 @@ static void section_reply_bounds(void) got = casefold_attr_stored_name(&f, sizeof(f), ref_off); if (got && !strcmp(got, "hello")) - ok(); + host_ok(); else - fail("reply bounds", "well-formed reply rejected"); + host_fail("reply bounds", "well-formed reply rejected"); f.name_ref.attr_dataoffset = -8; if (!casefold_attr_stored_name(&f, sizeof(f), ref_off)) - ok(); + host_ok(); else - fail("reply bounds", "negative attr_dataoffset accepted"); + host_fail("reply bounds", "negative attr_dataoffset accepted"); f.name_ref.attr_dataoffset = (int32_t) sizeof(f); if (!casefold_attr_stored_name(&f, sizeof(f), ref_off)) - ok(); + host_ok(); else - fail("reply bounds", "attr_dataoffset past the reply accepted"); + host_fail("reply bounds", "attr_dataoffset past the reply accepted"); f.name_ref.attr_dataoffset = name_off; f.name_ref.attr_length = 0; if (!casefold_attr_stored_name(&f, sizeof(f), ref_off)) - ok(); + host_ok(); else - fail("reply bounds", "zero attr_length accepted"); + host_fail("reply bounds", "zero attr_length accepted"); f.name_ref.attr_length = (u_int32_t) sizeof(f); if (!casefold_attr_stored_name(&f, sizeof(f), ref_off)) - ok(); + host_ok(); else - fail("reply bounds", "attr_length past the reply accepted"); + host_fail("reply bounds", "attr_length past the reply accepted"); /* The kernel claims fewer bytes than the reference needs: the documented * truncation shape, where the attribute data lies beyond what was copied. @@ -447,9 +434,10 @@ static void section_reply_bounds(void) f.name_ref.attr_length = 6; f.length = (u_int32_t) sizeof(u_int32_t); if (!casefold_attr_stored_name(&f, sizeof(f), ref_off)) - ok(); + host_ok(); else - fail("reply bounds", "reference beyond the claimed length accepted"); + host_fail("reply bounds", + "reference beyond the claimed length accepted"); /* A claimed length larger than the buffer must be capped at the buffer: * the kernel never writes more than attrBufSize, whatever length says. @@ -457,17 +445,17 @@ static void section_reply_bounds(void) f.length = (u_int32_t) sizeof(f) + 64; f.name_ref.attr_length = (u_int32_t) sizeof(f); if (!casefold_attr_stored_name(&f, sizeof(f), ref_off)) - ok(); + host_ok(); else - fail("reply bounds", "claimed length past the buffer accepted"); + host_fail("reply bounds", "claimed length past the buffer accepted"); f.length = sizeof(f); f.name_ref.attr_length = 6; memset(f.name, 'x', sizeof(f.name)); if (!casefold_attr_stored_name(&f, sizeof(f), ref_off)) - ok(); + host_ok(); else - fail("reply bounds", "name without a terminator accepted"); + host_fail("reply bounds", "name without a terminator accepted"); } /* Does this volume fold case? The resolver behaves the same either way from the @@ -498,18 +486,12 @@ static bool probe_folds(void) int main(int argc, char **argv) { - const char *base = argc > 1 ? argv[1] : getenv("TMPDIR"); char host[CASEFOLD_HOST_NAME_MAX + 1]; char p[PATH_MAX]; - if (!base || base[0] == '\0') - base = "/tmp"; - snprintf(root, sizeof(root), "%s/elfuse-walk-XXXXXX", base); - if (!mkdtemp(root)) { - fprintf(stderr, "cannot create a scratch directory in %s: %s\n", base, - strerror(errno)); + if (host_scratch_root(argv[0], "elfuse-walk", argc > 1 ? argv[1] : NULL, + root, sizeof(root)) < 0) return 1; - } snprintf(stub_sysroot, sizeof(stub_sysroot), "%s", root); volume_folds = probe_folds(); @@ -573,7 +555,5 @@ int main(int argc, char **argv) section_reply_bounds(); remove_tree(root); - printf("test-casefold-walk-host: %d passed, %d failed - %s\n", passes, - fails, fails ? "FAIL" : "PASS"); - return fails ? 1 : 0; + return host_summary("test-casefold-walk-host"); } diff --git a/tests/test-nosysroot-literal-names.c b/tests/test-nosysroot-literal-names.c deleted file mode 100644 index 6bb3f00b..00000000 --- a/tests/test-nosysroot-literal-names.c +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Escape-shaped host names are ordinary names without a sysroot - * - * Copyright 2026 elfuse contributors - * SPDX-License-Identifier: Apache-2.0 - * - * The escape encoding exists to let a case-folding sysroot hold names Linux - * keeps apart. Without --sysroot there is no sysroot and no escaping: the guest - * is looking straight at the host filesystem, where a file named ".ef=464f4f" - * is a file named ".ef=464f4f" and nothing else. Decoding it would invent a - * name the directory does not contain, and the guest would then be unable to - * open the entry under either spelling: not the name it was shown, which is - * not on disk, nor the name on disk, which it was never shown. - * - * Code under test: path_translate_dirent_name in src/syscall/path.c, reached - * from the getdents64 loop in src/syscall/fs.c. A regression shows up as a - * listing that reports a name no open() can then resolve. - * - * argv[1] is a host directory staged by the make recipe, because a test running - * without a sysroot has nowhere of its own to write. - */ - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "test-harness.h" -#include "test-util.h" - -int passes = 0, fails = 0; - -/* The escape of "FOO" on a folding sysroot. Here it must mean itself. */ -#define ESCAPE_SHAPED ".ef=464f4f" -#define DECODES_TO "FOO" - -int main(int argc, char **argv) -{ - char path[PATH_MAX]; - int fd; - - if (argc < 2) { - printf("test-nosysroot-literal-names: no directory given\n"); - return 1; - } - - printf("test-nosysroot-literal-names: literal names without a sysroot\n"); - - TEST("an escape-shaped host name is listed under its own bytes"); - EXPECT_TRUE(dir_contains(argv[1], ESCAPE_SHAPED), - "should appear as written"); - - /* The decisive one: if the listing decoded the name, the guest was shown - * DECODES_TO, which no directory entry matches. - */ - TEST("the listing does not invent the decoded name"); - EXPECT_TRUE(!dir_contains(argv[1], DECODES_TO), - "nothing on disk has that name"); - - TEST("the name the listing reported can be opened"); - snprintf(path, sizeof(path), "%s/%s", argv[1], ESCAPE_SHAPED); - fd = open(path, O_RDONLY); - EXPECT_TRUE(fd >= 0, "the listed name must resolve"); - if (fd >= 0) - close(fd); - - TEST("the decoded name resolves to nothing"); - snprintf(path, sizeof(path), "%s/%s", argv[1], DECODES_TO); - EXPECT_ERRNO(open(path, O_RDONLY), ENOENT, "should not exist"); - - SUMMARY("test-nosysroot-literal-names"); - return fails > 0 ? 1 : 0; -} diff --git a/tests/test-sysroot-absock-names.c b/tests/test-sysroot-absock-names.c index ff251f36..3119a3e6 100644 --- a/tests/test-sysroot-absock-names.c +++ b/tests/test-sysroot-absock-names.c @@ -48,24 +48,6 @@ int passes = 0, fails = 0; #define DIR_S "/sockdir" -static int bind_listener(const char *path) -{ - struct sockaddr_un sa; - int fd = socket(AF_UNIX, SOCK_STREAM, 0); - - if (fd < 0) - return -1; - memset(&sa, 0, sizeof(sa)); - sa.sun_family = AF_UNIX; - snprintf(sa.sun_path, sizeof(sa.sun_path), "%s", path); - if (bind(fd, (struct sockaddr *) &sa, sizeof(sa)) != 0 || - listen(fd, 4) != 0) { - close(fd); - return -1; - } - return fd; -} - /* Connect to @path, send @token, report what the peer heard through * @accepted on the listener side. */ @@ -87,23 +69,6 @@ static int connect_send(const char *path, char token) return fd; } -static int bind_dgram(const char *path) -{ - struct sockaddr_un sa; - int fd = socket(AF_UNIX, SOCK_DGRAM, 0); - - if (fd < 0) - return -1; - memset(&sa, 0, sizeof(sa)); - sa.sun_family = AF_UNIX; - snprintf(sa.sun_path, sizeof(sa.sun_path), "%s", path); - if (bind(fd, (struct sockaddr *) &sa, sizeof(sa)) != 0) { - close(fd); - return -1; - } - return fd; -} - static char accept_token(int listener) { char token = '?'; @@ -135,7 +100,7 @@ int main(void) */ TEST("bind creates a socket inside the sysroot"); snprintf(path, sizeof(path), "%s/My.Sock", DIR_S); - lfd = bind_listener(path); + lfd = unix_bind(path, SOCK_STREAM, 4); EXPECT_TRUE(lfd >= 0, "bind + listen"); TEST("getsockname returns the exact guest bytes"); @@ -188,8 +153,8 @@ int main(void) snprintf(lower, sizeof(lower), "%s/sock", DIR_S); snprintf(upper, sizeof(upper), "%s/Sock", DIR_S); - lfd2 = bind_listener(lower); - lfd3 = bind_listener(upper); + lfd2 = unix_bind(lower, SOCK_STREAM, 4); + lfd3 = unix_bind(upper, SOCK_STREAM, 4); if (lfd2 < 0 || lfd3 < 0) { FAIL("bind pair"); } else { @@ -231,8 +196,8 @@ int main(void) snprintf(path, sizeof(path), "%s/Recv.Sock", DIR_S); snprintf(sender, sizeof(sender), "%s/Sender.Sock", DIR_S); - rfd = bind_dgram(path); - sfd = bind_dgram(sender); + rfd = unix_bind(path, SOCK_DGRAM, 0); + sfd = unix_bind(sender, SOCK_DGRAM, 0); if (rfd < 0 || sfd < 0) { FAIL("bind dgram pair"); } else { @@ -288,7 +253,7 @@ int main(void) snprintf(path, sizeof(path), "%s/Very.Long.Mixed.Case.Directory.For.Escapes/S.sock", DIR_S); - lfd4 = bind_listener(path); + lfd4 = unix_bind(path, SOCK_STREAM, 4); if (lfd4 < 0) { FAIL("bind"); } else { @@ -375,7 +340,7 @@ int main(void) snprintf(path, sizeof(path), "%s/Very.Long.Mixed.Case.Directory.For.Escapes/F.sock", DIR_S); - lfd5 = bind_listener(path); + lfd5 = unix_bind(path, SOCK_STREAM, 4); if (lfd5 < 0) { FAIL("bind"); } else { diff --git a/tests/test-sysroot-dotdot.c b/tests/test-sysroot-dotdot.c index 584464ce..2bd79544 100644 --- a/tests/test-sysroot-dotdot.c +++ b/tests/test-sysroot-dotdot.c @@ -157,11 +157,10 @@ static void section_relative(void) close(root); } -/* An interior '..' must reach the host verbatim, so the component it pops is - * still resolved and type-checked. Only the shapes the sidecar leaves alone - * can be asserted here: on a case-folding sysroot it collapses every absolute - * path before the resolvers' spelling is used, and it owns these answers until - * it is removed. +/* An interior '..' must reach the host verbatim: the resolvers clamp '..' + * only at the guest root and spell interior ones through untouched, leaving + * the pop to the kernel's own resolution. What is asserted here is that the + * pop arrives, not how the kernel type-checks the component it pops. */ static void section_interior(void) { diff --git a/tests/test-sysroot-name-i18n.c b/tests/test-sysroot-name-i18n.c index b543a58e..d67ceca1 100644 --- a/tests/test-sysroot-name-i18n.c +++ b/tests/test-sysroot-name-i18n.c @@ -75,37 +75,14 @@ static int write_file(const char *name, const char *text) static int read_back(const char *name, char *buf, size_t bufsz) { char path[PATH_MAX]; - int fd; - ssize_t n; snprintf(path, sizeof(path), "%s/%s", DIR_I, name); - fd = open(path, O_RDONLY); - if (fd < 0) - return -1; - n = read(fd, buf, bufsz - 1); - close(fd); - if (n < 0) - return -1; - buf[n] = '\0'; - return 0; + return read_file_nul(path, buf, bufsz) < 0 ? -1 : 0; } static bool in_listing(const char *name) { - DIR *d = opendir(DIR_I); - struct dirent *de; - bool found = false; - - if (!d) - return false; - while ((de = readdir(d))) { - if (!strcmp(de->d_name, name)) { - found = true; - break; - } - } - closedir(d); - return found; + return dir_contains(DIR_I, name); } /* Create one name, read it back, and require it to appear in a listing spelled diff --git a/tests/test-sysroot-name-length.c b/tests/test-sysroot-name-length.c index 0f99e895..38dae6b9 100644 --- a/tests/test-sysroot-name-length.c +++ b/tests/test-sysroot-name-length.c @@ -84,16 +84,9 @@ static void build_utf8(char *buf, size_t bytes, const char *unit) static int create(const char *name, const char *text) { char path[PATH_MAX]; - int fd; - ssize_t n; snprintf(path, sizeof(path), "%s/%s", DIR_L, name); - fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0644); - if (fd < 0) - return -1; - n = write(fd, text, strlen(text)); - close(fd); - return n == (ssize_t) strlen(text) ? 0 : -1; + return file_write(path, text); } /* One name of a given length: create it, read it back, and require a listing diff --git a/tests/test-sysroot-name-relative.c b/tests/test-sysroot-name-relative.c index dd2304af..f26e4bf4 100644 --- a/tests/test-sysroot-name-relative.c +++ b/tests/test-sysroot-name-relative.c @@ -49,34 +49,6 @@ int passes = 0, fails = 0; #define NAME_B "Beta.Two" #define NAME_C "Gamma.Three" -static int write_at(int dirfd, const char *name, const char *text) -{ - int fd = openat(dirfd, name, O_CREAT | O_WRONLY | O_TRUNC, 0644); - ssize_t n; - - if (fd < 0) - return -1; - n = write(fd, text, strlen(text)); - close(fd); - return n == (ssize_t) strlen(text) ? 0 : -1; -} - -static int content_at(int dirfd, const char *name, const char *want) -{ - char buf[64]; - int fd = openat(dirfd, name, O_RDONLY); - ssize_t n; - - if (fd < 0) - return -1; - n = read(fd, buf, sizeof(buf) - 1); - close(fd); - if (n < 0) - return -1; - buf[n] = '\0'; - return !strcmp(buf, want) ? 0 : -1; -} - /* Outside the sysroot the guest is looking at the real host filesystem, where * elfuse owns nothing and must store names exactly as given. An absolute path * that falls through already does; a relative one has to agree, or the same @@ -102,21 +74,22 @@ static void section_outside_sysroot(const char *host_dir) EXPECT_TRUE(chdir(host_dir) == 0, "chdir"); TEST("create a mixed-case name there through a relative path"); - EXPECT_TRUE(write_at(AT_FDCWD, "Outside.Rel", "rel") == 0, "create"); + EXPECT_TRUE(file_write_at(AT_FDCWD, "Outside.Rel", "rel") == 0, "create"); TEST("create a mixed-case name there through an absolute path"); snprintf(abs, sizeof(abs), "%s/Outside.Abs", host_dir); - EXPECT_TRUE(write_at(AT_FDCWD, abs, "abs") == 0, "create"); + EXPECT_TRUE(file_write_at(AT_FDCWD, abs, "abs") == 0, "create"); /* Both spellings must reach both files: nothing was translated, so a * relative and an absolute name of the same file are the same name. */ TEST("the relative-created name opens absolutely"); snprintf(abs, sizeof(abs), "%s/Outside.Rel", host_dir); - EXPECT_TRUE(content_at(AT_FDCWD, abs, "rel") == 0, "content"); + EXPECT_TRUE(file_content_at_is(AT_FDCWD, abs, "rel") == 0, "content"); TEST("the absolute-created name opens relatively"); - EXPECT_TRUE(content_at(AT_FDCWD, "Outside.Abs", "abs") == 0, "content"); + EXPECT_TRUE(file_content_at_is(AT_FDCWD, "Outside.Abs", "abs") == 0, + "content"); TEST("chdir back to the sysroot root"); EXPECT_TRUE(chdir("/") == 0, "chdir"); @@ -195,7 +168,7 @@ static void section_openat2_no_symlinks(int dirfd) memset(longname, 'Q', 126); longname[126] = '\0'; - if (write_at(dirfd, longname, "long") != 0) { + if (file_write_at(dirfd, longname, "long") != 0) { FAIL("create"); } else { errno = 0; @@ -211,7 +184,7 @@ static void section_openat2_no_symlinks(int dirfd) TEST("openat2 RESOLVE_NO_SYMLINKS opens a 255-byte escaped name"); memset(longname, 'Q', 255); longname[255] = '\0'; - if (write_at(dirfd, longname, "long") != 0) { + if (file_write_at(dirfd, longname, "long") != 0) { FAIL("create"); } else { errno = 0; @@ -240,9 +213,9 @@ static void section_trailing_slash(void) int fd; TEST("stage a file and a directory whose names need escaping"); - EXPECT_TRUE(write_at(AT_FDCWD, DIR_V "/Slash.File", "f") == 0 && + EXPECT_TRUE(file_write_at(AT_FDCWD, DIR_V "/Slash.File", "f") == 0 && (mkdir(DIR_V "/Slash.Dir", 0755) == 0 || errno == EEXIST) && - write_at(AT_FDCWD, DIR_V "/slashfile", "g") == 0, + file_write_at(AT_FDCWD, DIR_V "/slashfile", "g") == 0, "stage"); TEST("open of an escaped file with a trailing slash is ENOTDIR"); @@ -307,8 +280,8 @@ static void section_below_non_directory(void) struct stat st; TEST("stage regular files to resolve below"); - EXPECT_TRUE(write_at(AT_FDCWD, DIR_V "/notdirfile", "f") == 0 && - write_at(AT_FDCWD, DIR_V "/Not.Dir.File", "g") == 0, + EXPECT_TRUE(file_write_at(AT_FDCWD, DIR_V "/notdirfile", "f") == 0 && + file_write_at(AT_FDCWD, DIR_V "/Not.Dir.File", "g") == 0, "stage"); TEST("stat below a fold-stable regular file is ENOTDIR"); @@ -353,13 +326,13 @@ int main(int argc, char **argv) /* Created absolutely, reopened relatively. */ snprintf(abs, sizeof(abs), "%s/%s", DIR_V, NAME_A); TEST("create through an absolute path"); - EXPECT_TRUE(write_at(AT_FDCWD, abs, "abs") == 0, "create"); + EXPECT_TRUE(file_write_at(AT_FDCWD, abs, "abs") == 0, "create"); TEST("chdir into the directory"); EXPECT_TRUE(chdir(DIR_V) == 0, "chdir"); TEST("the same file opens through a cwd-relative name"); - EXPECT_TRUE(content_at(AT_FDCWD, NAME_A, "abs") == 0, + EXPECT_TRUE(file_content_at_is(AT_FDCWD, NAME_A, "abs") == 0, "relative spelling reached a different file"); /* An O_EXCL create of a name that already exists must fail, whichever way @@ -372,11 +345,11 @@ int main(int argc, char **argv) /* Created relatively, reopened absolutely. */ TEST("create through a cwd-relative name"); - EXPECT_TRUE(write_at(AT_FDCWD, NAME_B, "rel") == 0, "create"); + EXPECT_TRUE(file_write_at(AT_FDCWD, NAME_B, "rel") == 0, "create"); snprintf(abs, sizeof(abs), "%s/%s", DIR_V, NAME_B); TEST("the same file opens through an absolute path"); - EXPECT_TRUE(content_at(AT_FDCWD, abs, "rel") == 0, + EXPECT_TRUE(file_content_at_is(AT_FDCWD, abs, "rel") == 0, "absolute spelling reached a different file"); /* The directory holds one entry per guest name and no more: a spelling that @@ -394,15 +367,15 @@ int main(int argc, char **argv) "open dirfd"); TEST("create through a dirfd"); - EXPECT_TRUE(write_at(dirfd, NAME_C, "dfd") == 0, "create"); + EXPECT_TRUE(file_write_at(dirfd, NAME_C, "dfd") == 0, "create"); snprintf(abs, sizeof(abs), "%s/%s", DIR_V, NAME_C); TEST("the dirfd-created file opens absolutely"); - EXPECT_TRUE(content_at(AT_FDCWD, abs, "dfd") == 0, + EXPECT_TRUE(file_content_at_is(AT_FDCWD, abs, "dfd") == 0, "dirfd spelling reached a different file"); TEST("the absolute-created file opens through the dirfd"); - EXPECT_TRUE(content_at(dirfd, NAME_A, "abs") == 0, + EXPECT_TRUE(file_content_at_is(dirfd, NAME_A, "abs") == 0, "dirfd lookup reached a different file"); TEST("three names, three entries"); @@ -419,7 +392,8 @@ int main(int argc, char **argv) EXPECT_TRUE(renameat(dirfd, NAME_C, dirfd, "Delta.Four") == 0, "renameat"); TEST("the renamed file opens absolutely under its new name"); snprintf(abs, sizeof(abs), "%s/Delta.Four", DIR_V); - EXPECT_TRUE(content_at(AT_FDCWD, abs, "dfd") == 0, "renamed content"); + EXPECT_TRUE(file_content_at_is(AT_FDCWD, abs, "dfd") == 0, + "renamed content"); TEST("mkdirat through the dirfd"); EXPECT_TRUE(mkdirat(dirfd, "Sub.Dir", 0755) == 0, "mkdirat"); @@ -455,11 +429,11 @@ int main(int argc, char **argv) mkdirat(dirfd, "Walk.Dir", 0755) == 0 && (sub = openat(dirfd, "Walk.Dir", O_RDONLY | O_DIRECTORY)) >= 0 && - write_at(sub, "Leaf.File", "leaf") == 0 && + file_write_at(sub, "Leaf.File", "leaf") == 0 && mkdirat(dirfd, "walkdir", 0755) == 0 && (plain = openat(dirfd, "walkdir", O_RDONLY | O_DIRECTORY)) >= 0 && - write_at(plain, "leaf", "plain") == 0 && + file_write_at(plain, "leaf", "plain") == 0 && symlinkat("walkdir", dirfd, "Walk.Link") == 0 && symlinkat("/proc", dirfd, "Cross.Link") == 0, "stage"); diff --git a/tests/test-sysroot-outside-names.c b/tests/test-sysroot-outside-names.c index 5a2feec5..40d7db6a 100644 --- a/tests/test-sysroot-outside-names.c +++ b/tests/test-sysroot-outside-names.c @@ -24,6 +24,12 @@ * recipe asserts host-side that the control file the guest creates inside * the sysroot was stored escaped, so a pass on a byte-exact volume cannot be * vacuous. + * + * argv[2] = "nosysroot" runs the same assertions with no sysroot configured + * at all, where nothing decodes anywhere: the other half of the scoping + * contract, reached through path_dirent_dir_holds_escapes, which refuses + * every directory once there is no sysroot to snapshot. The in-sysroot + * control is skipped, there being no sysroot to hold it. */ #include @@ -44,25 +50,6 @@ int passes = 0, fails = 0; #define ESCAPE_SHAPED ".ef=464f4f" #define DECODES_TO "FOO" -/* dir_contains answers membership only; the collapse regression needs the - * count, because the decoded spelling of one entry equals the literal - * spelling of another. - */ -static int dir_count_name(const char *dir, const char *name) -{ - DIR *d = opendir(dir); - struct dirent *de; - int n = 0; - - if (!d) - return -1; - while ((de = readdir(d))) - if (!strcmp(de->d_name, name)) - n++; - closedir(d); - return n; -} - int main(int argc, char **argv) { char path[PATH_MAX]; @@ -73,9 +60,17 @@ int main(int argc, char **argv) return 1; } - printf("test-sysroot-outside-names: literal names outside the sysroot\n"); + bool with_sysroot = !(argc > 2 && !strcmp(argv[2], "nosysroot")); + /* Both legs run the same assertions, so the summary has to say which one + * reported them. + */ + const char *lane = with_sysroot ? "test-sysroot-outside-names" + : "test-sysroot-outside-names (nosysroot)"; + + printf("%s: literal names %s\n", lane, + with_sysroot ? "outside the sysroot" : "without a sysroot"); - TEST("an escape-shaped name outside the sysroot lists as its own bytes"); + TEST("an escape-shaped name lists as its own bytes"); EXPECT_TRUE(dir_contains(argv[1], ESCAPE_SHAPED), "should appear as written"); @@ -118,15 +113,17 @@ int main(int argc, char **argv) * folds and the sysroot scope still decodes, proving the assertions * above did not pass merely because nothing was escaping anywhere. */ - TEST("a control name inside the sysroot still escapes"); - fd = open("/Ctrl", O_CREAT | O_WRONLY, 0644); - EXPECT_TRUE(fd >= 0, "create /Ctrl in the sysroot"); - if (fd >= 0) { - close(fd); - EXPECT_TRUE(dir_contains("/", "Ctrl"), - "the sysroot listing still decodes"); + if (with_sysroot) { + TEST("a control name inside the sysroot still escapes"); + fd = open("/Ctrl", O_CREAT | O_WRONLY, 0644); + EXPECT_TRUE(fd >= 0, "create /Ctrl in the sysroot"); + if (fd >= 0) { + close(fd); + EXPECT_TRUE(dir_contains("/", "Ctrl"), + "the sysroot listing still decodes"); + } } - SUMMARY("test-sysroot-outside-names"); + SUMMARY(lane); return fails > 0 ? 1 : 0; } diff --git a/tests/test-sysroot-tmp-remove.c b/tests/test-sysroot-tmp-remove.c index 768d0352..e4263f89 100644 --- a/tests/test-sysroot-tmp-remove.c +++ b/tests/test-sysroot-tmp-remove.c @@ -32,7 +32,7 @@ * The harness then checks placement from the host side: the staging directory * still holds exactly those three entries, and the sysroot gained the * redirected directory. It asserts at directory granularity because a - * case-insensitive sysroot stores leaves under sidecar-mangled names, so the + * case-insensitive sysroot may store a leaf under its escaped spelling, so the * guest side proves the file itself by reading its bytes back. * * The invariants are one-directional on purpose: a path the guest cannot see at diff --git a/tests/test-util.h b/tests/test-util.h index f37633e8..5975a830 100644 --- a/tests/test-util.h +++ b/tests/test-util.h @@ -13,29 +13,17 @@ #include #include #include +#include #include #include +#include #include +#include #include +#include "linux-openat2.h" #include "raw-syscall.h" -/* openat2(2) scaffolding for tests that drive the second walker directly. - * Static libcs predate the syscall's wrapper and uapi header, so the number, - * the struct, and the resolve bits have to be spelled out by hand; they live - * here once so every test that needs them agrees on the ABI. - */ -#ifndef SYS_openat2 -#define SYS_openat2 437 -#endif - -struct open_how { - unsigned long long flags, mode, resolve; -}; - -#define RESOLVE_NO_XDEV 0x01 -#define RESOLVE_NO_SYMLINKS 0x04 - static inline ssize_t read_fd_all_nul(int fd, char *buf, size_t bufsz) { if (bufsz == 0) @@ -137,6 +125,38 @@ static inline int file_content_is(const char *path, const char *want) return strcmp(buf, want) ? -1 : 0; } +/* Dirfd-relative siblings of file_write and file_content_is, for lanes that + * address fixtures through a descriptor rather than a full path. + */ +static inline int file_write_at(int dirfd, const char *name, const char *text) +{ + int fd = openat(dirfd, name, O_CREAT | O_WRONLY | O_TRUNC, 0644); + int rc; + + if (fd < 0) + return -1; + rc = write_fd_all(fd, text, strlen(text)); + close(fd); + return rc; +} + +static inline int file_content_at_is(int dirfd, + const char *name, + const char *want) +{ + char buf[256]; + int fd = openat(dirfd, name, O_RDONLY); + ssize_t n; + + if (fd < 0) + return -1; + n = read_fd_all_nul(fd, buf, sizeof(buf)); + close(fd); + if (n < 0) + return -1; + return strcmp(buf, want) ? -1 : 0; +} + /* 0 when @path begins with @want. For fixtures staged by a shell recipe, which * appends a newline that the assertion is not about. */ @@ -166,6 +186,10 @@ static inline int dir_entry_count(const char *dir) return n; } +/* Membership only; an unopenable @dir reads as absent, so a negative-only + * assertion must pair with a positive one or use dir_count_name, whose -1 + * keeps "cannot read" distinct from "not present". + */ static inline bool dir_contains(const char *dir, const char *name) { DIR *d = opendir(dir); @@ -184,6 +208,59 @@ static inline bool dir_contains(const char *dir, const char *name) return found; } +/* Occurrences of @name in @dir, or -1 if it cannot be opened. dir_contains + * answers membership only; a collapse regression needs the count, because the + * decoded spelling of one entry can equal the literal spelling of another. + */ +static inline int dir_count_name(const char *dir, const char *name) +{ + DIR *d = opendir(dir); + struct dirent *de; + int n = 0; + + if (!d) + return -1; + while ((de = readdir(d))) + if (!strcmp(de->d_name, name)) + n++; + closedir(d); + return n; +} + +/* Bind a pathname AF_UNIX socket of @type at @path, listening with @backlog + * when it is positive. Returns the descriptor, or -1 with the socket closed + * and no socket file created by this call left behind. + * An over-long @path is truncated into sun_path exactly as the lanes did by + * hand; the bind then addresses the truncated name, which the caller's own + * assertions catch. + */ +static inline int unix_bind(const char *path, int type, int backlog) +{ + struct sockaddr_un sa; + int fd = socket(AF_UNIX, type, 0); + + if (fd < 0) + return -1; + memset(&sa, 0, sizeof(sa)); + sa.sun_family = AF_UNIX; + snprintf(sa.sun_path, sizeof(sa.sun_path), "%s", path); + if (bind(fd, (struct sockaddr *) &sa, sizeof(sa)) != 0) { + close(fd); + return -1; + } + if (backlog > 0 && listen(fd, backlog) != 0) { + /* bind created the file, so a rerun would get EADDRINUSE from it. + * Only this branch unlinks: on a bind failure the file is not this + * call's, and removing it would break whoever owns it. sun_path, + * not @path, because the file carries the truncated name. + */ + unlink(sa.sun_path); + close(fd); + return -1; + } + return fd; +} + static inline void test_unreachable(void) { abort();