Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3db8916
Assert each syscall flag set and clear
xalestar Oct 9, 2026
36c20bc
Cover the fd-creating calls in the flag matrix
xalestar Oct 9, 2026
d77b3b5
Refuse undefined flag bits in four syscalls
xalestar Oct 9, 2026
b99ceb4
Answer ENOENT for an empty path on any dirfd
xalestar Oct 9, 2026
382b954
Refuse statx with both sync flags set
xalestar Oct 9, 2026
aad176d
Answer EISDIR for unlink of a directory
xalestar Oct 9, 2026
23dca1e
Report O_DIRECT on a pipe2 write end
xalestar Oct 9, 2026
c6919ce
Decline O_TMPFILE and refuse its misuse
xalestar Oct 9, 2026
ec2de4d
Cover fcntl, mmap and mprotect in the flag matrix
xalestar Oct 9, 2026
800215c
Cover memfd sealing and pipe packet mode
xalestar Oct 9, 2026
fd3da6a
Refuse an mmap with no valid mapping type
xalestar Oct 9, 2026
c3b78d9
Refuse the mprotect bits Linux refuses
xalestar Oct 9, 2026
9f7e0ff
Make a fresh PROT_READ mapping read-only
xalestar Oct 9, 2026
a879ab9
Refuse an unaligned offset on anonymous mmap
xalestar Oct 9, 2026
3c14a5f
Refuse MAP_HUGETLB on a file mapping
xalestar Oct 9, 2026
d8daa8c
Treat a protection without access bits as none
xalestar Oct 9, 2026
a9f75f9
Honor MFD_NOEXEC_SEAL and give a memfd its mode
xalestar Oct 9, 2026
9b47ac6
Honor AT_EMPTY_PATH in faccessat2 and utimensat
xalestar Oct 9, 2026
cffa733
Move the link-to-symlink fallback into a helper
xalestar Oct 9, 2026
41b1d15
Honor AT_EMPTY_PATH in linkat
xalestar Oct 9, 2026
31ef112
Read the link an O_PATH descriptor names
xalestar Oct 15, 2026
81abd03
Cover clone in the flag matrix
xalestar Oct 9, 2026
2e728fc
Refuse clone flags that need a missing partner
xalestar Oct 9, 2026
8004e78
Mark every pidfd close-on-exec
xalestar Oct 9, 2026
5fe4e1b
Honor CLONE_PIDFD and the tid slot in a fork clone
xalestar Oct 9, 2026
b469621
Let a CLONE_VM child see its tid in the slot
xalestar Oct 9, 2026
a8c562e
Hold the flag matrix rows in the coverage audit
xalestar Oct 9, 2026
ceb4104
Answer faccessat2 for a /proc file that is gone
xalestar Oct 15, 2026
476d4e4
Expect success or ENOMEM from a hugetlb memfd map
xalestar Oct 15, 2026
2c747be
Drop the PROT_BTI row no lane CPU agrees on
xalestar Oct 15, 2026
d2ea66e
Fail a fresh mapping whose PTEs cannot be set
xalestar Oct 16, 2026
fe09dee
Freeze an executable memfd sealed with F_SEAL_EXEC
xalestar Oct 16, 2026
133af71
Open a descriptor's directory for search only
xalestar Oct 16, 2026
06b583b
Roll back a fresh mapping whose PTEs cannot be set
xalestar Oct 16, 2026
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
16 changes: 16 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ DISPATCH_HEADER := $(BUILD_DIR)/dispatch.h
DEPARTED_MANIFEST := tests/usbdev-ioctl-departed.tbl
DEPARTED_GENERATOR := scripts/gen-usbdev-ioctl-departed.py
DEPARTED_HEADER := $(BUILD_DIR)/usbdev-ioctl-departed-vectors.h
# The per-flag rows tests/test-flag-matrix.c runs. Generated under build/ for
# the same reason the departed-device vectors are.
FLAG_MATRIX_MANIFEST := tests/flag-matrix.tbl
FLAG_MATRIX_GENERATOR := scripts/gen-flag-matrix.py
FLAG_MATRIX_HEADER := $(BUILD_DIR)/flag-matrix-vectors.h
HVF_LDFLAGS := -framework Hypervisor -framework IOKit -framework CoreFoundation -arch arm64

