Skip to content

Commit 78fa4c4

Browse files
authored
feat: start phase-2 correctness gates with e2e matrix and parser fuzz (#5)
1 parent 17b202a commit 78fa4c4

7 files changed

Lines changed: 357 additions & 6 deletions

File tree

.github/workflows/ci.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,36 @@ jobs:
310310
crash-*
311311
build-fuzz/crash-*
312312
313+
parser-fuzz:
314+
runs-on: ubuntu-24.04
315+
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
316+
steps:
317+
- uses: actions/checkout@v4
318+
with:
319+
fetch-depth: 0
320+
321+
- name: Install dependencies
322+
run: |
323+
sudo apt-get update
324+
sudo apt-get install -y clang llvm libbpf-dev pkg-config cmake ninja-build
325+
326+
- name: Configure with fuzzing
327+
env:
328+
CC: clang
329+
CXX: clang++
330+
run: cmake -S . -B build-fuzz -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DENABLE_FUZZING=ON -DBUILD_TESTING=OFF -DSKIP_BPF_BUILD=ON
331+
332+
- name: Build parser fuzz targets
333+
run: cmake --build build-fuzz --target fuzz_policy fuzz_bundle fuzz_network fuzz_path fuzz_event
334+
335+
- name: Run parser fuzz gate on changed files
336+
env:
337+
BASE_REF: ${{ github.base_ref }}
338+
DIFF_RANGE: ${{ github.event_name == 'push' && format('{0}...{1}', github.event.before, github.sha) || '' }}
339+
BUILD_DIR: build-fuzz
340+
FUZZ_SECONDS: "120"
341+
run: scripts/run_parser_fuzz_changed.sh
342+
313343
sbom:
314344
runs-on: ubuntu-24.04
315345
steps:

.github/workflows/e2e.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,8 @@ jobs:
4545
- name: E2E bypass tests
4646
run: sudo ./build/aegisbpf_bypass_test
4747

48+
- name: E2E file enforcement matrix (30+ checks)
49+
run: sudo PRESERVE_TMP_ON_FAIL=1 ./scripts/e2e_file_enforcement_matrix.sh
50+
4851
- name: Short soak reliability
4952
run: sudo AEGIS_BIN=./build/aegisbpf DURATION_SECONDS=180 MAX_RINGBUF_DROPS=100 MAX_RSS_GROWTH_KB=65536 scripts/soak_reliability.sh

.github/workflows/kernel-matrix.yml

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,60 @@ on:
44
workflow_dispatch:
55
schedule:
66
- cron: "0 5 * * 0"
7+
pull_request:
8+
paths:
9+
- "bpf/**"
10+
- "src/bpf_ops.cpp"
11+
- "src/bpf_ops.hpp"
12+
- "src/policy.cpp"
13+
- "src/policy.hpp"
14+
- "src/network_ops.cpp"
15+
- "src/network_ops.hpp"
16+
- "src/commands_block_allow.cpp"
17+
- "scripts/e2e_file_enforcement_matrix.sh"
18+
- ".github/workflows/kernel-matrix.yml"
19+
- ".github/workflows/e2e.yml"
720

821
jobs:
9-
kernel-matrix:
22+
kernel-matrix-pr:
23+
if: github.event_name == 'pull_request'
24+
strategy:
25+
fail-fast: false
26+
matrix:
27+
runner: [kernel-5.15, kernel-6.1]
28+
runs-on: [self-hosted, "${{ matrix.runner }}"]
29+
steps:
30+
- uses: actions/checkout@v4
31+
32+
- name: Install dependencies (if apt-get is available)
33+
run: |
34+
if command -v apt-get >/dev/null 2>&1; then
35+
sudo apt-get update
36+
sudo apt-get install -y clang llvm bpftool libbpf-dev libsystemd-dev pkg-config cmake ninja-build python3-jsonschema
37+
fi
38+
39+
- name: Verify environment
40+
run: scripts/verify_env.sh --strict
41+
42+
- name: Configure
43+
run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON
44+
45+
- name: Build
46+
run: cmake --build build
47+
48+
- name: Run tests
49+
run: ctest --test-dir build --output-on-failure --timeout 120
50+
51+
- name: Run file enforcement matrix
52+
run: sudo PRESERVE_TMP_ON_FAIL=1 ./scripts/e2e_file_enforcement_matrix.sh
53+
54+
kernel-matrix-weekly:
55+
if: github.event_name != 'pull_request'
1056
strategy:
1157
fail-fast: false
1258
matrix:
1359
runner: [kernel-5.15, kernel-6.1, kernel-6.5, kernel-5.14]
14-
runs-on: [self-hosted, ${{ matrix.runner }}]
60+
runs-on: [self-hosted, "${{ matrix.runner }}"]
1561
steps:
1662
- uses: actions/checkout@v4
1763

@@ -33,3 +79,6 @@ jobs:
3379

3480
- name: Run tests
3581
run: ctest --test-dir build --output-on-failure --timeout 120
82+
83+
- name: Run file enforcement matrix
84+
run: sudo PRESERVE_TMP_ON_FAIL=1 ./scripts/e2e_file_enforcement_matrix.sh

.github/workflows/nightly-fuzz.yml

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ name: Nightly Fuzz
22

33
on:
44
workflow_dispatch:
5+
inputs:
6+
max_total_time:
7+
description: "Per-target fuzz duration in seconds"
8+
default: "600"
9+
required: false
510
schedule:
611
- cron: "0 2 * * *"
712

@@ -23,9 +28,22 @@ jobs:
2328
run: cmake -S . -B build-fuzz -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DENABLE_FUZZING=ON -DBUILD_TESTING=OFF -DSKIP_BPF_BUILD=ON
2429

2530
- name: Build
26-
run: cmake --build build-fuzz
31+
run: cmake --build build-fuzz --target fuzz_policy fuzz_bundle fuzz_network fuzz_path fuzz_event
2732

28-
- name: Run fuzzers (short)
33+
- name: Run fuzzers (nightly deep)
34+
env:
35+
MAX_TOTAL_TIME: ${{ inputs.max_total_time || '600' }}
2936
run: |
30-
./build-fuzz/fuzz_policy -max_total_time=60
31-
./build-fuzz/fuzz_bundle -max_total_time=60
37+
for target in fuzz_policy fuzz_bundle fuzz_network fuzz_path fuzz_event; do
38+
echo "Fuzzing ${target} for ${MAX_TOTAL_TIME}s"
39+
./build-fuzz/${target} -max_total_time="${MAX_TOTAL_TIME}" -print_final_stats=1
40+
done
41+
42+
- name: Upload fuzz crash artifacts
43+
if: failure()
44+
uses: actions/upload-artifact@v4
45+
with:
46+
name: nightly-fuzz-crashes
47+
path: |
48+
crash-*
49+
build-fuzz/crash-*

docs/PRODUCTION_READINESS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,13 @@ For phase-by-phase numeric gates and the current MVP freeze contract, see
5858
- Security automation: `.github/workflows/security.yml` (CodeQL, dependency review, gitleaks)
5959
- Dependency update automation: `.github/dependabot.yml` (GitHub Actions) + `renovate.json` (CMake/regex-managed deps)
6060
- E2E workflow: `.github/workflows/e2e.yml`
61+
- Kernel file-enforcement matrix (>30 checks): `scripts/e2e_file_enforcement_matrix.sh` (run from `.github/workflows/e2e.yml` and `.github/workflows/kernel-matrix.yml`)
6162
- Soak reliability workflow: `.github/workflows/soak.yml` + `scripts/soak_reliability.sh`
6263
- Staging canary workflow: `.github/workflows/canary.yml` + `scripts/canary_gate.sh`
6364
- Perf regression workflow: `.github/workflows/perf.yml`
6465
- Kernel matrix workflow: `.github/workflows/kernel-matrix.yml`
6566
- Nightly fuzz workflow: `.github/workflows/nightly-fuzz.yml`
67+
- Parser-change fuzz gate: `parser-fuzz` job in `.github/workflows/ci.yml` + `scripts/run_parser_fuzz_changed.sh`
6668
- Release drill workflow: `.github/workflows/release-drill.yml` + `scripts/release_drill.sh`
6769
- Upgrade/migration runbook: `docs/UPGRADE.md`
6870
- Key rotation/revocation runbook: `docs/KEY_MANAGEMENT.md`
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
BIN="${BIN:-./build/aegisbpf}"
5+
PRESERVE_TMP_ON_FAIL="${PRESERVE_TMP_ON_FAIL:-0}"
6+
7+
declare -i TOTAL_CHECKS=0
8+
declare -i PASSED_CHECKS=0
9+
declare -i FAILED_CHECKS=0
10+
11+
AGENT_PID=""
12+
TMP_DIR=""
13+
LOG_FILE=""
14+
15+
cleanup() {
16+
local exit_code=$?
17+
if [[ -n "${AGENT_PID}" ]]; then
18+
kill "${AGENT_PID}" 2>/dev/null || true
19+
wait "${AGENT_PID}" 2>/dev/null || true
20+
AGENT_PID=""
21+
fi
22+
if [[ -n "${TMP_DIR}" && -d "${TMP_DIR}" ]]; then
23+
if [[ "${PRESERVE_TMP_ON_FAIL}" == "1" && ${exit_code} -ne 0 ]]; then
24+
echo "Preserving failed run artifacts at ${TMP_DIR}" >&2
25+
else
26+
rm -rf "${TMP_DIR}"
27+
fi
28+
fi
29+
}
30+
trap cleanup EXIT
31+
32+
pass() {
33+
local label="$1"
34+
TOTAL_CHECKS=$((TOTAL_CHECKS + 1))
35+
PASSED_CHECKS=$((PASSED_CHECKS + 1))
36+
echo "[PASS] ${label}"
37+
}
38+
39+
fail() {
40+
local label="$1"
41+
local detail="$2"
42+
TOTAL_CHECKS=$((TOTAL_CHECKS + 1))
43+
FAILED_CHECKS=$((FAILED_CHECKS + 1))
44+
echo "[FAIL] ${label}: ${detail}" >&2
45+
}
46+
47+
run_expect_success() {
48+
local label="$1"
49+
shift
50+
if "$@" >/dev/null 2>&1; then
51+
pass "${label}"
52+
else
53+
fail "${label}" "command failed unexpectedly"
54+
fi
55+
}
56+
57+
run_expect_blocked() {
58+
local label="$1"
59+
shift
60+
if "$@" >/dev/null 2>&1; then
61+
fail "${label}" "command succeeded (expected block)"
62+
else
63+
pass "${label}"
64+
fi
65+
}
66+
67+
require_prereqs() {
68+
if [[ $EUID -ne 0 ]]; then
69+
echo "Must run as root (BPF LSM enforcement tests)." >&2
70+
exit 1
71+
fi
72+
73+
if [[ ! -x "${BIN}" ]]; then
74+
echo "Agent binary not found at ${BIN}. Build first." >&2
75+
exit 1
76+
fi
77+
78+
if [[ ! -f /sys/fs/cgroup/cgroup.controllers ]]; then
79+
echo "cgroup v2 is required at /sys/fs/cgroup." >&2
80+
exit 1
81+
fi
82+
83+
if ! grep -qw bpf /sys/kernel/security/lsm 2>/dev/null; then
84+
echo "BPF LSM is not enabled; this suite requires enforce-capable kernel." >&2
85+
exit 1
86+
fi
87+
}
88+
89+
start_agent() {
90+
local enforce_signal="$1"
91+
LOG_FILE="${TMP_DIR}/agent-${enforce_signal}.log"
92+
"${BIN}" run --enforce --enforce-signal="${enforce_signal}" >"${LOG_FILE}" 2>&1 &
93+
AGENT_PID=$!
94+
sleep 1
95+
if ! kill -0 "${AGENT_PID}" 2>/dev/null; then
96+
echo "Agent failed to start for signal=${enforce_signal}. Log:" >&2
97+
cat "${LOG_FILE}" >&2 || true
98+
exit 1
99+
fi
100+
}
101+
102+
stop_agent() {
103+
if [[ -n "${AGENT_PID}" ]]; then
104+
kill "${AGENT_PID}" 2>/dev/null || true
105+
wait "${AGENT_PID}" 2>/dev/null || true
106+
AGENT_PID=""
107+
fi
108+
}
109+
110+
expected_action_for_signal() {
111+
case "$1" in
112+
none)
113+
echo "BLOCK"
114+
;;
115+
term)
116+
echo "TERM"
117+
;;
118+
int)
119+
echo "INT"
120+
;;
121+
*)
122+
echo "Unsupported enforce signal: $1" >&2
123+
exit 1
124+
;;
125+
esac
126+
}
127+
128+
run_signal_suite() {
129+
local signal="$1"
130+
local expected_action="$2"
131+
local scenario_dir="${TMP_DIR}/${signal}"
132+
local target="${scenario_dir}/target.txt"
133+
local symlink_path="${scenario_dir}/target.symlink"
134+
local hardlink_path="${scenario_dir}/target.hardlink"
135+
136+
mkdir -p "${scenario_dir}"
137+
printf 'signal=%s\n' "${signal}" >"${target}"
138+
ln -sf "${target}" "${symlink_path}"
139+
ln "${target}" "${hardlink_path}"
140+
141+
local inode
142+
inode="$(stat -c %i "${target}")"
143+
144+
start_agent "${signal}"
145+
146+
if ! "${BIN}" block add "${target}" >/dev/null 2>&1; then
147+
echo "Failed to add block rule for ${target}" >&2
148+
exit 1
149+
fi
150+
151+
# 10 blocked-open assertions per signal mode.
152+
run_expect_blocked "${signal}: cat direct" cat "${target}"
153+
run_expect_blocked "${signal}: head direct" head -c 1 "${target}"
154+
run_expect_blocked "${signal}: tail direct" tail -c 1 "${target}"
155+
run_expect_blocked "${signal}: dd direct" dd if="${target}" of=/dev/null bs=1 count=1 status=none
156+
run_expect_blocked "${signal}: grep direct" grep -m1 . "${target}"
157+
run_expect_blocked "${signal}: python direct" python3 -c "import pathlib,sys; pathlib.Path(sys.argv[1]).read_bytes()" "${target}"
158+
run_expect_blocked "${signal}: cat symlink" cat "${symlink_path}"
159+
run_expect_blocked "${signal}: head symlink" head -c 1 "${symlink_path}"
160+
run_expect_blocked "${signal}: cat hardlink" cat "${hardlink_path}"
161+
run_expect_blocked "${signal}: dd hardlink" dd if="${hardlink_path}" of=/dev/null bs=1 count=1 status=none
162+
163+
sleep 1
164+
run_expect_success "${signal}: expected action logged" grep -q "\"action\":\"${expected_action}\"" "${LOG_FILE}"
165+
run_expect_success "${signal}: inode logged" grep -q "\"ino\":${inode}" "${LOG_FILE}"
166+
167+
"${BIN}" block del "${target}" >/dev/null 2>&1 || true
168+
stop_agent
169+
}
170+
171+
main() {
172+
require_prereqs
173+
174+
TMP_DIR="$(mktemp -d /tmp/aegisbpf_e2e_matrix.XXXXXX)"
175+
echo "Running kernel enforcement matrix using ${BIN}"
176+
echo "Workspace: ${TMP_DIR}"
177+
178+
local signal
179+
for signal in none term int; do
180+
run_signal_suite "${signal}" "$(expected_action_for_signal "${signal}")"
181+
done
182+
183+
echo
184+
echo "E2E matrix summary: passed=${PASSED_CHECKS} failed=${FAILED_CHECKS} total=${TOTAL_CHECKS}"
185+
if ((FAILED_CHECKS > 0)); then
186+
exit 1
187+
fi
188+
}
189+
190+
main "$@"

0 commit comments

Comments
 (0)