Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
17 changes: 17 additions & 0 deletions .clusterfuzzlite/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# ClusterFuzzLite / OSS-Fuzz build image for the AegisBPF userspace fuzzers.
# The userspace library (policy parser, event/JSON decoders, network rules,
# signed-bundle parser) is the attacker-influenced input boundary; BPF object
# compilation is skipped here (SKIP_BPF_BUILD=ON) since fuzzers target userspace.
FROM gcr.io/oss-fuzz-base/base-builder:latest

RUN apt-get update && apt-get install -y --no-install-recommends \
cmake \
ninja-build \
pkg-config \
libbpf-dev \
zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*

COPY . $SRC/aegisbpf
WORKDIR $SRC/aegisbpf
COPY .clusterfuzzlite/build.sh $SRC/build.sh
27 changes: 27 additions & 0 deletions .clusterfuzzlite/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/bash -eu
#
# OSS-Fuzz / ClusterFuzzLite build script for AegisBPF userspace fuzzers.
#
# The fuzzing engine and sanitizer come from the OSS-Fuzz/CFLite environment
# ($CC/$CXX/$CFLAGS/$CXXFLAGS/$LIB_FUZZING_ENGINE). CMake honors $LIB_FUZZING_ENGINE
# (see the ENABLE_FUZZING block in CMakeLists.txt) and links it instead of the
# local-only -fsanitize=fuzzer,address. We do not set CMAKE_BUILD_TYPE so the
# injected $CXXFLAGS fully control optimization/instrumentation.

cd "$SRC/aegisbpf"

cmake -S . -B build-fuzz -G Ninja \
-DENABLE_FUZZING=ON \
-DBUILD_TESTING=OFF \
-DSKIP_BPF_BUILD=ON

FUZZERS="fuzz_policy fuzz_bundle fuzz_network fuzz_path fuzz_event"
cmake --build build-fuzz --target ${FUZZERS}

for f in ${FUZZERS}; do
cp "build-fuzz/${f}" "${OUT}/"
# Ship checked-in seed corpora as <fuzzer>_seed_corpus.zip when present.
if [ -d "tests/fuzz/corpus/${f}" ]; then
(cd "tests/fuzz/corpus/${f}" && zip -q -r "${OUT}/${f}_seed_corpus.zip" .)
fi
done
7 changes: 7 additions & 0 deletions .clusterfuzzlite/project.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
language: c++
main_repo: "https://github.com/ErenAri/Aegis-BPF"
sanitizers:
- address
- undefined
fuzzing_engines:
- libfuzzer
43 changes: 43 additions & 0 deletions .github/workflows/cflite-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: ClusterFuzzLite PR

# Free, in-repo continuous fuzzing of the userspace input-parsing surface.
# Builds the libFuzzer harnesses in the OSS-Fuzz base image and fuzzes only the
# code changed by the PR (mode: code-change), failing the check on a new crash.
# No run: steps and no untrusted event input are used (injection-safe).
on:
pull_request:
paths:
- 'src/**'
- 'tests/fuzz/**'
- '.clusterfuzzlite/**'
- 'CMakeLists.txt'

permissions: read-all

concurrency:
group: cflite-pr-${{ github.ref }}
cancel-in-progress: true

jobs:
PR:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
sanitizer: [address, undefined]
steps:
- name: Build fuzzers (${{ matrix.sanitizer }})
id: build
uses: google/clusterfuzzlite/actions/build_fuzzers@v1
Comment thread
ErenAri marked this conversation as resolved.
with:
language: c++
sanitizer: ${{ matrix.sanitizer }}
- name: Run fuzzers (${{ matrix.sanitizer }})
id: run
uses: google/clusterfuzzlite/actions/run_fuzzers@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
fuzz-seconds: 300
mode: 'code-change'
sanitizer: ${{ matrix.sanitizer }}
output-sarif: true
106 changes: 70 additions & 36 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,42 @@ if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebI
endif()

# Security hardening flags
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_FORTIFY_SOURCE=2 -fstack-protector-strong -fPIE")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_FORTIFY_SOURCE=2 -fstack-protector-strong -fPIE")
#
# Levels are probed for compiler support so the cross-compiler/cross-arch matrix
# (GCC/Clang, x86_64/arm64) degrades gracefully instead of failing the build.
# Skipped for fuzzing builds, where the sanitizer/fuzzing-engine toolchain owns
# the compile/link flags (FORTIFY can interfere with sanitizer instrumentation).
if(NOT ENABLE_FUZZING)
include(CheckCCompilerFlag)
include(CheckCXXCompilerFlag)

