lamprov is a CHERI-inspired C++ library that stores a provenance label in the upper bits of a pointer, using Intel's Linear Address Masking (LAM) feature. A label marks memory as secret, pii, or untrusted, and adds an instance number. Pointers derived from a labeled pointer keep its label, and a seccomp-BPF filter reads the label when a labeled pointer is passed to a syscall.
A layout sets where the label goes. init() selects the layout (InitOptions::layout) and turns on LAM with
ARCH_ENABLE_TAGGED_ADDR. Call init() before starting any threads, because the kernel refuses to turn on LAM once
the process has started a thread. Create arenas after calling init().
| Layout | Label | Requires |
|---|---|---|
U57 (LayoutKind::lam, the default) |
pointer bits 62:57: one bit each for secret, pii, and untrusted in bits 57 to 59, and an instance from 0 to 7 in bits 62:60 | LAM_U57, kernel tier u57 or u48 |
U48 (LayoutKind::u48) |
pointer bits 62:48: the same fields, with values up to 32767 | LAM_U48, kernel tier u48 |
init() and lamprov probe find the kernel tier by passing a labeled buffer to read and write:
| Tier | Meaning |
|---|---|
none |
LAM is not available |
rejects_labels |
a syscall fails with EFAULT on a labeled buffer, even when the filter allows the call |
u57 |
syscalls accept labeled pointers, with 6 tag bits |
u48 |
as u57, and LAM_U48 is available, with 15 tag bits |
Without LAM, init() fails if InitOptions::require_lam is set. Otherwise arenas return unlabeled pointers, or, with
InitOptions::label_without_lam, labeled pointers that the filter can read but that fault when dereferenced.
Arenaallocates frommmapmemory, and every pointer it returns carries the arena's label. It has size classes, per-thread caches, and guard pages.LabeledAllocator,LabeledVector, andLabeledStringput container storage in an arena.policy::parse()andpolicy::compile()turn a.lamppolicy file into a chain of seccomp filters.seccomp::install()installs the chain.seccomp::install_report_handler()installs a SIGSYS handler that reports calls caught bytrapandreportclauses, with a backtrace.supervisor::Supervisorreports calls caught bynotifyclauses and checks the buffers of syscalls that take an array of buffers, such aswritevandsendmsg.audit::Session(in thelamprov_ebpflibrary) uses eBPF to report labeled pointers passed to syscalls and to library functions such asprintf.trace::Sessionuses eBPF to record where labeled pointers go, from an allocation site or label to a syscall or function argument.liblamprov_shim.sois a library forLD_PRELOAD. It turns on LAM beforemainand labels a thread's heap allocations while aScopeis active. In site mode, it labels each heap allocation by itsmalloccall site. It also counts copies from labeled into unlabeled buffers.
examples/policy.lamp is an example policy that uses every rule kind:
label secret = class(secret) label pii = class(pii) label untrusted = class(untrusted)
egress write pwrite64 send sendto : buffer arg 1
any(secret) -> errno(EPERM) report any(pii) -> notify
name-sink openat execve unlink connect # pointer args per signature table
any(untrusted) -> trap
indirect writev pwritev pwritev2 sendmsg sendmmsg process_vm_writev vmsplice -> notify
forbid io_uring_setup io_uring_enter io_uring_register
A label line names a set of labels, selected by class(C | ...), instance(N), or value(V). The rule kinds are:
egress: checks a buffer argument.name-sink: checks path name and socket address arguments.indirect: sends the call to the supervisor, which reads the buffer pointers from the array.forbid: gives every call the same action,errno(ENOSYS)by default.
A clause has the form any(LABEL) -> ACTION, and the actions are errno(E), errno(E) report, trap, kill, and
notify. The first matching clause decides the action, and calls that match no clause are allowed. The filter kills
the process on ia32 and x32 syscalls, whose 32-bit pointers carry no labels.
| Command | Effect |
|---|---|
lamprov compile [--stats] POLICY |
prints the BPF program of a policy, or its instruction counts and longest path |
lamprov run [--report-loss] --policy POLICY -- COMMAND |
runs COMMAND under the filter with the shim preloaded, and supervises it when the policy has notify clauses or indirect rules |
lamprov audit [--sinks FILE] --policy POLICY -- COMMAND |
reports labeled pointers that COMMAND passes to syscalls and library functions, when the policy selects their label |
lamprov trace [--dot FILE] [--json FILE] -- COMMAND |
runs COMMAND in site mode and writes a graph of where its labeled pointers go |
lamprov probe [--json] |
checks LAM support, how the kernel handles labeled pointers, and the kernel tier |
lamprov-bench [--quick] BENCHMARK|all |
runs the filter, alloc, and observers benchmarks |
lamprov probe takes these options for further checks:
--observers: what eBPF and/procsee of a labeled pointer.--propagation: whether libc and libstdc++ functions return pointers with the label intact.--filter-cost:writelatency with and without a labeled filter.--fwrite: whichfwritesizes pass the caller's pointer towrite.
The lamprov commands except probe take --layout u57|u48; probe runs its checks under U57. audit, trace, and
probe --observers require eBPF support, Linux 5.17 or later, kernel BTF, CAP_BPF, and CAP_PERFMON.
Requirements: CMake 3.22 or later, Ninja, and GCC 11+ or Clang 14+ on x86-64 Linux. Catch2 v3 and nlohmann_json come
from the system if find_package finds them, and from GitHub otherwise.
cmake --preset gcc # or: clang
cmake --build --preset gcc
ctest --preset gccThe gcc and clang presets build in Debug mode into build-gcc/ and build-clang/ with -Wall -Wextra -Werror.
The bench preset builds lamprov-bench in Release mode into build-bench/.
eBPF support requires libbpf 0.7.0 or later, a Clang with the BPF target, and bpftool. tools/bootstrap installs
them on Debian. The LAMPROV_BPF cache variable is AUTO (the default), ON, or OFF.
The project has no install target. lamprov run and lamprov trace load liblamprov_shim.so from the directory of
the lamprov executable, so copy both files from build-gcc/tools/lamprov/ into the same directory.
Tests that call init() or install a filter run in forked children. Each test belongs to a lane, a condition that the
test requires. If the condition fails, the test prints a line starting with LAMPROV_SKIPPED:, and CTest reports it
as skipped.
| Lane | Condition |
|---|---|
| host | x86-64 Linux with seccomp |
| lam | ARCH_ENABLE_TAGGED_ADDR with 6 bits succeeds |
| uaccess | lam, and the kernel tier is u57 or u48 |
| u48 | the kernel tier is u48, and ARCH_ENABLE_TAGGED_ADDR with 15 bits succeeds |
| bpf | eBPF support was built, and the process has CAP_BPF, CAP_PERFMON, kernel BTF, and the raw_syscalls tracepoints |
The checks catch accidental leaks by code that does not handle labels, such as third-party libraries, legacy modules, and bugs. They do not contain hostile code: any code in the process can clear label bits or copy data into unlabeled memory.