Skip to content

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mac-overhead-benchmark

A microbenchmark framework for measuring the performance overhead introduced by Linux Mandatory Access Control (MAC) mechanisms at the system call level, with nanosecond precision.

The framework auto-detects the active MAC by reading /sys/kernel/security/lsm and records it in every output file. The bundled policies cover AppArmor and SELinux for both allow and deny scenarios.


What this framework measures

Each run measures the latency of a system call under one MAC, enforcing an explicit policy decision (allow or deny). Every iteration is validated: if the syscall outcome doesn't match the expected decision, it is flagged. This catches the most common pitfall in MAC benchmarking - running a "deny" experiment with no policy actually loaded.

Benchmark categories

Category Syscalls LSM hook
network socket() + connect() security_socket_create, security_socket_connect
ptrace ptrace(PTRACE_ATTACH) security_ptrace_access_check
setuid setuid() / seteuid() security_task_fix_setuid

Execution modes

Mode Description
continuous Back-to-back iterations - steady-state, warm-cache latency
delayed 10s sleep between iterations - cold CPU caches and TLB

Policy decisions

Decision What's measured
allow MAC permits the syscall - permit-path overhead
deny MAC blocks the syscall - rejection-path overhead

Why openSUSE Leap 16

This framework targets openSUSE Leap 16 because it is the only mainstream distribution that supports both AppArmor and SELinux on the same kernel, filesystem, and userland. This makes a fair head-to-head comparison possible: only the MAC changes between runs, while every other variable (kernel version, filesystem, libc, background services, compiler) stays identical.

This controlled-environment property is essential for the benchmark to attribute measured overhead to the MAC itself rather than to distribution differences.

On Leap 16:

  • SELinux is the default MAC (enforcing, production-grade policy)
  • AppArmor is supported and installed manually (zypper in -t pattern apparmor)

Recommended VM setup (VMware on Windows 11 host)

This project is developed against VMware Workstation running on a Windows 11 Home host (KVM is Linux-only and unavailable on Windows; Hyper-V needs Windows Pro). VMware is the best low-overhead option on Windows Home, and VMware Workstation Pro is now free for personal use.

VM specifications

Component Recommended Reason
OS openSUSE Leap 16 Both AppArmor and SELinux on one kernel
vCPU 1 core, 1 socket Eliminates cross-core scheduler jitter
RAM 2 GB Enough for full policy; avoids memory pressure
Disk 20 GB, pre-allocated Avoids dynamic-growth delays on CSV writes
Hypervisor VMware Workstation Lowest-overhead option on Windows Home

VMware VM settings

  • Processor: 1 core / 1 socket; enable "Virtualize Intel VT-x/EPT or AMD-V/RVI"
  • Memory: 2 GB
  • Disk: 20 GB, "Allocate all disk space now" (pre-allocated)
  • Options: "Synchronize guest time with host" OFF, 3D acceleration OFF

Important: disable Windows Hyper-V components

Windows 11 may silently enable Hyper-V (Core Isolation / Memory Integrity / WSL2 / Virtual Machine Platform). If active, VMware runs in a slower compatibility mode on top of Hyper-V, adding an extra virtualization layer and noise. To run VMware with direct hardware virtualization:

# PowerShell as Administrator, then restart Windows:
bcdedit /set hypervisorlaunchtype off

Re-enable later with bcdedit /set hypervisorlaunchtype auto. (Note: turning it off also disables WSL2.)

Before every benchmark session

Run the preflight check inside the VM:

sudo bash scripts/preflight_check.sh --fix

And on the Windows host:

  • Close other applications; pause Windows Defender real-time scanning
  • Pause Windows Update
  • On a laptop: plug in power, set Windows power mode to "Best performance"
  • Do not use the machine for anything else while the benchmark runs

Why this is still valid despite running in a VM

The benchmark reports relative overhead - (mean_MAC − mean_baseline) / mean_baseline. Any fixed cost added by the hypervisor appears in both the baseline and the MAC runs, so it cancels out in the ratio. Residual noise from host preemption is removed by IQR outlier filtering and absorbed by the large iteration count (10,000). The measured overhead percentages are therefore comparable to what you would get on bare metal, even though the absolute nanosecond values are higher.


Quick start

git clone https://github.com/stevjoo/mac-overhead-benchmark
cd mac-overhead-benchmark

# One-time setup: installs deps, AppArmor pattern, SELinux devel tools (--replacefiles avoids flatpak-selinux conflict)
sudo bash scripts/setup_leap16.sh

# Build and install the binary
make
sudo make install

Running the experiment on Leap 16

Switching the active MAC on Leap 16 requires editing GRUB and rebooting, so the full experiment spans three boots - one per MAC. The framework handles this with a reboot-aware runner: each time you run it, it detects the currently active MAC and runs that phase, loading the correct allow/deny policy automatically.

# ---- Boot 1: SELinux (the Leap 16 default) ----
sudo bash scripts/switch_mac.sh selinux && sudo reboot
# after reboot:
sudo bash scripts/run_experiment.sh

# ---- Boot 2: AppArmor ----
sudo bash scripts/switch_mac.sh apparmor && sudo reboot
# after reboot:
sudo bash scripts/run_experiment.sh

# ---- Boot 3: Baseline (no MAC) ----
sudo bash scripts/switch_mac.sh baseline && sudo reboot
# after reboot:
sudo bash scripts/run_experiment.sh

