From 5836ae0586177053202acaecfb9f1d4421b9ce46 Mon Sep 17 00:00:00 2001 From: Xalestar Date: Thu, 23 Jul 2026 11:54:58 +0800 Subject: [PATCH] Test readlinkat syscall coverage directly The syscall coverage audit exempted readlinkat on the grounds that libc readlink() and the procfs symlink paths exercise it indirectly. Indirect coverage cannot distinguish a working handler from a missing one, so add direct syscall(SYS_readlinkat, ...) tests for absolute paths, dirfd relative paths, truncation without NUL termination, and EBADF on an invalid dirfd. The exemption table only ever grew: entries survived after direct tests appeared, so a later test regression would still pass the audit. Make the audit fail on exemptions it did not use, and drop the five entries that are already covered directly. --- scripts/check-syscall-coverage.py | 30 ++++++++++++------- tests/test-file-ops.c | 49 ++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/scripts/check-syscall-coverage.py b/scripts/check-syscall-coverage.py index 81faacc1..b0d87a21 100644 --- a/scripts/check-syscall-coverage.py +++ b/scripts/check-syscall-coverage.py @@ -29,8 +29,10 @@ "signalfd4": {"signalfd"}, } +# Keep only syscalls that genuinely lack a direct test reference. main() rejects +# stale entries once a direct call or alias is added so exemptions cannot mask a +# later test regression. 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.", @@ -42,13 +44,9 @@ "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.", } @@ -122,6 +120,7 @@ def has_direct_reference(name: str, c_corpus: str, other_corpus: str) -> bool: def main() -> int: c_corpus, other_corpus = load_test_corpora() missing: list[str] = [] + used_indirect: dict[str, str] = {} for name in load_dispatch_names(): if has_direct_reference(name, c_corpus, other_corpus): @@ -132,17 +131,28 @@ def main() -> int: ): continue if name in INDIRECT_COVERAGE: + used_indirect[name] = INDIRECT_COVERAGE[name] 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) + stale_indirect = sorted(set(INDIRECT_COVERAGE) - set(used_indirect)) + if missing or stale_indirect: + if missing: + print("Uncovered syscalls in dispatch.tbl:", file=sys.stderr) + for name in missing: + print(f" - {name}", file=sys.stderr) + if stale_indirect: + print( + "Stale indirect-coverage exemptions (now directly covered or " + "absent from dispatch.tbl):", + file=sys.stderr, + ) + for name in stale_indirect: + print(f" - {name}", file=sys.stderr) return 1 print("syscall coverage audit: PASS") - for name, reason in sorted(INDIRECT_COVERAGE.items()): + for name, reason in sorted(used_indirect.items()): print(f" indirect {name}: {reason}") return 0 diff --git a/tests/test-file-ops.c b/tests/test-file-ops.c index 6b5eae73..a332acde 100644 --- a/tests/test-file-ops.c +++ b/tests/test-file-ops.c @@ -5,7 +5,8 @@ * Copyright 2025 Moritz Angermann, zw3rk pte. ltd. * SPDX-License-Identifier: Apache-2.0 * - * Tests: chmod, symlink, hardlink, utimensat, statx, faccessat2, renameat2 + * Tests: chmod, symlink, readlinkat, hardlink, utimensat, statx, faccessat2, + * renameat2 */ #include @@ -30,6 +31,10 @@ #define SYS_faccessat2 439 #endif +#ifndef SYS_readlinkat +#define SYS_readlinkat 78 +#endif + #ifndef SYS_renameat2 #define SYS_renameat2 276 #endif @@ -115,6 +120,48 @@ int main(void) } else FAIL("symlink failed"); + TEST("readlinkat absolute path"); + { + char buf[256]; + ssize_t len = + syscall(SYS_readlinkat, AT_FDCWD, symlink_path, buf, sizeof(buf)); + EXPECT_TRUE(len == (ssize_t) strlen(testfile) && + !memcmp(buf, testfile, (size_t) len), + "readlinkat absolute path mismatch"); + } + + TEST("readlinkat relative to dirfd"); + { + char buf[256]; + int dirfd = open("/tmp", O_RDONLY | O_DIRECTORY); + if (dirfd < 0) { + FAIL("open /tmp failed"); + } else { + ssize_t len = syscall(SYS_readlinkat, dirfd, "elfuse-test-symlink", + buf, sizeof(buf)); + close(dirfd); + EXPECT_TRUE(len == (ssize_t) strlen(testfile) && + !memcmp(buf, testfile, (size_t) len), + "readlinkat dirfd path mismatch"); + } + } + + TEST("readlinkat truncates without terminator"); + { + char buf[5] = {'X', 'X', 'X', 'X', 'X'}; + ssize_t len = syscall(SYS_readlinkat, AT_FDCWD, symlink_path, buf, 4); + EXPECT_TRUE(len == 4 && !memcmp(buf, testfile, 4) && buf[4] == 'X', + "readlinkat truncation mismatch"); + } + + TEST("readlinkat invalid dirfd"); + { + char buf[1]; + EXPECT_ERRNO(syscall(SYS_readlinkat, -1, "elfuse-test-symlink", buf, + sizeof(buf)), + EBADF, "expected EBADF for invalid dirfd"); + } + /* Test hardlink */ TEST("hardlink (link)"); if (link(testfile, hardlink_path) == 0) {