1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
|
# Sudo Hardening Plan for I/O Riot NG (ior)
## Problem Statement
The current build system (`Magefile.go`) silently wraps entire `go test` invocations—and several generation subcommands—with `sudo` when the invoking user is not root. This means:
1. **You must trust `mage`** (an arbitrary-code build tool) with root privileges.
2. **An attacker who can modify `Magefile.go`** (or any helper it imports) gets immediate privilege escalation.
3. **It is impossible to audit what actually ran as root**, because the `sudo` command is assembled dynamically inside Go code.
## Goal
Run `mage` **entirely as an unprivileged user**. Only the **minimal set of specific sub-processes** that genuinely require elevated privileges (`CAP_BPF` / root) should ever run under `sudo`, and they should be invoked with `sudo -n` so no interactive password prompt can be exploited by a malicious build script.
Granular `/etc/sudoers.d/` rules can then grant **password-less, command-locked** access to exactly those subprocesses.
## Threat Model
| Asset | Risk | Mitigation |
|---|---|---|
| `Magefile.go` | Malicious code injection → arbitrary root | `mage` never runs as root; `sudo` is only used for bounded, hard-coded subcommands |
| `go test` | `init()`, `_testmain.go`, or imported packages run arbitrary code at startup | Integration tests are compiled to a static binary first; only the final binary runs under `sudo` |
| `/sys/kernel/tracing` | Leak of kernel tracepoint data | Read-only access to public tracepoint format files (already world-readable on many systems, but the path traversal needs root on locked-down kernels) |
| `bpftool` | Arbitrary BPF object loading | Restricted to the single read-only invocation: `bpftool btf dump file /sys/kernel/btf/vmlinux format c` |
| `ior` binary | Full `CAP_BPF` access | Acceptable because it is the product being tested; access is via a compiled test binary with a deterministic path |
## What Needs Root (and Why)
| Operation | Needs root? | Reason |
|---|---|---|
| Compiling Go / BPF object | **No** | Pure toolchain work |
| `mage test` (unit tests) | **No** | Mocks / stubs; no real BPF attachment |
| `mage generate` / `mage world` | **Partially** | `bpftool btf dump ...` and reading `/sys/kernel/tracing/events/syscalls/*/format` |
| `mage integrationTest` | **Yes** | Spawns `./ior`, which attaches BPF tracepoints |
| `mage integrationTestSerial` | **Yes** | Same |
| `mage demo` / `mage demoOne` | **Yes** | Tapes launch `./ior` (BPF) and `pkill` |
| `mage installDemoTools` | **Yes** | `dnf install` system-wide package |
## Design Principles
1. **Compile as user, run binary as root.**
- Build the integration-test binary (`go test -c`) while fully unprivileged.
- Only the resulting `integrationtests.test` binary runs under `sudo`.
2. **Explicit `sudo -n` for every elevated command.**
- No silent wrapping.
- If `sudo -n` fails, the build fails with a clear error telling the admin which sudo rule is missing.
3. **Hard-code elevated commands; no dynamic construction.**
- The exact command line passed to `sudo` must be easily auditable in `Magefile.go`.
4. **Sudoers rules are command-granular.**
- Each rule locks the user to a single command with exact arguments (or wildcards only where strictly necessary).
## Changes to `Magefile.go`
### 1. Remove automatic `sudo` wrapping from `buildGoTestCmd`
Current behavior: when `os.Geteuid() != 0`, the function manufactures a `sudo env … go test …` command. **Delete this logic.** Going forward `buildGoTestCmd` returns a plain `go` command with no privilege elevation.
### 2. Add compiled-binary helper for integration tests
Two new helpers are introduced:
- `compileIntegrationTestBinary(env)` – runs `go test -c ./integrationtests/...` with the required `CGO_*` environment.
- `runIntegrationTestBinary(env, args…)` – elevates **only** the compiled binary via `sudo -n -E ./integrationtests.test …`, running it from the `integrationtests/` directory so that relative path resolution (`../ior`, `../ioworkload`) matches `go test` behaviour.
`IntegrationTest`, `IntegrationTestSerial`, and `testWithName` (when the target is an integration test) are updated to:
1. build `ior`, `ioworkload`, and compile the test binary as an unprivileged user;
2. invoke `runIntegrationTestBinary` to execute the tests under `sudo`.
### 3. Harden existing `sudo*` helpers
- `sudoOutput` is updated to prefix every elevated call with `sudo -n`.
- `sudoRunWithEnv` is updated to prefix every elevated call with `sudo -n env …`.
- `ensureSudoTimestamp` and `startSudoKeepalive` are **retained but restricted** to the `Demo` targets (they are harmless there, and the demo still needs a warm sudo timestamp).
### 4. What stays the same
- `mage build`, `mage test`, `mage testRace`, `mage generateTracepointsGo`, `mage generateTypesGo` – no root required.
- `ensureVMLINUX` and `readSyscallFormats` already call `sudoOutput` for discrete commands; they remain elevated, but now via `sudo -n`.
## Impact on Existing Workflows
| Before | After |
|---|---|
| `mage integrationTest` as non-root → auto `sudo env … go test …` | `mage integrationTest` as non-root → compiles as user, then `sudo -n -E ./integrationtests.test …` |
| Password prompt may appear mid-build | `sudo -n` fails fast if rule missing; no surprise prompts |
| JSON progress ticker from `go test -json` | Dropped for integration-test path; plain `-test.v` style output is printed directly. (The compiled test binary does not support the `go test` JSON protocol.) |
| `mage testWithName TEST_NAME=TestAioSetup` | Same compiled-binary dance when target is an integration test |
| `mage generate` | Unchanged UX; `sudo -n bpftool …` and `sudo -n sh -c 'cat /sys/kernel/tracing/…'` run automatically if sudoers rules exist |
## Files Modified
- `Magefile.go` – removes implicit sudo wrapping for `go test`; adds compiled-binary helpers.
- `docs/sudo-rules-for-ior.txt` – copy-paste rules for `/etc/sudoers.d/`.
## Deployment Steps for the Admin
1. As **root**, copy `docs/sudo-rules-for-ior.txt` into `/etc/sudoers.d/ior`.
2. Replace `%developers` with the actual Unix group or usernames that need to run tests.
3. Validate syntax: `visudo -c -f /etc/sudoers.d/ior`.
4. As **unprivileged user**, verify: `sudo -n bpftool btf dump file /sys/kernel/btf/vmlinux format c | head -3`.
5. Run `mage integrationTest` as the unprivileged user.
## Known Limitations
- **`mage demo`** is **not covered** by the granular rules. The demo tapes run a helper script (`run-tape.sh`) that performs multiple privileged actions (`pkill`, launching `./ior`). Running the demo still requires a broad `NOPASSWD` rule for that script, or running `mage demo` as root.
- **`mage installDemoTools`** requires `dnf` access and remains outside the scope of automated test rules.
- The compiled integration-test binary is invoked from the **repository root**. Sudoers rules using a wildcard path (`…/ior/integrationtests.test`) assume the repository is checked out somewhere predictable (e.g., `/home/<user>/git/ior/`). If the checkout lives in arbitrary locations, use a wrapper script at a fixed path.
|