# _FORTIFY_SOURCE: prefer level 3 (GCC 12+/Clang 9+ with glibc >= 2.34), else 2.
check_cxx_compiler_flag("-D_FORTIFY_SOURCE=3" AEGIS_HAS_FORTIFY3)
if(AEGIS_HAS_FORTIFY3)
set(AEGIS_FORTIFY_LEVEL 3)
else()
set(AEGIS_FORTIFY_LEVEL 2)
endif()

set(AEGIS_HARDEN_FLAGS "-D_FORTIFY_SOURCE=${AEGIS_FORTIFY_LEVEL} -fstack-protector-strong -fPIE")

# Stack clash protection (large-stack-probe guard; GCC 8+/Clang 11+, x86_64/arm64).
check_cxx_compiler_flag("-fstack-clash-protection" AEGIS_HAS_STACK_CLASH)
if(AEGIS_HAS_STACK_CLASH)
string(APPEND AEGIS_HARDEN_FLAGS " -fstack-clash-protection")
endif()

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${AEGIS_HARDEN_FLAGS}")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${AEGIS_HARDEN_FLAGS}")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pie -Wl,-z,relro,-z,now")

# libstdc++ hardened mode (C++ only): bounds/precondition assertions in std:: containers.
check_cxx_compiler_flag("-D_GLIBCXX_ASSERTIONS" AEGIS_HAS_GLIBCXX_ASSERTIONS)
if(AEGIS_HAS_GLIBCXX_ASSERTIONS)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_GLIBCXX_ASSERTIONS")
endif()
endif() # NOT ENABLE_FUZZING

# Sanitizer configuration
if(ENABLE_ASAN)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
Expand Down Expand Up @@ -479,6 +511,7 @@ if(BUILD_TESTING)
# Test sources
set(TEST_SOURCES
tests/test_main.cpp
tests/test_logging.cpp
tests/test_commands.cpp
tests/test_crash_policy.cpp
tests/test_crypto_safe.cpp
Expand Down Expand Up @@ -733,6 +766,12 @@ if(BUILD_TESTING)
)
set_tests_properties(cli_run_rejects_invalid_enforce_signal PROPERTIES WILL_FAIL TRUE)

add_test(
NAME cli_run_rejects_invalid_enforce_fallback
COMMAND $<TARGET_FILE:aegisbpf> run --enforce --enforce-fallback=bogus
)
set_tests_properties(cli_run_rejects_invalid_enforce_fallback PROPERTIES WILL_FAIL TRUE)

