Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions scripts/check-syscall-coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand All @@ -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.",
}


Expand Down Expand Up @@ -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):
Expand All @@ -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

Expand Down
49 changes: 48 additions & 1 deletion tests/test-file-ops.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 <errno.h>
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
Loading