# Generated headers under build/ that must exist before compiling sources that
Expand Down Expand Up @@ -155,6 +160,11 @@ $(DISPATCH_HEADER): $(DISPATCH_MANIFEST) $(DISPATCH_GENERATOR) src/syscall/abi.h

$(BUILD_DIR)/syscall/syscall.o: $(DISPATCH_HEADER)

$(FLAG_MATRIX_HEADER): $(FLAG_MATRIX_MANIFEST) $(FLAG_MATRIX_GENERATOR) \
$(DISPATCH_MANIFEST) | $(BUILD_DIR)
@echo " GEN $@"
$(Q)python3 $(FLAG_MATRIX_GENERATOR) --output $@

$(DEPARTED_HEADER): $(DEPARTED_MANIFEST) $(DEPARTED_GENERATOR) \
src/syscall/usbdev.c | $(BUILD_DIR)
@echo " GEN $@"
Expand Down Expand Up @@ -402,6 +412,12 @@ $(BUILD_DIR)/test-usbdev-ioctl-departed: tests/test-usbdev-ioctl-departed.c \
@echo " CROSS $<"
$(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -I$(BUILD_DIR) -o $@ $<

# test-flag-matrix includes its generated rows the same way.
$(BUILD_DIR)/test-flag-matrix: tests/test-flag-matrix.c \
Comment thread
xalestar marked this conversation as resolved.
$(FLAG_MATRIX_HEADER) | $(BUILD_DIR)
@echo " CROSS $<"
$(Q)$(CROSS_COMPILE)gcc $(CROSS_TEST_CFLAGS) -I$(BUILD_DIR) -o $@ $<

# test-usbdev-ioctl churns open/read/close on one usbdevfs node from four
# threads, so a close and a sibling's open contend for the same fd number.
$(BUILD_DIR)/test-usbdev-ioctl: tests/test-usbdev-ioctl.c | $(BUILD_DIR)
Expand Down
28 changes: 28 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ What they do:
- `make check`: fast elfuse-internal gate. Runs, in order:
- `scripts/check-syscall-coverage.py` so any new `dispatch.tbl`
entry without a direct or aliased test reference fails the build
(the same script also requires the syscalls it lists as flag-taking to
keep their rows in `tests/flag-matrix.tbl`)
- `scripts/check-eintr-contract.py` so a new interruptible wait fails
the build until it states whether it may be restarted (`forbids`,
`restartable`, or `not-a-wait`), with the `forbids` claims checked
Expand All @@ -116,6 +118,9 @@ What they do:
asks make for the target list, which is the point: a `VERIFY_<T>_SRC`
block written below the `:=` that builds `VERIFY_TARGETS` parses fine
and generates no rule
- `scripts/gen-flag-matrix.py --check` so the generated rows of
`test-flag-matrix` match `tests/flag-matrix.tbl`, and so a row naming a
syscall `dispatch.tbl` no longer serves fails the build
- the two harness self-tests, `test-config` (that `tests/test-config.sh`
keeps its CLI mode separate from its sourced mode) and `test-runner`
(the shared shell runner's output matching and exit-status checks), so
Expand Down Expand Up @@ -535,6 +540,29 @@ answers with its primary alone, because the backing half belongs to a stream
that has gone. Both rows are load-bearing in pairs -- neither number alone
separates the answers the site could give -- so both are printed.

`test-flag-matrix` records what Linux answers for one flag of one syscall,
with the flag set and with it clear, one row each in `tests/flag-matrix.tbl`.
Both answers are asserted. Where they differ, the row passes only when the flag
is what changes the result; a row whose last field is `=` records that Linux
answers the same either way, and passes only when it does. The qemu lane runs
the binary with `strict`, which is what keeps
the recorded answers true; a row it refuses is a wrong row, not a kernel
difference to skip. Two marks exist for elfuse. `pending` is a divergence still
to be fixed: its mismatch is reported and not failed, and the lane fails once
the row passes, until the mark is removed. `unsupported:ANSWER` is a flag
elfuse declines on purpose, and ANSWER has to be one Linux documents for a
kernel or filesystem without the feature. One row is pending by decision
rather than by backlog: `pipe2 O_DIRECT@packets`. The host pipe is a byte
stream, so an `O_DIRECT` pipe here delivers two writes in one read where Linux
delivers the first alone. Giving the pipe a framed backing would fix `pipe2`
and could not fix `F_SETFL` on a pipe that already exists; refusing the flag
with `EINVAL`, which `pipe2(2)` documents for kernels that predate it, would
undo the acceptance `test-negative` and `test-fcntl-flags` assert. Until one
of those is chosen the row reports the difference on every run. To add a
syscall, add rows; the
driver has no code for any one call. A new kind of argument or answer goes
into `scripts/gen-flag-matrix.py` and `tests/test-flag-matrix.c` together.

`test-usbdev-ioctl-departed` is the same idea with the recording moved out of
the lane and into data. Its rows are generated from
`tests/usbdev-ioctl-departed.tbl`, one per usbdevfs ioctl, and a row that
Expand Down
11 changes: 9 additions & 2 deletions mk/tests.mk
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# src/elfuse-limits.h.
ELFUSE_HOST_NOFILE_MIN ?= $(shell bash "$(CURDIR)/tests/test-config.sh" --host-nofile)

.PHONY: test-hello test-all check check-syscall-coverage check-eintr-contract check-lock-order check-atomics check-ascii check-usbdev-departed check-svc-tails check-skill-refs check-proof-targets test-gdbstub test-coreutils test-busybox test-shim-futex-stats test-vcpu-watchdog \
.PHONY: test-hello test-all check check-syscall-coverage check-eintr-contract check-lock-order check-atomics check-ascii check-usbdev-departed check-flag-matrix check-svc-tails check-skill-refs check-proof-targets test-gdbstub test-coreutils test-busybox test-shim-futex-stats test-vcpu-watchdog \
test-static-bins \
test-dynamic test-dynamic-coreutils test-glibc-dynamic \
test-glibc-coreutils test-perf \
Expand Down Expand Up @@ -309,7 +309,7 @@ check-sanitizer: $(ELFUSE_BIN) $(TEST_DEPS) $(CHECK_HOST_UNIT_BINS)
$(CHECK_SHARED_LANES)

## Run the unit test suite plus busybox applet validation
check: $(ELFUSE_BIN) $(TEST_DEPS) check-syscall-coverage check-eintr-contract check-lock-order check-atomics check-ascii check-svc-tails check-skill-refs check-proof-targets check-usbdev-departed check-usb-fixture-bin test-config test-runner \
check: $(ELFUSE_BIN) $(TEST_DEPS) check-syscall-coverage check-eintr-contract check-lock-order check-atomics check-ascii check-svc-tails check-skill-refs check-proof-targets check-usbdev-departed check-flag-matrix check-usb-fixture-bin test-config test-runner \
$(CHECK_HOST_UNIT_BINS)
@bash tests/driver.sh -e $(ELFUSE_BIN) -d $(TEST_DIR) -v
$(CHECK_SHARED_LANES)
Expand Down Expand Up @@ -1829,6 +1829,13 @@ check-usb-fixture-bin:
check-usbdev-departed: $(DEPARTED_HEADER)
@python3 $(DEPARTED_GENERATOR) --check --output $(DEPARTED_HEADER)

## Verify the generated flag-matrix rows still match tests/flag-matrix.tbl
#
# The generator also joins the table against dispatch.tbl, so a row for a
# syscall the dispatcher no longer serves fails here.
check-flag-matrix: $(FLAG_MATRIX_HEADER)
@python3 $(FLAG_MATRIX_GENERATOR) --check --output $(FLAG_MATRIX_HEADER)

## Build the fixture-enabled binary the two loopback lanes run
#
# The loopback fixture is not in the default build (USB_LOOPBACK_FIXTURE in
Expand Down
2 changes: 1 addition & 1 deletion scripts/check-eintr-contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@
"not-a-wait",
"Refuses the overlay because an execve is reaping this thread.",
),
"runtime/forkipc.c::sys_clone": (
"runtime/forkipc.c::clone_dispatch": (
"not-a-wait",
"Refuses the fork because an execve is reaping this thread.",
),
Expand Down
69 changes: 69 additions & 0 deletions scripts/check-syscall-coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,53 @@
}


FLAG_MATRIX = TESTS / "flag-matrix.tbl"

# Syscalls whose flags argument must have rows in tests/flag-matrix.tbl. A name
# mentioned in a test says the call was made; a row says each flag was asserted
# set and clear against a real kernel. Dropping the rows for one of these fails
# the audit, and so does listing a syscall the dispatcher no longer serves.
FLAG_MATRIX_REQUIRED: set[str] = {
"accept4",
"clone",
"dup3",
"epoll_create1",
"eventfd2",
"faccessat2",
"fchmodat",
"fchmodat2",
"fchownat",
"fcntl",
"inotify_init1",
"linkat",
"memfd_create",
"mmap",
"mprotect",
"newfstatat",
"openat",
"pipe2",
"renameat2",
"signalfd4",
"socket",
"socketpair",
"statx",
"timerfd_create",
"unlinkat",
"utimensat",
}

_ROW_PREFIX = re.compile(r"^(?:pending |unsupported:\S+ )?([a-z][a-z0-9_]*) \| ")


def load_flag_matrix_names() -> set[str]:
names: set[str] = set()
for line in FLAG_MATRIX.read_text(encoding="utf-8").splitlines():
match = _ROW_PREFIX.match(line.strip())
if match:
names.add(match.group(1))
return names


def load_dispatch_names() -> list[str]:
names: list[str] = []
for line in DISPATCH.read_text(encoding="utf-8").splitlines():
Expand Down Expand Up @@ -135,8 +182,28 @@ def main() -> int:
continue
missing.append(name)

matrix = load_flag_matrix_names()
no_rows = sorted(FLAG_MATRIX_REQUIRED - matrix)
not_dispatched = sorted(FLAG_MATRIX_REQUIRED - set(load_dispatch_names()))
unlisted = sorted(matrix - FLAG_MATRIX_REQUIRED)

stale_indirect = sorted(set(INDIRECT_COVERAGE) - set(used_indirect))
failed = False
if no_rows or not_dispatched or unlisted:
failed = True
for label, names in (
("Syscalls with no row left in tests/flag-matrix.tbl:", no_rows),
("FLAG_MATRIX_REQUIRED entries absent from dispatch.tbl:",
not_dispatched),
("Syscalls with rows but missing from FLAG_MATRIX_REQUIRED:",
unlisted),
):
if names:
print(label, file=sys.stderr)
for name in names:
print(f" - {name}", file=sys.stderr)
if missing or stale_indirect:
failed = True
if missing:
print("Uncovered syscalls in dispatch.tbl:", file=sys.stderr)
for name in missing:
Expand All @@ -149,9 +216,11 @@ def main() -> int:
)
for name in stale_indirect:
print(f" - {name}", file=sys.stderr)
if failed:
return 1

print("syscall coverage audit: PASS")
print(f" flag matrix: {len(matrix)} syscalls have per-flag rows")
for name, reason in sorted(used_indirect.items()):
print(f" indirect {name}: {reason}")
return 0
Expand Down
Loading
Loading