> [!WARNING]  
> **Execution Time:** The `run_experiment.sh` script runs both *Continuous* and *Delayed* benchmarks. The delayed phase sleeps for 10 seconds per iteration (100 iterations per test) to ensure cold CPU caches. This means the script will appear completely frozen/silent for about **50 minutes** per MAC phase. **Do not interrupt it**; just let it finish.

# ---- Analyze all three ----
python3 scripts/analyze_results.py --indir results/combined/

After each run, the script prints which phases are done and which remain:

Collected so far:
  [DONE] selinux
  [TODO] apparmor
  [TODO] baseline

For a quick trial, reduce iteration counts:

sudo bash scripts/run_experiment.sh --iters-cont 1000 --iters-delay 20

Verifying the active MAC after each reboot

cat /proc/cmdline          # check the security= parameter
getenforce                 # SELinux: Enforcing / Permissive / Disabled
aa-status                  # AppArmor: profile count
./mac_benchmark --help     # prints detected MAC at the top

Manual usage

sudo ./mac_benchmark [OPTIONS]
Option Values Default Description
--iters N int ≥ 10 10000 Iterations per run
--outdir DIR path ./results CSV output directory
--bench TYPE all network ptrace setuid all Benchmark category
--mode MODE all continuous delayed all Execution mode
--policy POL all allow deny all Policy decision
--help - - Show help

There is no --mac flag - the active MAC is detected at runtime.


How DENY measurements stay honest

A common failure in MAC benchmarking: running a "DENY" experiment with no policy applied. The syscalls succeed normally and you accidentally measure ALLOW overhead labeled as DENY.

This framework prevents that two ways:

  1. Bundled policies in policies/ are loaded automatically by the experiment script before each phase.
  2. Per-iteration outcome validation checks whether each syscall behaved as expected. Runs below 95% validation are flagged and excluded from plots.

If a DENY run wasn't actually denied, you'll see:

!!! WARNING: 100.0% of iterations had unexpected outcome.
    For DENY policy with MAC=baseline, this likely means:
      - The MAC policy that should block this syscall is NOT loaded
    Results from this run are NOT VALID for overhead analysis.

Repository structure

.
├── src/                          C source
│   ├── benchmark.h
│   ├── main.c                    CLI + orchestration
│   ├── core/{stats,runner}.c     statistics + lifecycle
│   ├── benchmarks/               network / ptrace / setuid
│   ├── output/{csv_writer,report}.c
│   └── utils/sysinfo.c           MAC + kernel + distro detection
├── policies/
│   ├── apparmor/
│   │   ├── mac_benchmark.allow   permissive profile (ALLOW runs)
│   │   └── mac_benchmark.deny    blocks net/ptrace/setuid (DENY runs)
│   └── selinux/
│       ├── mac_benchmark.te      type enforcement (allow/deny via boolean)
│       ├── mac_benchmark.fc      file context (binary label)
│       └── Makefile              build the .pp module
├── scripts/
│   ├── setup_leap16.sh           one-time system setup
│   ├── preflight_check.sh        verify VM is configured for clean runs
│   ├── switch_mac.sh             switch active MAC via GRUB (needs reboot)
│   ├── run_experiment.sh         reboot-aware phase runner
│   └── analyze_results.py        analysis + plots
├── Makefile
└── README.md

Output

Per-run: raw_<category>_<mode>_<policy>_<mac>.csv

iteration, latency_ns, syscall_ret, errno_val, outcome_ok,
mac_type, category, mode, policy, kernel_ver, distro, timestamp

Aggregated: summary.csv

label, mac_type, category, mode, policy, iterations,
valid_samples, outliers_removed, expected_outcomes, unexpected_outcomes,
mean_ns, median_ns, stddev_ns, min_ns, max_ns, p95_ns, p99_ns,
kernel_ver, distro, timestamp

Plots from analyze_results.py

File Content
overhead_bar.png Overhead (%) per category, by mode × policy
latency_boxplot.png Latency distribution per MAC
allow_vs_deny.png Allow vs deny mean latency per MAC

Design decisions

MAC detection via /sys/kernel/security/lsm

Reads the kernel's list of loaded LSMs, filters non-MAC modules, uses the rest.

CLOCK_MONOTONIC_RAW

Immune to NTP and adjtime() slew. Direct hardware clock for nanosecond deltas.

Per-iteration outcome validation

Each syscall result is checked against the expected policy decision, catching misconfiguration that would silently invalidate measurements.

IQR outlier removal

Syscall latency is right-skewed; IQR is more robust than z-score.

Continuous vs Delayed

Continuous = warm caches, steady state. Delayed = cold CPU caches and TLB between iterations. (Note: delayed does NOT expire the SELinux AVC, which has no TTL.)

ptrace measures only ATTACH

PEEKDATA and DETACH on an attached process don't re-run the LSM check; timing only ATTACH isolates MAC overhead.

setuid uses fork + drop-privilege

A child drops to nobody then attempts setuid(0), making DENY measurements meaningful (a non-root process attempting escalation is rejected).


Limitations

  • Running inside a VM introduces hypervisor preemption noise, mitigated by IQR filtering and large iteration counts.
  • delayed mode cools CPU caches but does not expire the SELinux AVC.
  • Results are specific to the policy complexity used; a larger/more complex policy may produce different overhead.
  • The comparison is valid within one distribution (Leap 16). Cross-distribution results would introduce kernel/filesystem/userland confounds.

About

Microbenchmark framework for measuring Mandatory Access Control (AppArmor, SELinux) syscall overhead with nanosecond precision, statistical analysis, and automated reporting.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages