|
| 1 | +--- |
| 2 | +name: cws-iouring-coverage |
| 3 | +description: Audit CWS (runtime-security) io_uring functional-test coverage and add a functional test for any io_uring opcode whose operation CWS observes but that is not exercised through io_uring. Test-driven — coverage is judged by tests, never by reading eBPF/hook internals. Use when auditing io_uring test coverage or after new IORING_OP_ opcodes appear. |
| 4 | +--- |
| 5 | + |
| 6 | +# CWS io_uring test coverage audit |
| 7 | + |
| 8 | +For every io_uring operation CWS observes, there must be a functional test that issues the |
| 9 | +operation *through io_uring* and asserts the event fires. This skill finds the gaps and fills |
| 10 | +them. |
| 11 | + |
| 12 | +## Principle: tests are the only arbiter |
| 13 | + |
| 14 | +Coverage is decided by tests, nothing else. **Do not read eBPF programs or C hooks** (anything |
| 15 | +under `pkg/security/ebpf/`) to judge coverage — implementation is out of scope and misleads. |
| 16 | +The only question is: *does an io_uring test exist for this operation, and does it pass?* |
| 17 | + |
| 18 | +The one io_uring-specific fact (it's a test assertion, not implementation): io_uring completes |
| 19 | +asynchronously, so **every io_uring test must assert `event.async == true`**. |
| 20 | + |
| 21 | +## The audit: build three lists, then intersect |
| 22 | + |
| 23 | +**List A — io_uring opcodes.** `WebFetch https://man7.org/linux/man-pages/man2/io_uring_enter.2.html` |
| 24 | +and list every `IORING_OP_*` with the syscall it performs (e.g. `IORING_OP_SOCKET` → `socket(2)`). |
| 25 | +Offline fallback: the iouring-go fork is `replace`d in `go.mod` — import path stays |
| 26 | +`github.com/iceber/iouring-go`, but source on disk is under |
| 27 | +`$(go env GOMODCACHE)/github.com/lebauce/iouring-go@*/syscall/types.go`. |
| 28 | + |
| 29 | +**List B — tested syscalls.** Read `pkg/security/tests/`. A syscall is *tested* when a test |
| 30 | +issues it **as the triggering action** (inside the `func() error {…}` passed to |
| 31 | +`WaitSignal`/`runSyscallTester`) **and asserts an event** — not when it only appears as setup. |
| 32 | +Build the list from test bodies, not event names: the asserted event may be named differently. |
| 33 | + |
| 34 | +**List C — tested io_uring opcodes.** `grep -rn 'iouring\.\|io_uring' pkg/security/tests/*.go`. |
| 35 | +Mostly `t.Run("io_uring", …)` subtests plus a few standalone funcs. |
| 36 | + |
| 37 | +**Conclude, per opcode in A:** |
| 38 | +- syscall ∉ B → **out of scope** (CWS doesn't observe it; no io_uring test expected). |
| 39 | +- syscall ∈ B → **in scope**; then opcode ∈ C → **tested**, else → **gap**. |
| 40 | + |
| 41 | +Write a test for every gap. |
| 42 | + |
| 43 | +## Writing the io_uring test |
| 44 | + |
| 45 | +Model on an existing subtest (`open_test.go` → `t.Run("io_uring", …)`). For each gap: |
| 46 | + |
| 47 | +1. **Rule scope.** The test process is `testsuite`. Many rules are scoped to |
| 48 | + `process.file.name == "syscall_tester"`; reuse the parent rule only if it already admits |
| 49 | + `testsuite` (e.g. `in [ "syscall_tester", "testsuite" ]`), otherwise add a new rule scoped to |
| 50 | + `process.file.name == "testsuite"`. Check the parent's `ruleDefs` first. |
| 51 | +2. **Submit.** `iour, err := iouring.New(1)`; submit the op; read the result; in the validation |
| 52 | + callback assert event type, key fields, and `event.async == true`. |
| 53 | +3. **Kernel gate, not errno skip.** Gate "kernel too old for this opcode" deterministically at |
| 54 | + the top of the subtest: |
| 55 | + `checkKernelCompatibility(t, "io_uring <op> needs Linux X.Y", func(kv *kernel.Version) bool { return kv.Code < kernel.VersionCode(X, Y, 0) })`. |
| 56 | + Don't skip on a negative errno — a malformed raw SQE returns one too, so skipping would hide |
| 57 | + the gap behind a green test. On a supported kernel, treat an unexpected negative result as a |
| 58 | + **failure** (`return fmt.Errorf(...)`). (A library prep helper can't be malformed, so an |
| 59 | + errno skip there is harmless.) |
| 60 | +4. **ebpfless.** Only matters if the parent test is in the `available` list (`~`-prefixed |
| 61 | + entries) in `pkg/security/tests/main_linux.go` — those prefix-match subtests and pull yours |
| 62 | + into the ebpfless run, where io_uring is unsupported. If so, add an `exclude` entry. The |
| 63 | + match is exact on the full `t.Name()`, so prefer a flat sibling name |
| 64 | + (`TestOpen/io_uring_ftruncate`, not a nested `TestOpen/io_uring/ftruncate`). |
| 65 | + |
| 66 | +**No prep helper in the fork?** The fork wraps only a subset. For other opcodes build a raw SQE |
| 67 | +with a custom `iouring.PrepRequest` using the helpers in `pkg/security/tests/iouring_test.go` |
| 68 | +(extend them as needed). This is the one place you may touch an opcode's low-level shape. |
| 69 | + |
| 70 | +## Verify |
| 71 | + |
| 72 | +- `gofmt -l <your files>` (no output = OK). |
| 73 | +- Run the test via the harness: |
| 74 | + `dda inv security-agent.functional-tests --skip-linters --testflags="-test.run <YourTest>"`. |
| 75 | + Green = covered, red = the gap is real. |
| 76 | + |
| 77 | +## Report |
| 78 | + |
| 79 | +Report the per-opcode classification (out of scope / tested / gap) and the test files |
| 80 | +added or changed. |
0 commit comments