diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-29 22:28:21 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-29 22:28:21 +0300 |
| commit | 5aadfad3a145de9967120065587d830f09ad87aa (patch) | |
| tree | c45532534ebb029bd55966ad5d94d13ecb3ed480 /internal/generate | |
| parent | 521964a730d828d63c324301deb206ea4b33089b (diff) | |
test(generate): lock in sync(2) void-but-returns classification
Audit of bare sync(2) per man 2 sync: void sync(void) takes no args and
returns no value. Confirmed it is correctly classified KindNull in
FamilyFS, its ret is UNCLASSIFIED, and — unlike noreturn exit/exit_group —
its exit handler IS emitted because sync does return (void != noreturn).
Docs and generated maps already match; no code or doc changes needed.
Add lock-in tests:
- TestGenerateSyncHandler: enter null_event with no arg capture (sync has
no args at all), live exit handler emitted, ret recorded UNCLASSIFIED.
- TestClassifyRetSyncUnclassified: meaningless void ret stays UNCLASSIFIED.
- TestSyncIsNotNoreturn: guards sync from the noreturn suppression list.
- Add sync (FamilyFS) to the family/exit-handler table test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'internal/generate')
| -rw-r--r-- | internal/generate/classify_test.go | 5 | ||||
| -rw-r--r-- | internal/generate/codegen_test.go | 69 |
2 files changed, 74 insertions, 0 deletions
diff --git a/internal/generate/classify_test.go b/internal/generate/classify_test.go index f0278e2..838d643 100644 --- a/internal/generate/classify_test.go +++ b/internal/generate/classify_test.go @@ -1757,6 +1757,11 @@ func TestClassifySyscallPairEmitsAllFamilies(t *testing.T) { {"listns", FormatListns, FormatExitListns, FamilyFS}, {"swapon", FormatSwapon, FormatExitSwapon, FamilyFS}, {"swapoff", FormatSwapoff, FormatExitSwapoff, FamilyFS}, + // Bare sync() takes no args and returns void, but it DOES return (it is + // not noreturn like exit/exit_group), so it belongs in FamilyFS and must + // still emit a live exit handler. Its fd-taking siblings (syncfs/fsync/ + // fdatasync/sync_file_range) are FamilyFS+KindFd and covered elsewhere. + {"sync", FormatSync, FormatExitSync, FamilyFS}, {"kill", FormatKill, FormatExitKill, FamilySignals}, {"exit_group", syntheticEnter("exit_group", 9316), syntheticExit("exit_group", 9315), FamilyProcess}, } diff --git a/internal/generate/codegen_test.go b/internal/generate/codegen_test.go index 0482dc3..814f114 100644 --- a/internal/generate/codegen_test.go +++ b/internal/generate/codegen_test.go @@ -624,6 +624,75 @@ func TestGenerateNullHandler(t *testing.T) { } } +// TestGenerateSyncHandler locks in how the bare sync(2) syscall is generated. +// Per sync(2): `void sync(void)` — it flushes all filesystem buffers to disk, +// takes NO arguments and returns NO value. This makes it distinct from its +// filesystem-sync siblings, which all take a leading fd and return int: +// - int syncfs(int fd) +// - int fsync(int fd) +// - int fdatasync(int fd) +// - int sync_file_range(int fd, off64_t off, off64_t n, unsigned flags) +// +// Because sync has no arguments, ior classifies it as KindNull in FamilyFS, so: +// - The enter handler emits a struct null_event and, since there are no args +// at all, must NOT reference ctx->args[...] anywhere in its body. +// - Crucially, although sync returns void, the syscall still *completes* and +// the kernel sys_exit_sync tracepoint fires with a (meaningless) ret field. +// Unlike the noreturn exit(2)/exit_group(2) syscalls, sync DOES return, so +// the generator must emit a live exit handler — it must NOT be suppressed. +// - The void return is recorded generically via EXIT_RET_EVENT and classified +// UNCLASSIFIED: it is not a byte count and must never be tagged +// READ/WRITE/TRANSFER. +func TestGenerateSyncHandler(t *testing.T) { + output := generateFromPair(t, FormatSync, FormatExitSync) + + enterSec := `SEC("tracepoint/syscalls/sys_enter_sync")` + exitSec := `SEC("tracepoint/syscalls/sys_exit_sync")` + + // Enter: null_event, no argument capture (sync takes no arguments). + requireContains(t, output, enterSec) + requireContains(t, output, "struct null_event *ev") + requireContains(t, output, "ev->event_type = ENTER_NULL_EVENT;") + requireContains(t, output, "ev->trace_id = SYS_ENTER_SYNC;") + + enterStart := strings.Index(output, enterSec) + exitStart := strings.Index(output, exitSec) + if enterStart < 0 || exitStart < 0 || exitStart <= enterStart { + t.Fatalf("sync: enter/exit handlers not found in expected order") + } + enterBody := output[enterStart:exitStart] + if strings.Contains(enterBody, "ctx->args[") { + t.Error("sync must be KindNull and takes no args: enter handler must not capture any arg") + } + + // Exit: sync is void but DOES return, so unlike exit/exit_group the exit + // handler must be emitted and report the meaningless ret as UNCLASSIFIED. + requireContains(t, output, exitSec) + requireContains(t, output, "ev->ret = ctx->ret;") + requireContains(t, output, "ev->ret_type = UNCLASSIFIED;") +} + +// TestClassifyRetSyncUnclassified locks in that the bare sync(2) return value is +// UNCLASSIFIED. sync returns void; the sys_exit_sync tracepoint still carries a +// ret field, but it is meaningless and certainly not a byte count, so it must +// stay UNCLASSIFIED — never READ/WRITE/TRANSFER. +func TestClassifyRetSyncUnclassified(t *testing.T) { + if got := ClassifyRet("sys_exit_sync"); got != Unclassified { + t.Errorf("sync ret classification = %q, want %q", got, Unclassified) + } +} + +// TestSyncIsNotNoreturn locks in that bare sync(2) is NOT treated as a noreturn +// syscall: it is void but returns control to userspace, so its exit handler must +// be generated (see TestGenerateSyncHandler). Only exit(2)/exit_group(2) are +// noreturn. This guards against sync accidentally being added to the noreturn +// suppression list, which would silently drop its exit events. +func TestSyncIsNotNoreturn(t *testing.T) { + if isNoreturnSyscall("sync") { + t.Error("sync must not be noreturn: it is void but DOES return, so its exit handler must be emitted") + } +} + func TestGenerateIoUringEnterHandler(t *testing.T) { output := generateFromPair(t, FormatIoUringEnter, FormatExitIoUringEnter) |
