From 684654f4c6938c967a2c4216e381948784e4ff38 Mon Sep 17 00:00:00 2001 From: Jim Huang Date: Thu, 14 May 2026 22:10:05 +0800 Subject: [PATCH] Add syscall coverage audit and direct-syscall smoke suite scripts/check-syscall-coverage.py parses src/syscall/dispatch.tbl and scans tests/ for direct or aliased references to every entry. C sources are matched on call-shape (name followed by an opening paren) or the SYS_/__NR_ macro forms; non-C sources require the explicit macro form so that coreutils applet invocations in the shell suites (run sync 0, run kill ..., run chroot ...) cannot falsely cover the like-named syscalls. ALIASES maps the pread64/pwrite64 dispatch names to the pread/pwrite libc wrappers; INDIRECT_COVERAGE documents the xattr family, rt_sig{return,suspend,pending}, ptrace, chroot, truncate, exit_group, and get_robust_list, which are exercised only through structural or out-of-band paths. mk/tests.mk runs the script as a check-syscall-coverage target that make check depends on. src/syscall/syscall.c::sc_memfd_create rejects a NULL name pointer with EFAULT, probes the first byte of the guest pointer to surface EFAULT before the temp file is created, and reads the flag bits through named LINUX_MFD_CLOEXEC and LINUX_MFD_ALLOW_SEALING constants in src/syscall/abi.h instead of the prior raw bitmask arithmetic. Unknown MFD_* bits continue to be accepted silently to match the behavior the existing fd-lifecycle tests expect. --- Makefile | 13 + mk/config.mk | 6 +- mk/tests.mk | 12 +- scripts/check-syscall-coverage.py | 150 +++++++ src/syscall/abi.h | 4 + src/syscall/syscall.c | 16 +- tests/driver.sh | 47 ++- tests/lib/coreutils-common.sh | 90 +++++ tests/lib/coreutils-suite.sh | 289 +++++++++++++ tests/lib/test-runner.sh | 14 + tests/manifest.txt | 5 + tests/test-coreutils-smoke.sh | 89 ---- tests/test-coreutils.sh | 166 +------- tests/test-dynamic-coreutils.sh | 163 +------- tests/test-fd-lifecycle.c | 23 +- tests/test-lowbase-mem.c | 101 +++++ tests/test-syscall-smoke.c | 648 ++++++++++++++++++++++++++++++ 17 files changed, 1427 insertions(+), 409 deletions(-) create mode 100644 scripts/check-syscall-coverage.py create mode 100644 tests/lib/coreutils-suite.sh delete mode 100644 tests/test-coreutils-smoke.sh create mode 100644 tests/test-lowbase-mem.c create mode 100644 tests/test-syscall-smoke.c diff --git a/Makefile b/Makefile index 276b933..6b7bee6 100644 --- a/Makefile +++ b/Makefile @@ -171,6 +171,19 @@ $(BUILD_DIR)/test-fork-lowbase: tests/test-fork-lowbase.c | $(BUILD_DIR) $(Q)$(CROSS_COMPILE)gcc -D_GNU_SOURCE -static -O2 -no-pie \ -Wl,-Ttext-segment=0x200000 -o $@ $< +# test-lowbase-mem variants must be non-PIE ET_EXEC binaries linked below +# ELF_DEFAULT_BASE so mprotect/munmap exercise the old low-address reject +# 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 \ + -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 \ + -Wl,-Ttext-segment=0x300000 -o $@ $< + endif include mk/tests.mk diff --git a/mk/config.mk b/mk/config.mk index fbabc9e..0c18aa9 100644 --- a/mk/config.mk +++ b/mk/config.mk @@ -16,6 +16,8 @@ endif # Exclude native macOS test files from cross-compilation NATIVE_TESTS := tests/test-multi-vcpu.c tests/test-rwx.c +SPECIAL_TEST_SRCS := tests/test-lowbase-mem.c +SPECIAL_TEST_BINS := $(BUILD_DIR)/test-lowbase-mem-200000 $(BUILD_DIR)/test-lowbase-mem-300000 ifdef GUEST_TEST_BINARIES TEST_DIR := $(GUEST_TEST_BINARIES)/bin @@ -23,9 +25,9 @@ ifdef GUEST_TEST_BINARIES TEST_HELLO_DEP := else TEST_DIR := $(BUILD_DIR) - TEST_C_SRCS := $(filter-out $(NATIVE_TESTS),$(wildcard tests/*.c)) + TEST_C_SRCS := $(filter-out $(NATIVE_TESTS) $(SPECIAL_TEST_SRCS),$(wildcard tests/*.c)) TEST_C_BINS := $(patsubst tests/%.c,$(BUILD_DIR)/%,$(TEST_C_SRCS)) - TEST_DEPS := $(BUILD_DIR)/test-hello $(TEST_C_BINS) + TEST_DEPS := $(BUILD_DIR)/test-hello $(TEST_C_BINS) $(SPECIAL_TEST_BINS) TEST_HELLO_DEP := $(BUILD_DIR)/test-hello endif diff --git a/mk/tests.mk b/mk/tests.mk index cd86dc3..b7c60c3 100644 --- a/mk/tests.mk +++ b/mk/tests.mk @@ -1,6 +1,6 @@ # Test targets -.PHONY: test-hello test-all check test-gdbstub test-coreutils test-busybox \ +.PHONY: test-hello test-all check check-syscall-coverage test-gdbstub test-coreutils test-busybox \ test-static-bins \ test-dynamic test-dynamic-coreutils test-glibc-dynamic \ test-glibc-coreutils test-perf \ @@ -15,8 +15,12 @@ test-hello: $(ELFUSE_BIN) $(TEST_HELLO_DEP) @printf "$(BLUE)▸ Running$(RESET) test-hello\n" $(ELFUSE_BIN) $(TEST_DIR)/test-hello +## Verify dispatch.tbl coverage of the kernel-supported syscall set +check-syscall-coverage: + @python3 scripts/check-syscall-coverage.py + ## Run the unit test suite plus busybox applet validation -check: $(ELFUSE_BIN) $(TEST_DEPS) +check: $(ELFUSE_BIN) $(TEST_DEPS) check-syscall-coverage @bash tests/driver.sh -e $(ELFUSE_BIN) -d $(TEST_DIR) -v @printf "\n$(BLUE)━━━ proctitle low-stack regression ━━━$(RESET)\n" @$(MAKE) --no-print-directory test-proctitle-low-stack @@ -124,7 +128,7 @@ test-coreutils: $(ELFUSE_BIN) exit 1; \ fi @if [ "$(COREUTILS_BIN)" = "$(FIXTURES_DIR)/aarch64-musl/dyn-bin" ]; then \ - bash tests/test-coreutils-smoke.sh $(ELFUSE_BIN) $(COREUTILS_BIN) $(SYSROOT_DIR); \ + COREUTILS_PROFILE=smoke bash tests/test-coreutils.sh $(ELFUSE_BIN) $(COREUTILS_BIN) $(SYSROOT_DIR); \ elif [ -n "$(SYSROOT_DIR)" ] && [ -d "$(SYSROOT_DIR)" ]; then \ bash tests/test-coreutils.sh $(ELFUSE_BIN) $(COREUTILS_BIN) $(SYSROOT_DIR); \ else \ @@ -270,7 +274,7 @@ test-dynamic-coreutils: $(ELFUSE_BIN) exit 1; \ fi @if [ "$(DYNAMIC_COREUTILS_BIN)" = "$(FIXTURES_DIR)/aarch64-musl/dyn-bin" ]; then \ - bash tests/test-coreutils-smoke.sh $(ELFUSE_BIN) $(DYNAMIC_COREUTILS_BIN) $(SYSROOT_DIR); \ + COREUTILS_PROFILE=smoke bash tests/test-dynamic-coreutils.sh $(ELFUSE_BIN) $(SYSROOT_DIR) $(DYNAMIC_COREUTILS_BIN); \ else \ bash tests/test-dynamic-coreutils.sh $(ELFUSE_BIN) $(SYSROOT_DIR) $(DYNAMIC_COREUTILS_BIN); \ fi diff --git a/scripts/check-syscall-coverage.py b/scripts/check-syscall-coverage.py new file mode 100644 index 0000000..e1610dc --- /dev/null +++ b/scripts/check-syscall-coverage.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""Best-effort syscall coverage audit for dispatch.tbl against tests/.""" + +from __future__ import annotations + +import pathlib +import re +import sys + +ROOT = pathlib.Path(__file__).resolve().parent.parent +DISPATCH = ROOT / "src" / "syscall" / "dispatch.tbl" +TESTS = ROOT / "tests" + +ENTRY_RE = re.compile(r"^(SYS_[A-Za-z0-9_]+)\s+(sc_[A-Za-z0-9_]+)\s+([01])$") + +ALIASES: dict[str, set[str]] = { + "faccessat": {"faccessat2"}, + "renameat": {"renameat2"}, + # Linux syscall name vs. libc wrapper name. On 64-bit aarch64 each + # entry on the left is the dispatch.tbl entry; the entries on the + # right are libc function names that route through that syscall. + "pread64": {"pread"}, + "pwrite64": {"pwrite"}, + "epoll_pwait": {"epoll_wait"}, + "eventfd2": {"eventfd"}, + "rt_sigaction": {"sigaction"}, + "rt_sigprocmask": {"sigprocmask"}, + "signalfd4": {"signalfd"}, +} + +INDIRECT_COVERAGE: dict[str, str] = { + "getxattr": "Covered indirectly through xattr plumbing and O_PATH rejection paths.", + "lgetxattr": "Symlink xattr semantics are filesystem-sensitive; audit via fs-xattr code and negative-path tests.", + "lsetxattr": "Symlink xattr semantics are filesystem-sensitive; audit via fs-xattr code and negative-path tests.", + "listxattr": "Covered indirectly through xattr plumbing; success-path coverage is filesystem-dependent.", + "llistxattr": "Symlink xattr list semantics are filesystem-sensitive; retained as indirect coverage.", + "flistxattr": "Covered indirectly through xattr plumbing and fd-based xattr checks.", + "fgetxattr": "Covered indirectly through xattr plumbing and fd-based xattr checks.", + "lremovexattr": "Symlink xattr semantics are filesystem-sensitive; retained as indirect coverage.", + "rt_sigsuspend": "Signal suspension is exercised by higher-level signal tests; direct raw coverage is timing-sensitive.", + "rt_sigpending": "Signal pending state is exercised indirectly by the signal suite.", + "ptrace": "Covered by debugger integration via tests/test-gdbstub.sh.", + "chroot": "Exercised only by the dynamic coreutils shell suite via the chroot(8) applet; the syscall itself has no dedicated C test (requires elevated privilege).", + "truncate": "Only ftruncate(2) is exercised directly; path-based truncate is exercised by coreutils 'truncate' applet in shell suites.", + "rt_sigreturn": "Kernel-only return-from-handler trampoline; invoked implicitly by every signal handler exit. No userspace callers.", + "exit_group": "Invoked implicitly by glibc/musl _exit() and exit(); every test process exits through this syscall.", + "get_robust_list": "Pthread-internal: glibc may set/get a robust-list pointer transparently during thread setup; rarely called directly by application code.", + "set_robust_list": "Pthread-internal: glibc and musl issue set_robust_list during thread bring-up via a path that the audit corpus does not call directly.", + "readlinkat": "Exercised indirectly through libc readlink() and the proc/openat symlink-resolution paths in test-procfs-exec; no direct readlinkat() call in C tests.", + "faccessat": "Exercised indirectly through libc access() and the coreutils suite (test, ls, cp); faccessat2 has no direct call-shape match either.", +} + + +def load_dispatch_names() -> list[str]: + names: list[str] = [] + for line in DISPATCH.read_text(encoding="utf-8").splitlines(): + match = ENTRY_RE.match(line.strip()) + if match: + names.append(match.group(1)[4:]) + return names + + +C_SUFFIXES = (".c", ".h") + +_BLOCK_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL) +_LINE_COMMENT = re.compile(r"//[^\n]*") + + +def strip_c_comments(text: str) -> str: + """Drop C block and line comments. Required before the call-shape + regex below so that mentions like "// TODO: test sync(2)" cannot + falsely cover a syscall. + """ + text = _BLOCK_COMMENT.sub(" ", text) + text = _LINE_COMMENT.sub(" ", text) + return text + + +def load_test_corpora() -> tuple[str, str]: + """Return (c_corpus, other_corpus). Splitting matters because shell + scripts that invoke coreutils applets ("run sync 0", "run kill ...") + would otherwise falsely cover the like-named syscalls. C corpus is + fed through strip_c_comments() so commented-out syscalls cannot + claim coverage either. + """ + c_chunks: list[str] = [] + other_chunks: list[str] = [] + for path in sorted(TESTS.rglob("*")): + if not path.is_file(): + continue + text = path.read_text(encoding="utf-8", errors="ignore") + if path.suffix in C_SUFFIXES: + c_chunks.append(strip_c_comments(text)) + else: + other_chunks.append(text) + return "\n".join(c_chunks), "\n".join(other_chunks) + + +def has_direct_reference(name: str, c_corpus: str, other_corpus: str) -> bool: + # C: require call-shape ("name(") or an explicit syscall-number macro. + # That covers libc wrappers (open(...), read(...), ...) and direct + # syscall(SYS_*, ...) uses, while rejecting bare-word occurrences in + # comments, TEST() labels, and error messages like FAIL("child sync recv"). + # Non-C corpus (shell, Python): only count explicit syscall-number + # macros. Coreutils applet names share words with syscalls (sync, kill, + # chroot, chmod) and "name(" rarely makes sense in those files anyway. + c_patterns = [ + rf"\b{name}\s*\(", + rf"\bSYS_{name}\b", + rf"\b__NR_{name}\b", + ] + other_patterns = [ + rf"\bSYS_{name}\b", + rf"\b__NR_{name}\b", + ] + if any(re.search(p, c_corpus) for p in c_patterns): + return True + return any(re.search(p, other_corpus) for p in other_patterns) + + +def main() -> int: + c_corpus, other_corpus = load_test_corpora() + missing: list[str] = [] + + for name in load_dispatch_names(): + if has_direct_reference(name, c_corpus, other_corpus): + continue + if any( + has_direct_reference(alias, c_corpus, other_corpus) + for alias in ALIASES.get(name, set()) + ): + continue + if name in INDIRECT_COVERAGE: + continue + missing.append(name) + + if missing: + print("Uncovered syscalls in dispatch.tbl:", file=sys.stderr) + for name in missing: + print(f" - {name}", file=sys.stderr) + return 1 + + print("syscall coverage audit: PASS") + for name, reason in sorted(INDIRECT_COVERAGE.items()): + print(f" indirect {name}: {reason}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/syscall/abi.h b/src/syscall/abi.h index 0655a18..a0ab74a 100644 --- a/src/syscall/abi.h +++ b/src/syscall/abi.h @@ -638,6 +638,10 @@ typedef struct { #define LINUX_F_SEAL_WRITE 0x0008 #define LINUX_F_SEAL_FUTURE_WRITE 0x0010 +/* memfd_create flags (MFD_*). */ +#define LINUX_MFD_CLOEXEC 0x0001U +#define LINUX_MFD_ALLOW_SEALING 0x0002U + /* fcntl sealing commands */ #define LINUX_F_ADD_SEALS 1033 #define LINUX_F_GET_SEALS 1034 diff --git a/src/syscall/syscall.c b/src/syscall/syscall.c index e8f31db..b0b15e1 100644 --- a/src/syscall/syscall.c +++ b/src/syscall/syscall.c @@ -1276,13 +1276,20 @@ static int64_t sc_memfd_create(guest_t *g, uint64_t x5, bool verbose) { - (void) g; - (void) x0; (void) x2; (void) x3; (void) x4; (void) x5; (void) verbose; + if (!x0) + return -LINUX_EFAULT; + + const unsigned int flags = (unsigned int) x1; + + char first = '\0'; + if (guest_read_small(g, x0, &first, sizeof(first)) < 0) + return -LINUX_EFAULT; + char template[] = "/tmp/elfuse-memfd-XXXXXX"; int fd = mkstemp(template); if (fd < 0) @@ -1293,9 +1300,10 @@ static int64_t sc_memfd_create(guest_t *g, close(fd); return linux_errno(); } - if ((int) x1 & 1) + if (flags & LINUX_MFD_CLOEXEC) fd_table[gfd].linux_flags |= LINUX_O_CLOEXEC; - fd_table[gfd].seals = ((int) x1 & 2) ? 0 : LINUX_F_SEAL_SEAL; + fd_table[gfd].seals = + (flags & LINUX_MFD_ALLOW_SEALING) ? 0 : LINUX_F_SEAL_SEAL; return gfd; } diff --git a/tests/driver.sh b/tests/driver.sh index effeba8..6d3a0b4 100755 --- a/tests/driver.sh +++ b/tests/driver.sh @@ -27,6 +27,7 @@ FILTER="" LIST_ONLY=0 VERBOSE=0 TAP=0 +ALLOW_MISSING_BINARIES="${ALLOW_MISSING_BINARIES:-auto}" usage() { @@ -91,6 +92,32 @@ case "$TESTDIR" in *) TESTDIR_ABS="$REPO_ROOT/$TESTDIR" ;; esac +# Canonicalize before the auto-policy comparison so that equivalent paths +# (./build, symlinked build dir, trailing-slash) still resolve to the +# default-strict branch instead of silently flipping into allow-missing +# mode. If the dir does not exist yet, fall back to the raw string; the +# per-test "not built" check still fires later. +canonicalize() +{ + if [ -d "$1" ]; then + (cd "$1" && pwd -P) + else + printf '%s' "$1" + fi +} + +if [ "$ALLOW_MISSING_BINARIES" = "auto" ]; then + testdir_canon=$(canonicalize "$TESTDIR_ABS") + build_canon=$(canonicalize "$REPO_ROOT/build") + bin_canon=$(canonicalize "$REPO_ROOT/build/bin") + if [ "$testdir_canon" = "$build_canon" ] \ + || [ "$testdir_canon" = "$bin_canon" ]; then + ALLOW_MISSING_BINARIES=0 + else + ALLOW_MISSING_BINARIES=1 + fi +fi + if [ ! -f "$TEST_LIST" ]; then echo "error: $TEST_LIST not found" >&2 exit 1 @@ -271,16 +298,30 @@ for i in "${filtered_idx[@]}"; do done if [ ! -f "$binary" ]; then + if [ "$ALLOW_MISSING_BINARIES" -eq 1 ]; then + if [ "$TAP" -eq 1 ]; then + echo "ok $test_num - $name # SKIP binary not found" + else + if [ "$section" != "$prev_section" ]; then + printf "%s\n" "$section" + prev_section="$section" + fi + report_case skip "$name" "" + fi + skip=$((skip + 1)) + continue + fi + if [ "$TAP" -eq 1 ]; then - echo "ok $test_num - $name # SKIP binary not found" + echo "not ok $test_num - $name # missing binary: $binary" else if [ "$section" != "$prev_section" ]; then printf "%s\n" "$section" prev_section="$section" fi - report_case skip "$name" "" + report_case fail "$name" " (missing binary)" fi - skip=$((skip + 1)) + fail=$((fail + 1)) continue fi diff --git a/tests/lib/coreutils-common.sh b/tests/lib/coreutils-common.sh index 700446c..9392fbc 100644 --- a/tests/lib/coreutils-common.sh +++ b/tests/lib/coreutils-common.sh @@ -28,6 +28,96 @@ coreutils_populate_fixtures() ln -s "$tmpdir/hello.txt" "$tmpdir/symlink.txt" } +coreutils_file_mode() +{ + if stat -f '%Lp' -- "$1" > /dev/null 2>&1; then + stat -f '%Lp' -- "$1" + else + stat -c '%a' -- "$1" + fi +} + +coreutils_file_size() +{ + if stat -f '%z' -- "$1" > /dev/null 2>&1; then + stat -f '%z' -- "$1" + else + stat -c '%s' -- "$1" + fi +} + +coreutils_assert_exists() +{ + local label="${1:?missing label}" path="${2:?missing path}" + check_host "$label" test -e "$path" +} + +coreutils_assert_not_exists() +{ + local label="${1:?missing label}" path="${2:?missing path}" + check_host "$label" test ! -e "$path" +} + +coreutils_assert_fifo() +{ + local label="${1:?missing label}" path="${2:?missing path}" + check_host "$label" test -p "$path" +} + +coreutils_assert_symlink_target() +{ + local label="${1:?missing label}" path="${2:?missing path}" expected="${3:?missing expected}" + local got + got=$(readlink -- "$path" 2> /dev/null || true) + if [ "$got" = "$expected" ]; then + test_report ok "$label" + pass=$((pass + 1)) + else + test_report fail "$label" + fail=$((fail + 1)) + fi +} + +coreutils_assert_file_equals() +{ + local label="${1:?missing label}" path="${2:?missing path}" expected="${3:?missing expected}" + check_host "$label" cmp -s -- "$path" "$expected" +} + +coreutils_assert_mode() +{ + local label="${1:?missing label}" path="${2:?missing path}" expected="${3:?missing expected}" + local got + got=$(coreutils_file_mode "$path" 2> /dev/null || true) + if [ "$got" = "$expected" ]; then + test_report ok "$label" + pass=$((pass + 1)) + else + test_report fail "$label" + fail=$((fail + 1)) + fi +} + +coreutils_assert_size() +{ + local label="${1:?missing label}" path="${2:?missing path}" expected="${3:?missing expected}" + local got + got=$(coreutils_file_size "$path" 2> /dev/null || true) + if [ "$got" = "$expected" ]; then + test_report ok "$label" + pass=$((pass + 1)) + else + test_report fail "$label" + fail=$((fail + 1)) + fi +} + +coreutils_assert_contains() +{ + local label="${1:?missing label}" path="${2:?missing path}" pattern="${3:?missing pattern}" + check_host "$label" grep -qE -- "$pattern" "$path" +} + coreutils_print_suite_header() { local title="${1:?missing title}" diff --git a/tests/lib/coreutils-suite.sh b/tests/lib/coreutils-suite.sh new file mode 100644 index 0000000..6d68384 --- /dev/null +++ b/tests/lib/coreutils-suite.sh @@ -0,0 +1,289 @@ +#!/usr/bin/env bash +# Shared GNU coreutils suite definitions +# +# Copyright 2026 elfuse contributors +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +coreutils_suite_basic_text() +{ + coreutils_print_section "Output / text utilities" + run_check cat "hello world" "$TMPDIR/hello.txt" + run_check echo "hello" "hello" + run_check printf "42" "%d" 42 + run_timeout 2 yes 124 + run_check head "line1" "$TMPDIR/lines.txt" + run_check tail "line5" "$TMPDIR/lines.txt" + run_check wc "5" "-l" "$TMPDIR/lines.txt" + run_check sort "^apple" "$TMPDIR/unsorted.txt" + run_check uniq "aaa" "$TMPDIR/dups.txt" + run_check cut "b" "-d:" "-f2" "$TMPDIR/delim.txt" + run_pipe tr "HELLO" "hello" "a-z" "A-Z" + run_check paste "one" "$TMPDIR/tabs.txt" + run_check nl "hello" "$TMPDIR/hello.txt" + run_check od "0000000" "-c" "$TMPDIR/hello.txt" +} + +coreutils_suite_extended_text() +{ + run_check expand "one" "$TMPDIR/tabs.txt" + run_check unexpand "one" "$TMPDIR/tabs.txt" + run_check fmt "hello world" "$TMPDIR/hello.txt" + run_check fold "hello" "-w5" "$TMPDIR/hello.txt" + run_check pr "hello" "-l20" "$TMPDIR/hello.txt" + run_check tac "line5" "$TMPDIR/lines.txt" + run_check comm "apple" "$TMPDIR/unsorted.txt" "$TMPDIR/unsorted.txt" + run_check join "a:b:c" "$TMPDIR/delim.txt" "$TMPDIR/delim.txt" + run_check ptx "hello" "$TMPDIR/hello.txt" + run_pipe tsort "a" "a b\nb c\n" + run_check shuf "line" "-n1" "$TMPDIR/lines.txt" + run split 0 "-l2" "$TMPDIR/lines.txt" "$TMPDIR/split-" + coreutils_assert_exists "split xaa" "$TMPDIR/split-aa" + run csplit 0 "-f" "$TMPDIR/xx" "$TMPDIR/lines.txt" 3 + coreutils_assert_exists "csplit xx00" "$TMPDIR/xx00" +} + +coreutils_suite_basic_encoding() +{ + coreutils_print_section "Encoding / hashing" + if [ -e "$BIN/base32" ]; then + run_check base32 "NBSWY" "$TMPDIR/hello.txt" + fi + run_check base64 "aGVsbG8" "$TMPDIR/hello.txt" + if [ -e "$BIN/basenc" ]; then + run_check basenc "aGVsbG8" "--base64" "$TMPDIR/hello.txt" + fi + run_check md5sum "hello.txt" "$TMPDIR/hello.txt" + run_check sha1sum "hello.txt" "$TMPDIR/hello.txt" + if [ -e "$BIN/sha224sum" ]; then + run_check sha224sum "95041d" "$TMPDIR/hello.txt" + fi + run_check sha256sum "hello.txt" "$TMPDIR/hello.txt" + if [ -e "$BIN/sha384sum" ]; then + run_check sha384sum "6b3b69" "$TMPDIR/hello.txt" + fi + run_check sha512sum "hello.txt" "$TMPDIR/hello.txt" + if [ -e "$BIN/b2sum" ]; then + run_check b2sum "hello.txt" "$TMPDIR/hello.txt" + fi + run_check cksum "hello.txt" "$TMPDIR/hello.txt" + if [ -e "$BIN/sum" ]; then + run_check sum "[0-9]" "$TMPDIR/hello.txt" + fi +} + +coreutils_suite_basic_files() +{ + coreutils_print_section "File operations" + + run cp 0 "$TMPDIR/hello.txt" "$TMPDIR/hello-copy.txt" + coreutils_assert_file_equals "cp preserved data" \ + "$TMPDIR/hello-copy.txt" "$TMPDIR/hello.txt" + + run mv 0 "$TMPDIR/hello-copy.txt" "$TMPDIR/hello-moved.txt" + coreutils_assert_not_exists "mv removed source" "$TMPDIR/hello-copy.txt" + coreutils_assert_file_equals "mv preserved data" \ + "$TMPDIR/hello-moved.txt" "$TMPDIR/hello.txt" + + run rm 0 "$TMPDIR/hello-moved.txt" + coreutils_assert_not_exists "rm removed file" "$TMPDIR/hello-moved.txt" + + run ln 0 "-s" "$TMPDIR/hello.txt" "$TMPDIR/newlink.txt" + coreutils_assert_symlink_target "ln -s target" \ + "$TMPDIR/newlink.txt" "$TMPDIR/hello.txt" + + run link 0 "$TMPDIR/hello.txt" "$TMPDIR/hardlink.txt" + coreutils_assert_file_equals "link preserved data" \ + "$TMPDIR/hardlink.txt" "$TMPDIR/hello.txt" + + run unlink 0 "$TMPDIR/hardlink.txt" + coreutils_assert_not_exists "unlink removed hardlink" "$TMPDIR/hardlink.txt" + + run mkdir 0 "$TMPDIR/newdir" + coreutils_assert_exists "mkdir created dir" "$TMPDIR/newdir" + + run rmdir 0 "$TMPDIR/newdir" + coreutils_assert_not_exists "rmdir removed dir" "$TMPDIR/newdir" + + run mkfifo 0 "$TMPDIR/testfifo" + coreutils_assert_fifo "mkfifo created fifo" "$TMPDIR/testfifo" + + run touch 0 "$TMPDIR/touched.txt" + coreutils_assert_exists "touch created file" "$TMPDIR/touched.txt" + + run truncate 0 "-s0" "$TMPDIR/touched.txt" + coreutils_assert_size "truncate size 0" "$TMPDIR/touched.txt" 0 + + run install 0 "-m" "644" "$TMPDIR/hello.txt" "$TMPDIR/installed.txt" + coreutils_assert_file_equals "install preserved data" \ + "$TMPDIR/installed.txt" "$TMPDIR/hello.txt" + coreutils_assert_mode "install mode 644" "$TMPDIR/installed.txt" 644 + + run dd 0 "if=$TMPDIR/hello.txt" "of=$TMPDIR/dd-out.txt" "bs=12" "count=1" + coreutils_assert_file_equals "dd preserved block" \ + "$TMPDIR/dd-out.txt" "$TMPDIR/hello.txt" + + run sync 0 + run_check mktemp "$TMPDIR/" "-p" "$TMPDIR" +} + +coreutils_suite_extended_files() +{ + run shred 0 "-u" "$TMPDIR/touched.txt" + coreutils_assert_not_exists "shred removed file" "$TMPDIR/touched.txt" +} + +coreutils_suite_basic_info() +{ + coreutils_print_section "File info" + run_check ls "hello.txt" "$TMPDIR" + run_check dir "hello.txt" "$TMPDIR" + run_check vdir "hello.txt" "$TMPDIR" + run_check stat "File:" "$TMPDIR/hello.txt" + run_check du "[0-9]" "-s" "$TMPDIR" + run_check df "Filesystem" "$TMPDIR" + run_check readlink "$TMPDIR/hello.txt" "$TMPDIR/symlink.txt" + run_check realpath "hello.txt" "$TMPDIR/hello.txt" + + coreutils_print_section "Path utilities" + run_check basename "hello.txt" "$TMPDIR/hello.txt" + run_check dirname "$TMPDIR" "$TMPDIR/hello.txt" + run_check pathchk "" "$TMPDIR/hello.txt" + run_check pwd "/" +} + +coreutils_suite_extended_info() +{ + run_check dircolors "COLOR" "-b" +} + +coreutils_suite_basic_math() +{ + coreutils_print_section "Math / sequence" + run_check seq "5" "1" "5" + run_check expr "3" "1" "+" "2" + run_check factor "2 2 3" "12" + if [ -e "$BIN/numfmt" ]; then + run_check numfmt "1\\.0[kK]" "--to=si" "1000" + fi +} + +coreutils_suite_basic_sysinfo() +{ + coreutils_print_section "System info" + run_check uname "Linux" "-s" + run_check date "202" "+%Y" + run_check nproc "[0-9]" + run_check printenv "/" "PATH" + run_check id "uid=" +} + +coreutils_suite_extended_sysinfo() +{ + run_check uptime "load average" + run_check hostid "[0-9a-f]" +} + +coreutils_suite_basic_process() +{ + coreutils_print_section "Process utilities" + run true 0 + run false 1 + run sleep 0 "0" + run env 0 "$BIN/true" + run nice 0 "$BIN/true" + run nohup 0 "$BIN/true" + run_check kill "TERM" "-l" + run timeout 124 "1" "$BIN/sleep" "5" +} + +coreutils_suite_extended_permissions() +{ + coreutils_print_section "Permissions / ownership" + run chmod 0 "644" "$TMPDIR/hello.txt" + coreutils_assert_mode "chmod mode 644" "$TMPDIR/hello.txt" 644 + run chown 1 "root:root" "$TMPDIR/hello.txt" + run chgrp 0 "root" "$TMPDIR/hello.txt" + run mknod 1 "$TMPDIR/testnode" "c" "1" "1" +} + +coreutils_suite_extended_users() +{ + coreutils_print_section "User info" + run_check whoami "user" + run logname 1 + run_check groups "user" + run_check pinky "Login" "-l" "user" + run who 0 + run users 0 +} + +coreutils_suite_extended_terminal() +{ + coreutils_print_section "Terminal" + run tty 1 + run stty 1 +} + +coreutils_suite_extended_io() +{ + coreutils_print_section "I/O utilities" + run_pipe tee "hello world" "hello world\n" "$TMPDIR/tee-out.txt" + coreutils_assert_contains "tee wrote file" "$TMPDIR/tee-out.txt" "^hello world$" +} + +coreutils_suite_extended_special() +{ + coreutils_print_section "Special / test" + run test 0 "-f" "$TMPDIR/hello.txt" + run "[" 0 "-f" "$TMPDIR/hello.txt" "]" + + coreutils_print_section "Expected failures / skips" + run_timeout 10 timeout 0 "5" "$BIN/true" + run_skip stdbuf "requires LD_PRELOAD" +} + +coreutils_run_smoke_suite() +{ + coreutils_suite_basic_text + coreutils_suite_basic_encoding + coreutils_suite_basic_files + coreutils_suite_basic_info + coreutils_suite_basic_math + coreutils_suite_basic_sysinfo + coreutils_suite_basic_process +} + +coreutils_run_full_suite() +{ + coreutils_suite_basic_text + coreutils_suite_extended_text + coreutils_suite_basic_encoding + coreutils_suite_basic_files + coreutils_suite_extended_files + coreutils_suite_basic_info + coreutils_suite_extended_info + coreutils_suite_basic_math + coreutils_suite_basic_sysinfo + coreutils_suite_extended_sysinfo + coreutils_suite_basic_process + coreutils_suite_extended_permissions + coreutils_suite_extended_users + coreutils_suite_extended_terminal + coreutils_suite_extended_io + coreutils_suite_extended_special +} + +coreutils_run_suite() +{ + local profile="${1:?missing profile}" + case "$profile" in + smoke) coreutils_run_smoke_suite ;; + full) coreutils_run_full_suite ;; + *) + echo "unknown coreutils suite profile: $profile" >&2 + return 1 + ;; + esac +} diff --git a/tests/lib/test-runner.sh b/tests/lib/test-runner.sh index aa6ef6e..b01d8f1 100644 --- a/tests/lib/test-runner.sh +++ b/tests/lib/test-runner.sh @@ -254,3 +254,17 @@ run_timeout() fail=$((fail + 1)) fi } + +check_host() +{ + local label="$1" + shift + + if "$@"; then + test_report ok "$label" + pass=$((pass + 1)) + else + test_report fail "$label" + fail=$((fail + 1)) + fi +} diff --git a/tests/manifest.txt b/tests/manifest.txt index a54078a..564a4c6 100644 --- a/tests/manifest.txt +++ b/tests/manifest.txt @@ -44,6 +44,7 @@ test-socket test-file-ops test-sysinfo test-io-opt +test-syscall-smoke test-poll # diff=skip [section] I/O subsystem tests @@ -95,6 +96,10 @@ test-opath test-guard-page test-mmap-hint +[section] Low-base ET_EXEC memory regression tests +test-lowbase-mem-200000 +test-lowbase-mem-300000 + [section] mremap tests test-mremap diff --git a/tests/test-coreutils-smoke.sh b/tests/test-coreutils-smoke.sh deleted file mode 100644 index 93163db..0000000 --- a/tests/test-coreutils-smoke.sh +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env bash -# test-coreutils-smoke.sh — Self-contained coreutils smoke tests for elfuse. -# -# SPDX-License-Identifier: Apache-2.0 - -set -euo pipefail - -ELFUSE="${1:?Usage: $0 [sysroot]}" -BIN="${2:?Usage: $0 [sysroot]}" -SYSROOT="${3:-}" - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -TEST_RUNNER=("$ELFUSE") -if [ -n "$SYSROOT" ]; then - TEST_RUNNER+=(--sysroot "$SYSROOT") -fi -TEST_LABEL_WIDTH=14 -TEST_TIMEOUT=10 -source "$SCRIPT_DIR/lib/test-runner.sh" -source "$SCRIPT_DIR/lib/coreutils-common.sh" - -TMPDIR=$(coreutils_make_tmpdir) -trap 'rm -rf "$TMPDIR"' EXIT - -coreutils_populate_fixtures "$TMPDIR" -coreutils_print_suite_header "Dynamic GNU coreutils smoke suite (--sysroot)" - -coreutils_print_section "Output / text utilities" -run_check echo "hello" "hello" -run_check cat "hello world" "$TMPDIR/hello.txt" -run_check head "line1" "$TMPDIR/lines.txt" -run_check tail "line5" "$TMPDIR/lines.txt" -run_check wc "5" "-l" "$TMPDIR/lines.txt" -run_check sort "apple" "$TMPDIR/unsorted.txt" -run_pipe tr "HELLO" "hello" "a-z" "A-Z" -run_check seq "5" "1" "5" -run_check expr "3" "1" "+" "2" -run_check factor "2 2 3" "12" -run_check base64 "aGVsbG8" "$TMPDIR/hello.txt" -run_check md5sum "hello.txt" "$TMPDIR/hello.txt" -run_check sha256sum "hello.txt" "$TMPDIR/hello.txt" - -coreutils_print_section "File operations" -run cp 0 "$TMPDIR/hello.txt" "$TMPDIR/hello-cp-$$" -run touch 0 "$TMPDIR/touched-$$" -run_check ls "hello" "$TMPDIR" -run_check stat "File:" "$TMPDIR/hello.txt" -run_check basename "hello.txt" "$TMPDIR/hello.txt" -run_check dirname "$TMPDIR" "$TMPDIR/hello.txt" -run_check realpath "hello.txt" "$TMPDIR/hello.txt" -run_check df "Filesystem" "$TMPDIR" -run_check du "[0-9]" "-s" "$TMPDIR" - -coreutils_print_section "System info" -run_check uname "Linux" "-s" -run_check date "202" "+%Y" -run_check id "uid=" -run_check printenv "/" "PATH" -run_check nproc "[0-9]" - -coreutils_print_section "Process utilities" -run true 0 -run false 1 -run sleep 0 "0" -run env 0 "$BIN/true" -run nice 0 "$BIN/true" -run nohup 0 "$BIN/true" -run_timeout 10 timeout 0 "5" "$BIN/true" - -coreutils_print_section "Encoding / hashing" -if [ -e "$BIN/base32" ]; then - run_check base32 "NBSWY" "$TMPDIR/hello.txt" -fi -run_check sha1sum "hello.txt" "$TMPDIR/hello.txt" -run_check sha512sum "hello.txt" "$TMPDIR/hello.txt" -if [ -e "$BIN/b2sum" ]; then - run_check b2sum "hello.txt" "$TMPDIR/hello.txt" -fi -run_check cksum "hello.txt" "$TMPDIR/hello.txt" -if [ -e "$BIN/numfmt" ]; then - run_check numfmt "1\\.0[kK]" "--to=si" "1000" -fi - -coreutils_print_summary "Dynamic results" - -if [ "$fail" -gt 0 ]; then - exit 1 -fi -exit 0 diff --git a/tests/test-coreutils.sh b/tests/test-coreutils.sh index 31837cf..157efce 100755 --- a/tests/test-coreutils.sh +++ b/tests/test-coreutils.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# test-coreutils.sh — Automated GNU coreutils 9.9 test suite for elfuse +# test-coreutils.sh — GNU coreutils integration suite for elfuse # # Copyright 2026 elfuse contributors # Copyright 2025 Moritz Angermann, zw3rk pte. ltd. @@ -7,10 +7,7 @@ # # shellcheck disable=SC1091,SC2034,SC2059,SC2154 # -# Tests all 104 coreutils through elfuse, verifying each tool can at least -# run and produce expected output or exit code. Each test is designed to -# exercise the tool's core functionality without requiring network, users -# database, or signal delivery. +# Shared entry point for both smoke and full coreutils coverage. # # Usage: tests/test-coreutils.sh [sysroot] # Example: tests/test-coreutils.sh build/elfuse /path/to/coreutils/bin /path/to/sysroot @@ -22,171 +19,30 @@ BIN="${2:?Usage: $0 }" SYSROOT="${3:-}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +COREUTILS_PROFILE="${COREUTILS_PROFILE:-full}" TEST_RUNNER=("$ELFUSE") if [ -n "$SYSROOT" ]; then TEST_RUNNER+=(--sysroot "$SYSROOT") fi TEST_LABEL_WIDTH=14 TEST_TIMEOUT=10 -TEST_SKIP_MISSING_TOOLS=1 +TEST_SKIP_MISSING_TOOLS=0 +if [ "$COREUTILS_PROFILE" = "smoke" ]; then + TEST_SKIP_MISSING_TOOLS=1 +fi source "$SCRIPT_DIR/lib/test-runner.sh" source "$SCRIPT_DIR/lib/coreutils-common.sh" +source "$SCRIPT_DIR/lib/coreutils-suite.sh" TMPDIR=$(coreutils_make_tmpdir) trap 'rm -rf "$TMPDIR"' EXIT coreutils_populate_fixtures "$TMPDIR" -coreutils_print_suite_header "GNU coreutils 9.9 test suite" - -# Output / text utilities -coreutils_print_section "Output / text utilities" -run_check cat "hello world" "$TMPDIR/hello.txt" -run_check echo "hello" "hello" -run_check printf "42" "%d" 42 -# yes writes infinitely — use timeout to limit; rc=124 (timeout) is expected -run_timeout 2 yes 124 -run_check head "line1" "$TMPDIR/lines.txt" -run_check tail "line5" "$TMPDIR/lines.txt" -run_check wc "5" "-l" "$TMPDIR/lines.txt" -run_check sort "^apple" "$TMPDIR/unsorted.txt" # verify apple is first (sorted order) -run_check uniq "aaa" "$TMPDIR/dups.txt" -run_check cut "b" "-d:" "-f2" "$TMPDIR/delim.txt" -run_pipe tr "HELLO" "hello" "a-z" "A-Z" -run_check paste "one" "$TMPDIR/tabs.txt" -run_check expand "one" "$TMPDIR/tabs.txt" -run_check unexpand "one" "$TMPDIR/tabs.txt" -run_check fmt "hello world" "$TMPDIR/hello.txt" -run_check fold "hello" "-w5" "$TMPDIR/hello.txt" -run_check nl "hello" "$TMPDIR/hello.txt" -run_check od "0000000" "-c" "$TMPDIR/hello.txt" -run_check pr "hello" "-l20" "$TMPDIR/hello.txt" -run_check tac "line5" "$TMPDIR/lines.txt" -run_check comm "apple" "$TMPDIR/unsorted.txt" "$TMPDIR/unsorted.txt" -run_check join "a:b:c" "$TMPDIR/delim.txt" "$TMPDIR/delim.txt" -run_check ptx "hello" "$TMPDIR/hello.txt" -run_pipe tsort "a" "a b\nb c\n" # topological sort -run_check shuf "line" "-n1" "$TMPDIR/lines.txt" -run split 0 "-l2" "$TMPDIR/lines.txt" "$TMPDIR/split-" -run csplit 0 "$TMPDIR/lines.txt" 3 - -# Encoding / hashing -coreutils_print_section "Encoding / hashing" -run_check base32 "NBSWY" "$TMPDIR/hello.txt" -run_check base64 "aGVsbG8gd29ybGQ" "$TMPDIR/hello.txt" -run_check basenc "aGVsbG8" "--base64" "$TMPDIR/hello.txt" -run_check md5sum "6f5902ac237024bdd0c176cb93063dc4" "$TMPDIR/hello.txt" -run_check sha1sum "22596363b3de40b06f981fb85d82312e8c0ed511" "$TMPDIR/hello.txt" -run_check sha224sum "95041dd60ab08c0bf5636d50be85fe9790300f39eb84602858a9b430" "$TMPDIR/hello.txt" -run_check sha256sum "a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447" "$TMPDIR/hello.txt" -run_check sha384sum "6b3b69ff0a404f28d75e98a066d3fc64fffd9940870cc68bece28545b9a75086" "$TMPDIR/hello.txt" -run_check sha512sum "db3974a97f2407b7cae1ae637c0030687a11913274d578492558e39c16c017de" "$TMPDIR/hello.txt" -run_check b2sum "hello.txt" "$TMPDIR/hello.txt" -run_check cksum "hello.txt" "$TMPDIR/hello.txt" -run_check sum "[0-9]" "$TMPDIR/hello.txt" - -# File operations -coreutils_print_section "File operations" -run cp 0 "$TMPDIR/hello.txt" "$TMPDIR/hello-copy.txt" -run mv 0 "$TMPDIR/hello-copy.txt" "$TMPDIR/hello-moved.txt" -run rm 0 "$TMPDIR/hello-moved.txt" -run ln 0 "-s" "$TMPDIR/hello.txt" "$TMPDIR/newlink.txt" -run link 0 "$TMPDIR/hello.txt" "$TMPDIR/hardlink.txt" -run unlink 0 "$TMPDIR/hardlink.txt" -run mkdir 0 "$TMPDIR/newdir" -run rmdir 0 "$TMPDIR/newdir" -run mkfifo 0 "$TMPDIR/testfifo" -run touch 0 "$TMPDIR/touched.txt" -run truncate 0 "-s0" "$TMPDIR/touched.txt" -run shred 0 "-u" "$TMPDIR/touched.txt" -run install 0 "-m" "644" "$TMPDIR/hello.txt" "$TMPDIR/installed.txt" -run dd 0 "if=$TMPDIR/hello.txt" "of=$TMPDIR/dd-out.txt" "bs=12" "count=1" -run sync 0 -run mktemp 0 "-p" "$TMPDIR" - -# File info -coreutils_print_section "File info" -run_check ls "hello.txt" "$TMPDIR" -run_check dir "hello.txt" "$TMPDIR" -run_check vdir "hello.txt" "$TMPDIR" -run_check stat "File:" "$TMPDIR/hello.txt" -run_check du "[0-9]" "-s" "$TMPDIR" -run_check df "Filesystem" "$TMPDIR" -run_check dircolors "COLOR" "-b" -run_check readlink "$TMPDIR/hello.txt" "$TMPDIR/symlink.txt" -run_check realpath "hello.txt" "$TMPDIR/hello.txt" - -# Path utilities -coreutils_print_section "Path utilities" -run_check basename "hello.txt" "$TMPDIR/hello.txt" -run_check dirname "$TMPDIR" "$TMPDIR/hello.txt" -run_check pathchk "" "$TMPDIR/hello.txt" -run_check pwd "/" # some path - -# Math / sequence -coreutils_print_section "Math / sequence" -run_check seq "5" "1" "5" -run_check expr "3" "1" "+" "2" -run_check factor "2 2 3" "12" -run_check numfmt "1.0k" "--to=si" "1000" - -# System info -coreutils_print_section "System info" -run_check uname "Linux" "-s" -run_check date "202" "+%Y" -run_check nproc "[0-9]" # prints CPU count -run_check uptime "load average" # reads /proc/uptime + /proc/loadavg -run_check hostid "[0-9a-f]" # prints hex host ID -run_check printenv "/" "PATH" -run_check id "uid=" # prints uid/gid info - -# Process utilities -coreutils_print_section "Process utilities" -run true 0 -run false 1 -run sleep 0 "0" -run env 0 "$BIN/true" -run nice 0 "$BIN/true" -run nohup 0 "$BIN/true" -run_check kill "TERM" "-l" - -# Permissions / ownership -coreutils_print_section "Permissions / ownership" -run chmod 0 "644" "$TMPDIR/hello.txt" -run chown 1 "root:root" "$TMPDIR/hello.txt" # expected to fail (not root) -run chgrp 0 "root" "$TMPDIR/hello.txt" # succeeds (fchown stub + /etc/group) -run mknod 1 "$TMPDIR/testnode" "c" "1" "1" # expected to fail (not root) - -# User info (limited without /etc/passwd) -coreutils_print_section "User info" -run_check whoami "user" # reads /etc/passwd (synthetic) -run logname 1 # exit 1 = "no login name" (no tty) -run_check groups "user" # reads /etc/group (synthetic) -run_check pinky "Login" "-l" "user" # reads /etc/passwd (synthetic) -run who 0 # musl getutxent() is a stub; only exit status is meaningful here -run users 0 # musl getutxent() is a stub; only exit status is meaningful here - -# Terminal -coreutils_print_section "Terminal" -run tty 1 # exit 1 = "not a tty" (correct) -run stty 1 # exit 1 = "not a tty" (correct) - -# I/O utilities -coreutils_print_section "I/O utilities" -run_pipe tee "hello world" "hello world\n" "$TMPDIR/tee-out.txt" - -# Special / test -coreutils_print_section "Special / test" -run test 0 "-f" "$TMPDIR/hello.txt" -run "[" 0 "-f" "$TMPDIR/hello.txt" "]" - -# Expected failures / skips -coreutils_print_section "Expected failures / skips" -run_timeout 10 timeout 0 "5" "$BIN/true" # child exits immediately, timeout returns 0 -run_skip stdbuf "requires LD_PRELOAD (N/A for static)" +coreutils_print_suite_header "${SUITE_LABEL:-GNU coreutils integration suite}" +coreutils_run_suite "$COREUTILS_PROFILE" run_skip chroot "needs root privileges" - -coreutils_print_summary "Results" +coreutils_print_summary "${SUITE_SUMMARY:-Results}" if [ "$fail" -gt 0 ]; then exit 1 diff --git a/tests/test-dynamic-coreutils.sh b/tests/test-dynamic-coreutils.sh index b397f79..55db318 100755 --- a/tests/test-dynamic-coreutils.sh +++ b/tests/test-dynamic-coreutils.sh @@ -7,8 +7,7 @@ # # shellcheck disable=SC1091,SC2034,SC2059,SC2154 # -# Mirrors test-coreutils.sh but invokes every tool through elfuse --sysroot, -# exercising the dynamic linker (ld-musl-aarch64.so.1) and shared libc.so. +# Shared entry point for dynamic coreutils coverage through elfuse --sysroot. # # Usage: tests/test-dynamic-coreutils.sh # Example: tests/test-dynamic-coreutils.sh build/elfuse $GUEST_SYSROOT $GUEST_DYNAMIC_COREUTILS/bin @@ -20,11 +19,17 @@ SYSROOT="${2:?Usage: $0 }" BIN="${3:?Usage: $0 }" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +COREUTILS_PROFILE="${COREUTILS_PROFILE:-full}" TEST_RUNNER=("$ELFUSE" --sysroot "$SYSROOT") TEST_LABEL_WIDTH=14 TEST_TIMEOUT=10 +TEST_SKIP_MISSING_TOOLS=0 +if [ "$COREUTILS_PROFILE" = "smoke" ]; then + TEST_SKIP_MISSING_TOOLS=1 +fi source "$SCRIPT_DIR/lib/test-runner.sh" source "$SCRIPT_DIR/lib/coreutils-common.sh" +source "$SCRIPT_DIR/lib/coreutils-suite.sh" TMPDIR=$(coreutils_make_tmpdir) trap 'rm -rf "$TMPDIR"' EXIT @@ -32,154 +37,12 @@ trap 'rm -rf "$TMPDIR"' EXIT coreutils_populate_fixtures "$TMPDIR" coreutils_print_suite_header "${SUITE_LABEL:-Dynamic GNU coreutils test suite (--sysroot)}" - -# Output / text utilities -coreutils_print_section "Output / text utilities" -run_check cat "hello world" "$TMPDIR/hello.txt" -run_check echo "hello" "hello" -run_check printf "42" "%d" 42 -# yes writes infinitely — use timeout to limit; rc=124 (timeout) is expected -run_timeout 2 yes 124 -run_check head "line1" "$TMPDIR/lines.txt" -run_check tail "line5" "$TMPDIR/lines.txt" -run_check wc "5" "-l" "$TMPDIR/lines.txt" -run_check sort "^apple" "$TMPDIR/unsorted.txt" # verify apple is first (sorted order) -run_check uniq "aaa" "$TMPDIR/dups.txt" -run_check cut "b" "-d:" "-f2" "$TMPDIR/delim.txt" -run_pipe tr "HELLO" "hello" "a-z" "A-Z" -run_check paste "one" "$TMPDIR/tabs.txt" -run_check expand "one" "$TMPDIR/tabs.txt" -run_check unexpand "one" "$TMPDIR/tabs.txt" -run_check fmt "hello world" "$TMPDIR/hello.txt" -run_check fold "hello" "-w5" "$TMPDIR/hello.txt" -run_check nl "hello" "$TMPDIR/hello.txt" -run_check od "0000000" "-c" "$TMPDIR/hello.txt" -run_check pr "hello" "-l20" "$TMPDIR/hello.txt" -run_check tac "line5" "$TMPDIR/lines.txt" -run_check comm "apple" "$TMPDIR/unsorted.txt" "$TMPDIR/unsorted.txt" -run_check join "a:b:c" "$TMPDIR/delim.txt" "$TMPDIR/delim.txt" -run_check ptx "hello" "$TMPDIR/hello.txt" -run_pipe tsort "a" "a b\nb c\n" # topological sort -run_check shuf "line" "-n1" "$TMPDIR/lines.txt" -run split 0 "-l2" "$TMPDIR/lines.txt" "$TMPDIR/split-" -run csplit 0 "$TMPDIR/lines.txt" 3 - -# Encoding / hashing -coreutils_print_section "Encoding / hashing" -run_check base32 "NBSWY" "$TMPDIR/hello.txt" -run_check base64 "aGVsbG8gd29ybGQ" "$TMPDIR/hello.txt" -run_check basenc "aGVsbG8" "--base64" "$TMPDIR/hello.txt" -run_check md5sum "6f5902ac237024bdd0c176cb93063dc4" "$TMPDIR/hello.txt" -run_check sha1sum "22596363b3de40b06f981fb85d82312e8c0ed511" "$TMPDIR/hello.txt" -run_check sha224sum "95041dd60ab08c0bf5636d50be85fe9790300f39eb84602858a9b430" "$TMPDIR/hello.txt" -run_check sha256sum "a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447" "$TMPDIR/hello.txt" -run_check sha384sum "6b3b69ff0a404f28d75e98a066d3fc64fffd9940870cc68bece28545b9a75086" "$TMPDIR/hello.txt" -run_check sha512sum "db3974a97f2407b7cae1ae637c0030687a11913274d578492558e39c16c017de" "$TMPDIR/hello.txt" -run_check b2sum "hello.txt" "$TMPDIR/hello.txt" -run_check cksum "hello.txt" "$TMPDIR/hello.txt" -run_check sum "[0-9]" "$TMPDIR/hello.txt" - -# File operations -coreutils_print_section "File operations" -run cp 0 "$TMPDIR/hello.txt" "$TMPDIR/hello-copy.txt" -run mv 0 "$TMPDIR/hello-copy.txt" "$TMPDIR/hello-moved.txt" -run rm 0 "$TMPDIR/hello-moved.txt" -run ln 0 "-s" "$TMPDIR/hello.txt" "$TMPDIR/newlink.txt" -run link 0 "$TMPDIR/hello.txt" "$TMPDIR/hardlink.txt" -run unlink 0 "$TMPDIR/hardlink.txt" -run mkdir 0 "$TMPDIR/newdir" -run rmdir 0 "$TMPDIR/newdir" -run mkfifo 0 "$TMPDIR/testfifo" -run touch 0 "$TMPDIR/touched.txt" -run truncate 0 "-s0" "$TMPDIR/touched.txt" -run shred 0 "-u" "$TMPDIR/touched.txt" -run install 0 "-m" "644" "$TMPDIR/hello.txt" "$TMPDIR/installed.txt" -run dd 0 "if=$TMPDIR/hello.txt" "of=$TMPDIR/dd-out.txt" "bs=12" "count=1" -run sync 0 -run mktemp 0 "-p" "$TMPDIR" - -# File info -coreutils_print_section "File info" -run_check ls "hello.txt" "$TMPDIR" -run_check dir "hello.txt" "$TMPDIR" -run_check vdir "hello.txt" "$TMPDIR" -run_check stat "File:" "$TMPDIR/hello.txt" -run_check du "[0-9]" "-s" "$TMPDIR" -run_check df "Filesystem" "$TMPDIR" -run_check dircolors "COLOR" "-b" -run_check readlink "$TMPDIR/hello.txt" "$TMPDIR/symlink.txt" -run_check realpath "hello.txt" "$TMPDIR/hello.txt" - -# Path utilities -coreutils_print_section "Path utilities" -run_check basename "hello.txt" "$TMPDIR/hello.txt" -run_check dirname "$TMPDIR" "$TMPDIR/hello.txt" -run_check pathchk "" "$TMPDIR/hello.txt" -run_check pwd "/" # some path - -# Math / sequence -coreutils_print_section "Math / sequence" -run_check seq "5" "1" "5" -run_check expr "3" "1" "+" "2" -run_check factor "2 2 3" "12" -run_check numfmt "1.0k" "--to=si" "1000" - -# System info -coreutils_print_section "System info" -run_check uname "Linux" "-s" -run_check date "202" "+%Y" -run_check nproc "[0-9]" # prints CPU count -run_check uptime "load average" # reads /proc/uptime + /proc/loadavg -run_check hostid "[0-9a-f]" # prints hex host ID -run_check printenv "/" "PATH" -run_check id "uid=" # prints uid/gid info - -# Process utilities -coreutils_print_section "Process utilities" -run true 0 -run false 1 -run sleep 0 "0" -run env 0 "$BIN/true" -run nice 0 "$BIN/true" -run nohup 0 "$BIN/true" -run_check kill "TERM" "-l" - -# Permissions / ownership -coreutils_print_section "Permissions / ownership" -run chmod 0 "644" "$TMPDIR/hello.txt" -run chown 1 "root:root" "$TMPDIR/hello.txt" # expected to fail (not root) -run chgrp 0 "root" "$TMPDIR/hello.txt" # succeeds (fchown stub + /etc/group) -run mknod 1 "$TMPDIR/testnode" "c" "1" "1" # expected to fail (not root) - -# User info (limited without /etc/passwd) -coreutils_print_section "User info" -run_check whoami "user" # reads /etc/passwd (synthetic) -run logname 1 # exit 1 = "no login name" (no tty) -run_check groups "user" # reads /etc/group (synthetic) -run_check pinky "Login" "-l" "user" # reads /etc/passwd (synthetic) -run who 0 # musl getutxent() is a stub; only exit status is meaningful here -run users 0 # musl getutxent() is a stub; only exit status is meaningful here - -# Terminal -coreutils_print_section "Terminal" -run tty 1 # exit 1 = "not a tty" (correct) -run stty 1 # exit 1 = "not a tty" (correct) - -# I/O utilities -coreutils_print_section "I/O utilities" -run_pipe tee "hello world" "hello world\n" "$TMPDIR/tee-out.txt" - -# Special / test -coreutils_print_section "Special / test" -run test 0 "-f" "$TMPDIR/hello.txt" -run "[" 0 "-f" "$TMPDIR/hello.txt" "]" - -# Expected failures / skips -coreutils_print_section "Expected failures / skips" -run_timeout 10 timeout 0 "5" "$BIN/true" -run_skip stdbuf "requires LD_PRELOAD (N/A for elfuse)" -run chroot 0 "/" "$BIN/true" - +coreutils_run_suite "$COREUTILS_PROFILE" +if [ "$COREUTILS_PROFILE" = "smoke" ]; then + run_skip chroot "needs root privileges" +else + run chroot 0 "/" "$BIN/true" +fi coreutils_print_summary "${SUITE_SUMMARY:-Dynamic results}" if [ "$fail" -gt 0 ]; then diff --git a/tests/test-fd-lifecycle.c b/tests/test-fd-lifecycle.c index fe54a2e..8eb971c 100644 --- a/tests/test-fd-lifecycle.c +++ b/tests/test-fd-lifecycle.c @@ -16,12 +16,16 @@ #include "test-harness.h" +#ifndef SYS_memfd_create +#define SYS_memfd_create 279 +#endif + #ifndef MFD_ALLOW_SEALING #define MFD_ALLOW_SEALING 0x0002U #endif -#ifndef SYS_memfd_create -#define SYS_memfd_create 279 +#ifndef MFD_HUGETLB +#define MFD_HUGETLB 0x0004U #endif #ifndef F_ADD_SEALS @@ -40,6 +44,20 @@ static int create_memfd(void) MFD_ALLOW_SEALING); } +static void test_memfd_accepts_valid_linux_flags(void) +{ + TEST("memfd accepts valid flags"); + + int fd = (int) syscall(SYS_memfd_create, "elfuse-valid-flags", + MFD_ALLOW_SEALING | MFD_HUGETLB); + if (fd < 0) { + FAIL("memfd_create rejected valid flags"); + return; + } + close(fd); + PASS(); +} + static void test_memfd_seals_survive_dup(void) { TEST("memfd seals copied to dup"); @@ -217,6 +235,7 @@ int main(void) test_memfd_seals_survive_dup(); test_memfd_seals_propagate_to_dup(); test_memfd_seals_cleared_on_reuse(); + test_memfd_accepts_valid_linux_flags(); test_rlimit_nofile_reports_emfile(); test_dup3_above_rlimit_fails(); diff --git a/tests/test-lowbase-mem.c b/tests/test-lowbase-mem.c new file mode 100644 index 0000000..8d96baf --- /dev/null +++ b/tests/test-lowbase-mem.c @@ -0,0 +1,101 @@ +/* Low-linked ET_EXEC memory-syscall regression test + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Validates that sys_mprotect, sys_munmap, and the page-table build path + * accept operations from an ET_EXEC linked below ELF_DEFAULT_BASE + * (0x400000). The legacy layout pinned page-table pool + shim at fixed + * low addresses [0x10000, 0x400000) and the four infra-range guards + * (sys_mmap MAP_FIXED, sys_munmap, sys_mprotect, rt_sigreturn) blanket- + * rejected operations on [SHIM_BASE, ELF_DEFAULT_BASE), so a low-linked + * binary could not RELRO its own data or munmap anything in the same + * window. The fix relocated infra to [interp_base - 4 MiB, interp_base) + * and retargeted the guards via guest_range_hits_infra. + * + * Two link addresses are exercised: + * - 0x200000: inside the legacy shim_data block, was rejected. + * - 0x300000: also inside [SHIM_BASE, ELF_DEFAULT_BASE); catches an + * off-by-one if a future patch ever re-introduces a partial low + * guard at the OLD shim_data upper edge. + * + * Primary failure mode: the binary loads but mprotect/munmap on a low + * address returns -EINVAL. The checks below assert the syscall return + * values explicitly so a silent fall-through cannot mask the regression. + */ + +#include +#include +#include +#include +#include +#include + +#include "test-harness.h" + +int passes = 0, fails = 0; + +/* A full page in the binary's own .data, forced page-aligned so the + * mprotect target covers exactly one page and does not stomp neighboring + * globals. The initializer keeps it out of .bss. + */ +__attribute__((aligned(4096), used)) static volatile char data_page[4096] = + "low-base test data page\n"; + +static void test_binary_low_linked(void) +{ + TEST("binary linked below ELF_DEFAULT_BASE"); + uintptr_t pc = (uintptr_t) &test_binary_low_linked; + /* The legacy guard window is [0x100000, 0x400000); the test binary's + * .text must land inside it for the regression to be meaningful. + */ + EXPECT_TRUE(pc >= 0x100000ULL && pc < 0x400000ULL, + "test binary not linked below 0x400000"); +} + +static void test_mprotect_own_data_page(void) +{ + TEST("mprotect own .data page to PROT_READ"); + void *page = (void *) data_page; + /* Drop the page to read-only, RELRO-style. Pre-fix this returned + * -EINVAL because [SHIM_BASE, ELF_DEFAULT_BASE) was rejected outright. + */ + int rc_drop = mprotect(page, 4096, PROT_READ); + if (rc_drop != 0) { + FAIL("mprotect(PROT_READ) on low-linked .data page failed"); + return; + } + /* Restore RW immediately so any later code that touches the page + * (e.g. printf's stdout buffer if the linker placed it nearby, or + * libc exit cleanup) cannot fault. mprotect-restoring is itself + * the same code path; failing here also indicates a guard bug. + */ + int rc_restore = mprotect(page, 4096, PROT_READ | PROT_WRITE); + EXPECT_TRUE(rc_drop == 0 && rc_restore == 0, + "mprotect restore after low-page RELRO failed"); +} + +static void test_munmap_low_scratch_range(void) +{ + TEST("munmap on scratch range below ELF_DEFAULT_BASE"); + /* 0x180000 sits in the legacy guard window [0x100000, 0x400000) but + * outside the binary's loaded segments (which start at the link + * address >= 0x200000). With the new high-IPA infra reserve, this + * range has no PT entries and no tracked region, so munmap must + * fast-path to success (return 0). Pre-fix this returned -EINVAL. + */ + int rc = munmap((void *) 0x180000UL, 4096); + EXPECT_TRUE(rc == 0, "munmap on low scratch range returned EINVAL"); +} + +int main(void) +{ + printf("test-lowbase-mem: starting\n"); + + test_binary_low_linked(); + test_mprotect_own_data_page(); + test_munmap_low_scratch_range(); + + SUMMARY("test-lowbase-mem"); + return fails ? 1 : 0; +} diff --git a/tests/test-syscall-smoke.c b/tests/test-syscall-smoke.c new file mode 100644 index 0000000..809998f --- /dev/null +++ b/tests/test-syscall-smoke.c @@ -0,0 +1,648 @@ +/* Direct syscall smoke coverage for less frequently hit dispatch entries + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test-harness.h" + +#ifndef SYS_close_range +#define SYS_close_range 436 +#endif + +#ifndef SYS_execveat +#define SYS_execveat 281 +#endif + +#ifndef SYS_pwrite64 +#define SYS_pwrite64 68 +#endif + +#ifndef SYS_preadv2 +#define SYS_preadv2 286 +#endif + +#ifndef SYS_renameat +#define SYS_renameat 38 +#endif + +#ifndef SYS_sigaltstack +#define SYS_sigaltstack 132 +#endif + +#ifndef SYS_set_tid_address +#define SYS_set_tid_address 96 +#endif + +int passes = 0, fails = 0; +extern char **environ; + +struct linux_cap_user_header { + uint32_t version; + int32_t pid; +}; + +struct linux_cap_user_data { + uint32_t effective; + uint32_t permitted; + uint32_t inheritable; +}; + +union semun { + int val; + struct semid_ds *buf; + unsigned short *array; +}; + +static void test_pwrite64_basic(void) +{ + TEST("pwrite64"); + char path[] = "/tmp/elfuse-pwrite64-XXXXXX"; + int fd = mkstemp(path); + if (fd < 0) { + FAIL("mkstemp"); + return; + } + unlink(path); + + const char *msg = "xyz"; + ssize_t rc = (ssize_t) syscall(SYS_pwrite64, fd, msg, 3, 1); + if (rc != 3) { + FAIL("pwrite64"); + close(fd); + return; + } + + char buf[8] = {0}; + lseek(fd, 0, SEEK_SET); + if (read(fd, buf, sizeof(buf)) >= 4 && memcmp(buf, "\0xyz", 4) == 0) + PASS(); + else + FAIL("pwrite64 contents"); + close(fd); +} + +static void test_splice_family(void) +{ + TEST("vmsplice + splice"); + int src[2] = {-1, -1}; + int mid[2] = {-1, -1}; + struct iovec iov = {.iov_base = (void *) "pipe-data", .iov_len = 9}; + char out[16] = {0}; + bool ok = false; + + if (pipe(src) != 0 || pipe(mid) != 0) { + FAIL("pipe"); + goto out; + } + if (syscall(SYS_vmsplice, src[1], &iov, 1, 0) != 9) { + FAIL("vmsplice"); + goto out; + } + if (syscall(SYS_splice, src[0], NULL, mid[1], NULL, 9, 0) != 9) { + FAIL("splice"); + goto out; + } + if (read(mid[0], out, sizeof(out)) != 9 || + memcmp(out, "pipe-data", 9) != 0) { + FAIL("splice payload"); + goto out; + } + ok = true; + +out: + if (src[0] >= 0) + close(src[0]); + if (src[1] >= 0) + close(src[1]); + if (mid[0] >= 0) + close(mid[0]); + if (mid[1] >= 0) + close(mid[1]); + if (ok) + PASS(); +} + +static void test_tee_stub_errno(void) +{ + TEST("tee returns EINVAL"); + int src[2] = {-1, -1}; + int dst[2] = {-1, -1}; + + if (pipe(src) != 0 || pipe(dst) != 0) { + FAIL("pipe"); + goto out; + } + + errno = 0; + EXPECT_ERRNO(syscall(SYS_tee, src[0], dst[1], 1, 0), EINVAL, + "tee should report EINVAL"); + +out: + if (src[0] >= 0) + close(src[0]); + if (src[1] >= 0) + close(src[1]); + if (dst[0] >= 0) + close(dst[0]); + if (dst[1] >= 0) + close(dst[1]); +} + +static void test_close_range_basic(void) +{ + TEST("close_range"); + int pipes[3][2]; + memset(pipes, -1, sizeof(pipes)); + for (size_t i = 0; i < 3; i++) { + if (pipe(pipes[i]) != 0) { + FAIL("pipe"); + goto out; + } + } + + unsigned first = (unsigned) pipes[0][0]; + unsigned last = (unsigned) pipes[2][1]; + if (syscall(SYS_close_range, first, last, 0) != 0) { + FAIL("close_range"); + goto out; + } + + /* Probe before clearing pipes[] so the EXPECT_ERRNO actually tests + * close_range's effect, not close(-1). On success every fd in the + * range is closed, so leave pipes[] zeroed afterward to keep the + * cleanup loop from double-closing. + */ + int probe_fd = pipes[1][0]; + memset(pipes, -1, sizeof(pipes)); + errno = 0; + EXPECT_ERRNO(close(probe_fd), EBADF, "close_range left fd open"); + +out: + for (size_t i = 0; i < 3; i++) { + if (pipes[i][0] >= 0) + close(pipes[i][0]); + if (pipes[i][1] >= 0) + close(pipes[i][1]); + } +} + +static void test_at_path_ops(void) +{ + TEST("mknodat + mkdirat + symlinkat + renameat"); + char dir_template[] = "/tmp/elfuse-atops-XXXXXX"; + char fifo_path[256] = ""; + char link_path[256] = ""; + char moved_path[256] = ""; + char subdir_path[256] = ""; + int dirfd = -1; + bool ok = false; + + if (!mkdtemp(dir_template)) { + FAIL("mkdtemp"); + return; + } + /* Materialize cleanup paths up-front so that any early goto-out still + * unlinks/rmdirs whatever the syscalls under test managed to create. + */ + snprintf(fifo_path, sizeof(fifo_path), "%s/fifo", dir_template); + snprintf(link_path, sizeof(link_path), "%s/fifo.link", dir_template); + snprintf(moved_path, sizeof(moved_path), "%s/fifo.moved", dir_template); + snprintf(subdir_path, sizeof(subdir_path), "%s/sub", dir_template); + + dirfd = open(dir_template, O_RDONLY | O_DIRECTORY); + if (dirfd < 0) { + FAIL("open dir"); + goto out; + } + if (syscall(SYS_mkdirat, dirfd, "sub", 0700) != 0) { + FAIL("mkdirat"); + goto out; + } + if (syscall(SYS_mknodat, dirfd, "fifo", S_IFIFO | 0600, 0) != 0) { + FAIL("mknodat"); + goto out; + } + if (syscall(SYS_symlinkat, "fifo", dirfd, "fifo.link") != 0) { + FAIL("symlinkat"); + goto out; + } + if (syscall(SYS_renameat, dirfd, "fifo.link", dirfd, "fifo.moved") != 0) { + FAIL("renameat"); + goto out; + } + if (access(link_path, F_OK) == 0 || errno != ENOENT || + access(moved_path, F_OK) != 0 || access(fifo_path, F_OK) != 0) { + FAIL("path ops verification"); + goto out; + } + ok = true; + +out: + if (dirfd >= 0) + close(dirfd); + if (moved_path[0]) + unlink(moved_path); + if (link_path[0]) + unlink(link_path); + if (fifo_path[0]) + unlink(fifo_path); + if (subdir_path[0]) + rmdir(subdir_path); + rmdir(dir_template); + /* Body-side FAIL() already reported the specific step on failure; only + * report PASS here. Adding a trailing else-FAIL would double-count. + */ + if (ok) + PASS(); +} + +static void test_statfs_and_umask(void) +{ + TEST("statfs + umask"); + struct statfs st; + mode_t old = umask(022); + mode_t restored = umask(old); + if (statfs("/tmp", &st) == 0 && st.f_bsize > 0 && restored == 022) + PASS(); + else + FAIL("statfs/umask"); +} + +static void test_direct_gap_syscalls(void) +{ + TEST( + "preadv2 + fstatfs + fchmodat + sync + setregid + setresgid + " + "sched_yield"); + char path[] = "/tmp/elfuse-syscall-gap-XXXXXX"; + int fd = -1; + struct iovec iov; + char buf[8] = {0}; + struct statfs st; + struct stat sb; + bool ok = false; + + fd = mkstemp(path); + if (fd < 0) { + FAIL("mkstemp"); + return; + } + + if (write(fd, "abcd", 4) != 4) { + FAIL("write"); + goto out; + } + + iov.iov_base = buf; + iov.iov_len = 3; + if (preadv2(fd, &iov, 1, 1, 0) != 3 || memcmp(buf, "bcd", 3) != 0) { + FAIL("preadv2"); + goto out; + } + if (fstatfs(fd, &st) != 0 || st.f_bsize <= 0) { + FAIL("fstatfs"); + goto out; + } + if (fchmodat(AT_FDCWD, path, 0600, 0) != 0) { + FAIL("fchmodat"); + goto out; + } + if (stat(path, &sb) != 0 || (sb.st_mode & 0777) != 0600) { + FAIL("fchmodat verify"); + goto out; + } + sync(); + if (setregid((gid_t) -1, (gid_t) -1) != 0) { + FAIL("setregid"); + goto out; + } + if (setresgid((gid_t) -1, (gid_t) -1, (gid_t) -1) != 0) { + FAIL("setresgid"); + goto out; + } + if (sched_yield() != 0) { + FAIL("sched_yield"); + goto out; + } + ok = true; + +out: + if (fd >= 0) + close(fd); + unlink(path); + if (ok) + PASS(); +} + +static void test_sigaltstack_and_timers(void) +{ + TEST("sigaltstack + getitimer + setitimer + clock_getres"); + stack_t old_ss; + stack_t ss = {0}; + sigset_t oldmask, blockmask; + bool mask_saved = false; + struct itimerval old_timer; + struct itimerval timer = {0}; + struct timespec ts; + bool ok = false; + + ss.ss_size = SIGSTKSZ; + ss.ss_sp = malloc(ss.ss_size); + if (!ss.ss_sp) { + FAIL("malloc"); + return; + } + if (syscall(SYS_sigaltstack, &ss, &old_ss) != 0) { + FAIL("sigaltstack"); + goto out; + } + sigemptyset(&blockmask); + sigaddset(&blockmask, SIGALRM); + if (sigprocmask(SIG_BLOCK, &blockmask, &oldmask) != 0) { + FAIL("sigprocmask"); + goto out; + } + mask_saved = true; + timer.it_value.tv_usec = 1000; + if (setitimer(ITIMER_REAL, &timer, &old_timer) != 0) { + FAIL("setitimer"); + goto out; + } + if (getitimer(ITIMER_REAL, &timer) != 0) { + FAIL("getitimer"); + goto out; + } + if (clock_getres(CLOCK_MONOTONIC, &ts) != 0) { + FAIL("clock_getres"); + goto out; + } + if (timer.it_value.tv_sec < 0 || ts.tv_nsec < 0) { + FAIL("timer query"); + goto out; + } + ok = true; + +out: + timer.it_value.tv_sec = 0; + timer.it_value.tv_usec = 0; + timer.it_interval.tv_sec = 0; + timer.it_interval.tv_usec = 0; + setitimer(ITIMER_REAL, &timer, NULL); + if (mask_saved) + sigprocmask(SIG_SETMASK, &oldmask, NULL); + ss.ss_flags = SS_DISABLE; + syscall(SYS_sigaltstack, &ss, NULL); + free(ss.ss_sp); + /* Body-side FAIL() already reported the specific step on failure. */ + if (ok) + PASS(); +} + +static void test_waitid(void) +{ + TEST("waitid"); + pid_t pid = fork(); + if (pid < 0) { + FAIL("fork"); + return; + } + if (pid == 0) + _exit(17); + + siginfo_t info; + memset(&info, 0, sizeof(info)); + long rc = syscall(SYS_waitid, P_PID, pid, &info, WEXITED, NULL); + if (rc == 0 && info.si_pid == pid && info.si_status == 17) { + PASS(); + return; + } + /* waitid failed or returned the wrong siginfo; reap with waitpid so + * the child does not linger as a zombie and skew later tests. + */ + int status; + waitpid(pid, &status, 0); + FAIL("waitid"); +} + +static void test_execveat(int argc, char **argv) +{ + if (argc > 1 && strcmp(argv[1], "--execveat-child") == 0) + _exit(23); + + TEST("execveat"); + pid_t pid = fork(); + if (pid < 0) { + FAIL("fork"); + return; + } + if (pid == 0) { + char *child_argv[] = {argv[0], "--execveat-child", NULL}; + syscall(SYS_execveat, AT_FDCWD, argv[0], child_argv, environ, 0); + _exit(127); + } + + int status = 0; + if (waitpid(pid, &status, 0) == pid && WIFEXITED(status) && + WEXITSTATUS(status) == 23) + PASS(); + else + FAIL("execveat"); +} + +static void test_process_query_stubs(void) +{ + TEST("set_tid_address + capget + personality"); + int clear_tid = 0; + struct linux_cap_user_header hdr = { + .version = 0x20080522, + .pid = 0, + }; + struct linux_cap_user_data data[2]; + memset(data, 0, sizeof(data)); + + long tid = syscall(SYS_set_tid_address, &clear_tid); + long caps = syscall(SYS_capget, &hdr, data); + long pers = syscall(SYS_personality, 0xffffffffu); + if (tid > 0 && caps == 0 && pers >= 0) + PASS(); + else + FAIL("process query stubs"); +} + +static void test_stub_errnos(void) +{ + TEST("sethostname returns EPERM"); + errno = 0; + EXPECT_ERRNO(syscall(SYS_sethostname, "elfuse", 6), EPERM, + "sethostname should fail with EPERM"); + + TEST("io_destroy returns EINVAL"); + errno = 0; + EXPECT_ERRNO(syscall(SYS_io_destroy, 1), EINVAL, + "io_destroy should fail with EINVAL"); + + TEST("mincore returns ENOSYS"); + char page[4096]; + unsigned char vec = 0xff; + errno = 0; + EXPECT_ERRNO(syscall(SYS_mincore, page, sizeof(page), &vec), ENOSYS, + "mincore should fail with ENOSYS"); +} + +static void test_memory_stubs(void) +{ + TEST("mlock + munlock"); + void *p = mmap(NULL, 4096, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (p == MAP_FAILED) { + FAIL("mmap"); + return; + } + if (mlock(p, 4096) == 0 && munlock(p, 4096) == 0) + PASS(); + else + FAIL("mlock/munlock"); + munmap(p, 4096); +} + +static void test_accept4(void) +{ + TEST("accept4"); + int listen_fd = -1, client_fd = -1, server_fd = -1; + struct sockaddr_in addr; + socklen_t addrlen = sizeof(addr); + pid_t pid; + int status = 0; + + listen_fd = socket(AF_INET, SOCK_STREAM, 0); + if (listen_fd < 0) { + FAIL("socket"); + return; + } + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + if (bind(listen_fd, (struct sockaddr *) &addr, sizeof(addr)) != 0 || + listen(listen_fd, 1) != 0 || + getsockname(listen_fd, (struct sockaddr *) &addr, &addrlen) != 0) { + FAIL("listen setup"); + goto out; + } + + pid = fork(); + if (pid < 0) { + FAIL("fork"); + goto out; + } + if (pid == 0) { + client_fd = socket(AF_INET, SOCK_STREAM, 0); + if (client_fd < 0) + _exit(1); + int rc = connect(client_fd, (struct sockaddr *) &addr, sizeof(addr)); + _exit(rc == 0 ? 0 : 2); + } + + /* Gate the blocking accept4() on a poll(): if the child fails before + * connect(), no incoming connection will ever arrive and the parent + * would wedge the test until the driver timeout. The poll bounds the + * wait so we can detect that case and fail fast. + */ + struct pollfd pfd = {.fd = listen_fd, .events = POLLIN}; + int pr = poll(&pfd, 1, 2000); + if (pr <= 0) { + waitpid(pid, &status, 0); + FAIL("no client connection within 2s"); + goto out; + } + server_fd = accept4(listen_fd, NULL, NULL, SOCK_CLOEXEC); + waitpid(pid, &status, 0); + if (server_fd < 0 || !WIFEXITED(status) || WEXITSTATUS(status) != 0) { + FAIL("accept4 handshake"); + goto out; + } + int flags = fcntl(server_fd, F_GETFD); + if (flags < 0 || !(flags & FD_CLOEXEC)) { + FAIL("SOCK_CLOEXEC not applied"); + goto out; + } + PASS(); + +out: + if (server_fd >= 0) + close(server_fd); + if (client_fd >= 0) + close(client_fd); + if (listen_fd >= 0) + close(listen_fd); +} + +static void test_sysv_semaphore_ops(void) +{ + TEST("semget + semctl + semop"); + int semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600); + if (semid < 0) { + FAIL("semget"); + return; + } + + union semun arg = {.val = 1}; + struct sembuf sop = {.sem_num = 0, .sem_op = -1, .sem_flg = 0}; + if (semctl(semid, 0, SETVAL, arg) == 0 && semop(semid, &sop, 1) == 0 && + semctl(semid, 0, IPC_RMID, arg) == 0) + PASS(); + else { + semctl(semid, 0, IPC_RMID, arg); + FAIL("semop"); + } +} + +int main(int argc, char **argv) +{ + printf("test-syscall-smoke: direct syscall smoke coverage\n\n"); + + test_pwrite64_basic(); + test_splice_family(); + test_tee_stub_errno(); + test_close_range_basic(); + test_at_path_ops(); + test_statfs_and_umask(); + test_direct_gap_syscalls(); + test_sigaltstack_and_timers(); + test_waitid(); + test_execveat(argc, argv); + test_process_query_stubs(); + test_stub_errnos(); + test_memory_stubs(); + test_accept4(); + test_sysv_semaphore_ops(); + + SUMMARY("test-syscall-smoke"); + return fails > 0 ? 1 : 0; +}