add_test(
NAME cli_run_rejects_sigkill_without_allow_gate
COMMAND $<TARGET_FILE:aegisbpf> run --enforce --enforce-signal=kill
Expand All @@ -756,40 +795,35 @@ endif()
if(ENABLE_FUZZING)
message(STATUS "Building fuzzing targets")

# Policy parser fuzzer
add_executable(fuzz_policy tests/fuzz/fuzz_policy_parser.cpp)
target_link_libraries(fuzz_policy PRIVATE aegisbpf_lib)
target_include_directories(fuzz_policy PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_compile_options(fuzz_policy PRIVATE -fsanitize=fuzzer,address -fno-omit-frame-pointer)
target_link_options(fuzz_policy PRIVATE -fsanitize=fuzzer,address)

# Signed bundle parser fuzzer
add_executable(fuzz_bundle tests/fuzz/fuzz_signed_bundle.cpp)
target_link_libraries(fuzz_bundle PRIVATE aegisbpf_lib)
target_include_directories(fuzz_bundle PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_compile_options(fuzz_bundle PRIVATE -fsanitize=fuzzer,address -fno-omit-frame-pointer)
target_link_options(fuzz_bundle PRIVATE -fsanitize=fuzzer,address)

# Network rules fuzzer
add_executable(fuzz_network tests/fuzz/fuzz_network_rules.cpp)
target_link_libraries(fuzz_network PRIVATE aegisbpf_lib)
target_include_directories(fuzz_network PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_compile_options(fuzz_network PRIVATE -fsanitize=fuzzer,address -fno-omit-frame-pointer)
target_link_options(fuzz_network PRIVATE -fsanitize=fuzzer,address)

# Path validation fuzzer
add_executable(fuzz_path tests/fuzz/fuzz_path_validation.cpp)
target_link_libraries(fuzz_path PRIVATE aegisbpf_lib)
target_include_directories(fuzz_path PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_compile_options(fuzz_path PRIVATE -fsanitize=fuzzer,address -fno-omit-frame-pointer)
target_link_options(fuzz_path PRIVATE -fsanitize=fuzzer,address)

# Event handling fuzzer
add_executable(fuzz_event tests/fuzz/fuzz_event_handling.cpp)
target_link_libraries(fuzz_event PRIVATE aegisbpf_lib)
target_include_directories(fuzz_event PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_compile_options(fuzz_event PRIVATE -fsanitize=fuzzer,address -fno-omit-frame-pointer)
target_link_options(fuzz_event PRIVATE -fsanitize=fuzzer,address)
# libFuzzer harness name -> source mapping (flat pairs for list(POP_FRONT)).
set(AEGIS_FUZZ_TARGETS
fuzz_policy tests/fuzz/fuzz_policy_parser.cpp
fuzz_bundle tests/fuzz/fuzz_signed_bundle.cpp
fuzz_network tests/fuzz/fuzz_network_rules.cpp
fuzz_path tests/fuzz/fuzz_path_validation.cpp
fuzz_event tests/fuzz/fuzz_event_handling.cpp
)

# OSS-Fuzz / ClusterFuzzLite inject the fuzzing engine and sanitizers via
# $LIB_FUZZING_ENGINE + $CFLAGS/$CXXFLAGS. When that is present we must NOT
# hardcode -fsanitize=fuzzer,address (it conflicts with their toolchain);
# for local/standalone builds we wire libFuzzer + ASan ourselves.
set(AEGIS_FUZZ_ENGINE "$ENV{LIB_FUZZING_ENGINE}")

while(AEGIS_FUZZ_TARGETS)
list(POP_FRONT AEGIS_FUZZ_TARGETS _fuzz_name _fuzz_src)
add_executable(${_fuzz_name} ${_fuzz_src})
target_link_libraries(${_fuzz_name} PRIVATE aegisbpf_lib)
target_include_directories(${_fuzz_name} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_compile_options(${_fuzz_name} PRIVATE -fno-omit-frame-pointer)
if(AEGIS_FUZZ_ENGINE)
# Engine + sanitizer supplied by the OSS-Fuzz/CFLite environment.
target_link_options(${_fuzz_name} PRIVATE ${AEGIS_FUZZ_ENGINE})
else()
target_compile_options(${_fuzz_name} PRIVATE -fsanitize=fuzzer,address)
target_link_options(${_fuzz_name} PRIVATE -fsanitize=fuzzer,address)
endif()
endwhile()
endif()

# Installation
Expand Down
29 changes: 17 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,12 @@ Legend: ✅ full · ◐ partial · ❌ absent
| Tracepoint audit when LSM absent | ✅ | ✅ | ✅ | ✅ | ◐ |
| File enforcement | ✅ Kernel deny | ❌ Detect only | ✅ | ◐ | ✅ |
| Network enforcement (full socket lifecycle) | ✅ connect/bind/listen/accept/sendmsg/recvmsg | ❌ | ✅ | ◐ | ◐ |
| **OverlayFS `inode_copy_up` propagation** | | ❌ | ❌ | ❌ | ❌ |
| **OverlayFS `inode_copy_up` detection + async re-propagation** | ◐ async | ❌ | ❌ | ❌ | ❌ |
| **IMA-backed trusted exec** (kernel 6.1+) | ✅ `bpf_ima_file_hash` | ❌ | ◐ | ❌ | ❌ |
| Process ancestry + argv | ✅ 4 MB priority ringbuf | ◐ | ✅ | ✅ | ◐ |
| Cgroup-scoped policy | ✅ inode / IPv4 / port | ◐ | ✅ | ◐ | ✅ |
| LPM CIDR v4/v6 network deny | ✅ | ◐ | ✅ | ◐ | ◐ |
| Ptrace / module-load / BPF syscall blocking | ✅ all three | ❌ | ◐ | ❌ | ◐ |
| Ptrace / module-load / BPF syscall blocking | ✅ ptrace + bpf · ◐ module (needs kernel lockdown) | ❌ | ◐ | ❌ | ◐ |
| Policy evaluation | O(1) BPF map lookup | O(rules) userspace | In-kernel TracingPolicy | Hybrid signatures | In-kernel + userspace |
| Policy language | INI + K8s CRD | YAML DSL | K8s CRD TracingPolicy | Rego / Go signatures | K8s CRD KubeArmorPolicy |
| Break-glass / deadman-TTL | ✅ Emergency + revert | ❌ | ❌ | ❌ | ❌ |
Expand All @@ -167,8 +167,11 @@ Legend: ✅ full · ◐ partial · ❌ absent
### Where AegisBPF is uniquely differentiated today

- **OverlayFS copy-up propagation.** No other open-source runtime
security agent enforces on `lsm/inode_copy_up`. This closes a
real container-escape bypass class.
security agent hooks `lsm/inode_copy_up` at all. AegisBPF detects the
copy-up synchronously, then re-propagates the deny rule to the new
upper-layer inode asynchronously from userspace (best-effort: a brief
window exists between copy-up and re-propagation). This addresses a
real container-escape bypass class that other agents miss entirely.
- **IMA-backed trusted exec identity.** Kernel 6.1+ `bpf_ima_file_hash()`
integration ties allow-listed execs to cryptographic file hashes inside
`bprm_check_security`.
Expand Down Expand Up @@ -239,14 +242,15 @@ Current flagship contract:
> cgroup-scoped workloads, with safe rollback and signed policy provenance.

Current scope labels:
- `ENFORCED`: file deny via LSM (`file_open` / `inode_permission`), OverlayFS
copy-up propagation via `inode_copy_up`, outbound network deny for configured
`connect()` / `sendmsg()` rules, inbound `recvmsg()` deny, port-oriented
`bind()` / `listen()` deny, accepted-peer `accept()` deny, cgroup-scoped
inode/IPv4/port deny rules, and IMA-backed exec hash trust on kernel 6.1+
when those LSM hooks/helpers are available
- `ENFORCED`: file deny via LSM (`file_open` / `inode_permission`), outbound
network deny for configured `connect()` / `sendmsg()` rules, inbound
`recvmsg()` deny, port-oriented `bind()` / `listen()` deny, accepted-peer
`accept()` deny, cgroup-scoped inode/IPv4/port deny rules, and IMA-backed
exec hash trust on kernel 6.1+ when those LSM hooks/helpers are available
- `AUDITED`: tracepoint fallback path (no syscall deny), detailed metrics mode,
forensic block events with UID/username and exec identity
forensic block events with UID/username and exec identity, and OverlayFS
copy-up detection via `inode_copy_up` (the deny rule is re-propagated to the
new upper-layer inode asynchronously from userspace, so a brief window exists)
- `PLANNED`: broader runtime surfaces beyond current documented hooks

## Validation Results
Expand All @@ -265,7 +269,7 @@ Current scope labels:
| **Binary Hardening** | VERIFIED | FORTIFY_SOURCE, stack-protector, PIE, full RELRO |

**Security Hardening Applied:**
- Compiler security flags (FORTIFY_SOURCE=2, stack-protector-strong, PIE, RELRO)
- Compiler security flags (FORTIFY_SOURCE=3, stack-protector-strong, stack-clash-protection, _GLIBCXX_ASSERTIONS, PIE, full RELRO) — probed per-compiler, fall back gracefully
- Timeout protection on BPF operations (prevents indefinite hangs)
- Secure temporary file creation via `mkstemp()` (symlink-attack resistant)
- Atomic file writes (write-rename pattern) for all persistent state
Expand All @@ -292,6 +296,7 @@ Public proof lives in the docs and CI artifacts:
- Kernel/CI execution model: `docs/CI_EXECUTION_STRATEGY.md`
- Kernel/distro compatibility: `docs/COMPATIBILITY.md`
- Threat model + non-goals: `docs/THREAT_MODEL.md`
- Memory-safety posture: `docs/MEMORY_SAFETY.md`
- Enforcement guarantees + TOCTOU analysis: `docs/GUARANTEES.md`
- Enforce posture guarantees contract: `docs/ENFORCEMENT_GUARANTEES.md`
- Emergency control contract: `docs/EMERGENCY_CONTROL_CONTRACT.md`
Expand Down
4 changes: 3 additions & 1 deletion bpf/aegis_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,8 @@ struct agent_config {
__u8 deny_ptrace; /* block ptrace attachment (MITRE T1055.008) */
__u8 deny_module_load; /* block kernel module loading (MITRE T1547.006) */
__u8 deny_bpf; /* block unauthorized BPF program load (MITRE T1562) */
__u8 _reserved[4]; /* alignment padding */
__u8 signal_fallback_enforce; /* enforce via bpf_send_signal on tracepoints when BPF-LSM is absent */
__u8 _reserved[3]; /* alignment padding */
};

/* Agent config is stored as a BPF global so programs can read it without a
Expand All @@ -364,6 +365,7 @@ volatile struct agent_config agent_cfg = {
.deny_ptrace = 0,
.deny_module_load = 0,
.deny_bpf = 0,
.signal_fallback_enforce = 0,
._reserved = {0},
};

Expand Down
Loading