Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
12 changes: 11 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,17 @@ install(TARGETS prevail libbtf GSL
install(DIRECTORY "${prevail_source_dir}/src/"
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/prevail"
FILES_MATCHING PATTERN "*.hpp" PATTERN "*.h"
PATTERN "test/*" EXCLUDE
PATTERN "test" EXCLUDE
PATTERN "ir" EXCLUDE
)

# ir/ holds both public and internal headers; list the public ones explicitly.
install(FILES
"${prevail_source_dir}/src/ir/marshal.hpp"
"${prevail_source_dir}/src/ir/parse.hpp"
"${prevail_source_dir}/src/ir/program.hpp"
"${prevail_source_dir}/src/ir/syntax.hpp"
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/prevail/ir"
)

# Install convenience header to top-level include/
Expand Down
44 changes: 25 additions & 19 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ ELF Binary -> Unmarshal -> Build CFG -> Abstract Interpretation -> Result
3. **Semantic Translation**: Map hardware opcodes to semantic IR

The unmarshaller handles:

- Instruction decoding (opcode, registers, immediate values)
- Wide instruction handling (64-bit immediates span two instructions)
- Endianness conversion
Expand All @@ -36,6 +37,7 @@ The CFG builder transforms the linear instruction sequence into a control flow g
4. **Function Inlining**: Inline local function calls with stack frame prefixes

Key transformations:

- Conditional jumps split into two paths with explicit `Assume` instructions
- Loop heads are identified for widening
- Exit nodes connect to a special exit label
Expand All @@ -57,6 +59,7 @@ The core verification uses forward abstract interpretation:
**Files**: `src/result.cpp`

The analysis produces:

- **Invariants**: Pre/post states at each program point
- **Errors**: List of safety violations
- **Exit value**: Range of possible return values in R0
Expand All @@ -81,24 +84,24 @@ EbpfDomain = TypeToNumDomain × ArrayDomain

Implements transfer functions for each instruction type:

| Instruction | Semantic Effect |
|-------------|-----------------|
| `Bin` (ADD, SUB, ...) | Update numeric constraints |
| `Mem` (load/store) | Read/write array domain |
| `Call` | Apply helper function contracts |
| `Assume` | Refine domain with branch condition |
| `Jmp` | No state change (control flow only) |
| Instruction | Semantic Effect |
|-----------------------|-------------------------------------|
| `Bin` (ADD, SUB, ...) | Update numeric constraints |
| `Mem` (load/store) | Read/write array domain |
| `Call` | Apply helper function contracts |
| `Assume` | Refine domain with branch condition |
| `Jmp` | No state change (control flow only) |

### EbpfChecker (Assertion Verification)

Verifies safety properties by checking domain entailment:

| Assertion | Property Checked |
|-----------|------------------|
| `ValidAccess` | Memory access within bounds |
| `ValidStore` | Type-correct store operation |
| `ValidDivisor` | Non-zero divisor |
| `BoundedLoopCount` | Loop iteration limit |
| Assertion | Property Checked |
|--------------------|------------------------------|
| `ValidAccess` | Memory access within bounds |
| `ValidStore` | Type-correct store operation |
| `ValidDivisor` | Non-zero divisor |
| `BoundedLoopCount` | Loop iteration limit |

## Data Flow

Expand Down Expand Up @@ -141,6 +144,7 @@ Verifies safety properties by checking domain entailment:
### 1. Forward Analysis

Prevail uses forward (rather than backward) analysis because:

- eBPF programs have a single entry point
- Memory safety depends on tracking pointer provenance from entry
- Type information flows naturally forward
Expand All @@ -149,20 +153,23 @@ Prevail uses forward (rather than backward) analysis because:
### 2. Composite Domain

The domain hierarchy enables:

- **Type-guided precision**: Different numeric tracking per pointer type
- **Efficient joins**: Type mismatches detected early
- **Modular extension**: New pointer types can be added

### 3. Weak Topological Ordering

WTO-based iteration provides:

- **Efficient convergence**: Widening applied only at loop heads
- **Nested loop handling**: Inner loops stabilize before outer
- **Deterministic order**: Reproducible analysis results

### 4. Assertion-Based Checking

Separating assertions from semantics enables:

- **Modular safety properties**: Easy to add new checks
- **Precise error reporting**: Knows exactly which property failed
- **Configurable strictness**: Can enable/disable specific checks
Expand All @@ -178,6 +185,7 @@ Separating assertions from semantics enables:
```

Options:

- `-q`/`--quiet`: No stdout output, exit code only
- `--cfg`: Print control-flow graph and exit
- `--failure-slice`: Print causal trace for failures
Expand All @@ -190,20 +198,18 @@ Options:
```cpp
#include "ebpf_verifier.hpp"

// Load and verify
auto raw_progs = read_elf(filename, section, options, platform);
auto prog = Program::from_sequence(instructions, info, options);
auto result = analyze(prog);

// Check result
if (!result.failed) {
std::vector<std::vector<std::string>> notes;
auto prog = Program::from_raw(raw_progs.front(), notes, options);
if (prog && verify(*prog)) {
// Program is safe
}
```

## Thread Safety

The verifier uses thread-local storage for:

- **Variable registry**: Maps variable names to indices
- **Global program counter**: Tracks current instruction during analysis

Expand Down
1 change: 0 additions & 1 deletion src/ebpf_verifier.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,5 @@
#include "config.hpp"
#include "io/elf_loader.hpp"
#include "ir/program.hpp"
#include "ir/unmarshal.hpp"
#include "platform.hpp"
#include "verifier.hpp"
11 changes: 11 additions & 0 deletions src/ir/cfg_builder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "config.hpp"
#include "ir/program.hpp"
#include "ir/syntax.hpp"
#include "ir/unmarshal.hpp"
#include "platform.hpp"

using std::optional;
Expand Down Expand Up @@ -715,6 +716,16 @@ Program Program::from_sequence(const InstructionSeq& inst_seq, const ProgramInfo
return std::move(builder.prog);
}

std::optional<Program> Program::from_raw(const RawProgram& raw_prog, std::vector<std::vector<std::string>>& notes,
const ebpf_verifier_options_t& options) {
auto inst_seq = unmarshal(raw_prog, notes, options);
if (!inst_seq.has_value()) {
notes.push_back({std::move(inst_seq).error()});
return std::nullopt;
}
return from_sequence(*inst_seq, raw_prog.info, options);
}

std::set<BasicBlock> BasicBlock::collect_basic_blocks(const Cfg& cfg, const bool simplify) {
if (!simplify) {
std::set<BasicBlock> res;
Expand Down
15 changes: 15 additions & 0 deletions src/ir/program.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
#pragma once

#include <map>
#include <optional>
#include <ostream>
#include <string>
#include <vector>

#include "cfg/cfg.hpp"
#include "cfg/label.hpp"
#include "config.hpp"
#include "crab_utils/debug.hpp"
#include "ir/syntax.hpp"
#include "spec/type_descriptors.hpp"

namespace prevail {
class Program {
Expand Down Expand Up @@ -55,6 +59,12 @@ class Program {

static Program from_sequence(const InstructionSeq& inst_seq, const ProgramInfo& info,
const ebpf_verifier_options_t& options);

/// Build a Program directly from a raw (ELF-bytecode) representation.
/// On failure returns std::nullopt and appends the error message to `notes`
/// (the same vector that collects unmarshal warnings).
static std::optional<Program> from_raw(const RawProgram& raw_prog, std::vector<std::vector<std::string>>& notes,
const ebpf_verifier_options_t& options);
};

class InvalidControlFlow final : public std::runtime_error {
Expand All @@ -67,4 +77,9 @@ std::vector<Assertion> get_assertions(const Instruction& ins, const ProgramInfo&

void print_program(const Program& prog, std::ostream& os, bool simplify);
void print_dot(const Program& prog, const std::string& outfile);

/// Write a textual disassembly of `raw_prog` to `out`. On failure writes the
/// error message to `out` and returns false.
bool disassemble(const RawProgram& raw_prog, const ebpf_verifier_options_t& options, std::ostream& out,
const std::optional<Label>& label_to_print = {}, bool print_line_info = false);
} // namespace prevail
2 changes: 1 addition & 1 deletion src/ir/syntax.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ inline std::ostream& operator<<(std::ostream& os, Value const& a) {
std::ostream& operator<<(std::ostream& os, const Assertion& a);
std::string to_string(const Assertion& constraint);

void print(const InstructionSeq& insts, std::ostream& out, const std::optional<const Label>& label_to_print,
void print(const InstructionSeq& insts, std::ostream& out, const std::optional<Label>& label_to_print,
bool print_line_info = false);

int size(const Instruction& inst);
Expand Down
14 changes: 7 additions & 7 deletions src/ir/unmarshal.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Prevail Verifier contributors.
// SPDX-License-Identifier: MIT
#include <cassert>
#include <expected>
#include <iostream>
#include <string>
#include <vector>
Expand Down Expand Up @@ -782,8 +783,7 @@ struct Unmarshaller {
}
}

vector<LabeledInstruction> unmarshal(vector<EbpfInst> const& insts,
const prevail::ebpf_verifier_options_t& options) {
vector<LabeledInstruction> unmarshal(vector<EbpfInst> const& insts, const ebpf_verifier_options_t& options) {
options.validate();
subprogram_stack_size = options.subprogram_stack_size;
vector<LabeledInstruction> prog;
Expand Down Expand Up @@ -882,20 +882,20 @@ struct Unmarshaller {
}
};

std::variant<InstructionSeq, std::string> unmarshal(const RawProgram& raw_prog, vector<vector<string>>& notes,
const prevail::ebpf_verifier_options_t& options) {
std::expected<InstructionSeq, std::string> unmarshal(const RawProgram& raw_prog, vector<vector<string>>& notes,
const ebpf_verifier_options_t& options) {
thread_local_program_info = raw_prog.info;
try {
return Unmarshaller{notes, raw_prog.info}.unmarshal(raw_prog.prog, options);
} catch (InvalidInstruction& arg) {
std::ostringstream ss;
ss << arg.pc << ": " << arg.what() << "\n";
return ss.str();
return std::unexpected(ss.str());
}
}

std::variant<InstructionSeq, std::string> unmarshal(const RawProgram& raw_prog,
const prevail::ebpf_verifier_options_t& options) {
std::expected<InstructionSeq, std::string> unmarshal(const RawProgram& raw_prog,
const ebpf_verifier_options_t& options) {
vector<vector<string>> notes;
return unmarshal(raw_prog, notes, options);
}
Expand Down
13 changes: 7 additions & 6 deletions src/ir/unmarshal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// SPDX-License-Identifier: MIT
#pragma once

#include <expected>

@coderabbitai coderabbitai Bot Apr 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== C++ standard configuration =="
rg -n --iglob 'CMakeLists.txt' --iglob '*.cmake' 'CMAKE_CXX_STANDARD|CXX_STANDARD|cxx_std_'

echo
echo "== expected feature usage / compatibility shims =="
rg -n 'std::expected|<expected>|tl::expected|expected.hpp|std::unexpected'

Repository: vbpf/prevail

Length of output: 1136


🏁 Script executed:

cd examples/using_installed_package && cat -n CMakeLists.txt

Repository: vbpf/prevail

Length of output: 727


🏁 Script executed:

grep -r "unmarshal\|ir" examples/using_installed_package/ --include="*.cpp" --include="*.hpp" --include="*.h"

Repository: vbpf/prevail

Length of output: 173


🏁 Script executed:

cat -n examples/using_installed_package/main.cpp

Repository: vbpf/prevail

Length of output: 1221


🏁 Script executed:

cat -n src/prevail.hpp | head -50

Repository: vbpf/prevail

Length of output: 103


🏁 Script executed:

grep -n "unmarshal" src/prevail.hpp

Repository: vbpf/prevail

Length of output: 104


🏁 Script executed:

find . -name "prevail.hpp" -type f

Repository: vbpf/prevail

Length of output: 38


🏁 Script executed:

git ls-files | grep -E "\.hpp$" | head -20

Repository: vbpf/prevail

Length of output: 535


🏁 Script executed:

git ls-files | grep -i "prevail" | grep -E "\.(hpp|h)$"

Repository: vbpf/prevail

Length of output: 38


🏁 Script executed:

rg -n "unmarshal" --iglob "*.hpp" --iglob "*.h" src/

Repository: vbpf/prevail

Length of output: 977


🏁 Script executed:

cat -n src/ebpf_verifier.hpp

Repository: vbpf/prevail

Length of output: 372


🏁 Script executed:

grep -rn "public\|install" CMakeLists.txt | head -30

Repository: vbpf/prevail

Length of output: 577


🏁 Script executed:

cat -n CMakeLists.txt | sed -n '240,265p'

Repository: vbpf/prevail

Length of output: 1084


🏁 Script executed:

cat -n src/ebpf_verifier.hpp

Repository: vbpf/prevail

Length of output: 372


C++23 features in public headers break C++20 consumer builds.

The project is configured for C++23 (CMakeLists.txt:33), but unmarshal.hpp exposes std::expected (C++23 only) in the public API via ebpf_verifier.hpp. This prevents C++20-configured consumers like examples/using_installed_package from using the installed library headers. Either restrict C++23 features to implementation-only files, provide a C++20-compatible compatibility layer, or document this as a breaking requirement for library consumers.

🧰 Tools
🪛 Clang (14.0.6)

[error] 5-5: 'expected' file not found

(clang-diagnostic-error)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ir/unmarshal.hpp` at line 5, unmarshal.hpp currently exposes the C++23
header <expected> to public APIs (via ebpf_verifier.hpp); remove the direct
public dependency by introducing a small compatibility alias (e.g.,
expected_compat.hpp) that conditionally uses std::expected when __cplusplus >=
202302L and falls back to a header-only tl::expected (or a project-local
Expected<T,E>) for C++20, then replace `#include` <expected> in unmarshal.hpp with
`#include` "expected_compat.hpp" and update any public function signatures that
reference std::expected to use the compatibility alias (e.g., Expected<T,E>) so
C++20 consumers can include the installed headers.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Alan-Jowett do we need unmarshal.hpp as part of the API?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we currently call prevail::unmarshal() directly in 4 places across verifier_service.cpp and Verifier.cpp, and ebpf_verifier.hpp includes unmarshal.hpp. However, in all cases we immediately pass the InstructionSeq to the next step (Program::from_sequence, print, or analyze) — we never use InstructionSeq on its own.

So if prevail provided higher-level APIs that accept RawProgram directly (e.g., Program::from_raw(RawProgram, notes, options) that internalizes the unmarshal step, and a disassemble-to-stream function), we could stop depending on unmarshal.hpp and InstructionSeq entirely. That would also resolve the C++23 public API concern since std::expected would stay internal.

Our project currently builds with C++20, so exposing std::expected in public headers would require us to bump our standard.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like this?
https://github.com/vbpf/prevail/pull/1074/changes#diff-6714101bca0d9472baf09f9c8eaf5708d12f6fa8f5dc3160e52f319eb2e32310

/// Build a Program directly from a raw (ELF-bytecode) representation.
/// On failure returns std::nullopt and appends the error message to `notes`
/// (the same vector that collects unmarshal warnings).
static std::optional<Program> from_raw(const RawProgram& raw_prog, std::vector<std::vector<std::string>>& notes,
                                       const ebpf_verifier_options_t& options);
/// Write a textual disassembly of `raw_prog` to `out`. On failure writes the
/// error message to `out` and returns false.
bool disassemble(const RawProgram& raw_prog, const ebpf_verifier_options_t& options, std::ostream& out,
                 const std::optional<Label>& label_to_print = {}, bool print_line_info = false);

#include <string>
#include <variant>
#include <vector>

#include "ir/syntax.hpp"
Expand All @@ -17,13 +17,14 @@ namespace prevail {
*
* \param raw_prog is the input program to parse.
* \param[out] notes is a vector for storing errors and warnings.
* \param options unmarshalling options
* \return a sequence of instructions if successful, an error string otherwise.
*/
std::variant<InstructionSeq, std::string> unmarshal(const RawProgram& raw_prog,
std::vector<std::vector<std::string>>& notes,
const prevail::ebpf_verifier_options_t& options);
std::variant<InstructionSeq, std::string> unmarshal(const RawProgram& raw_prog,
const prevail::ebpf_verifier_options_t& options);
std::expected<InstructionSeq, std::string> unmarshal(const RawProgram& raw_prog,
std::vector<std::vector<std::string>>& notes,
const ebpf_verifier_options_t& options);
std::expected<InstructionSeq, std::string> unmarshal(const RawProgram& raw_prog,
const ebpf_verifier_options_t& options);

Call make_call(int imm, const ebpf_platform_t& platform);
} // namespace prevail
18 changes: 11 additions & 7 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@
#include <ranges>
#include <vector>

#include "ebpf_verifier.hpp"
#include "config.hpp"
#include "io/elf_loader.hpp"
#include "ir/program.hpp"
#include "ir/unmarshal.hpp"
#include "platform.hpp"
#include "result.hpp"
#include "verifier.hpp"

// Avoid affecting other headers by macros.
#include <CLI11/CLI11.hpp>
Expand Down Expand Up @@ -201,16 +206,15 @@ int main(int argc, char** argv) {
const RawProgram& raw_prog = raw_progs.back();

// Convert the raw program section to a set of instructions.
std::variant<InstructionSeq, std::string> prog_or_error = unmarshal(raw_prog, ebpf_verifier_options);
if (auto prog = std::get_if<string>(&prog_or_error)) {
std::cout << "unmarshaling error at " << *prog << "\n";
const auto inst_seq = unmarshal(raw_prog, ebpf_verifier_options);
if (!inst_seq.has_value()) {
std::cout << "unmarshaling error at " << inst_seq.error() << "\n";
return 1;
}

auto& inst_seq = std::get<InstructionSeq>(prog_or_error);
if (!asmfile.empty()) {
std::ofstream out{asmfile};
print(inst_seq, out, {});
print(*inst_seq, out, {});
print_map_descriptors(thread_local_program_info->map_descriptors, out);
}

Expand All @@ -226,7 +230,7 @@ int main(int argc, char** argv) {
}
}
const auto verbosity = ebpf_verifier_options.verbosity_opts;
const Program prog = Program::from_sequence(inst_seq, raw_prog.info, ebpf_verifier_options);
const Program prog = Program::from_sequence(*inst_seq, raw_prog.info, ebpf_verifier_options);

if (!dotfile.empty()) {
print_dot(prog, dotfile);
Expand Down
15 changes: 14 additions & 1 deletion src/printing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
#include "crab/interval.hpp"
#include "crab/type_encoding.hpp"
#include "crab/var_registry.hpp"
#include "ir/program.hpp"
#include "ir/syntax.hpp"
#include "ir/unmarshal.hpp"
#include "platform.hpp"
#include "spec/function_prototypes.hpp"
#include "verifier.hpp"
Expand Down Expand Up @@ -670,7 +672,7 @@ auto get_labels(const InstructionSeq& insts) {
return pc_of_label;
}

void print(const InstructionSeq& insts, std::ostream& out, const std::optional<const Label>& label_to_print,
void print(const InstructionSeq& insts, std::ostream& out, const std::optional<Label>& label_to_print,
const bool print_line_info) {
const auto pc_of_label = get_labels(insts);
Pc pc = 0;
Expand Down Expand Up @@ -725,6 +727,17 @@ void print_map_descriptors(const std::vector<EbpfMapDescriptor>& descriptors, st
}
}

bool disassemble(const RawProgram& raw_prog, const ebpf_verifier_options_t& options, std::ostream& out,
const std::optional<Label>& label_to_print, const bool print_line_info) {
auto inst_seq = unmarshal(raw_prog, options);
if (!inst_seq.has_value()) {
out << "unmarshaling error at " << inst_seq.error();
return false;
}
print(*inst_seq, out, label_to_print, print_line_info);
return true;
}
Comment thread
elazarg marked this conversation as resolved.

std::ostream& operator<<(std::ostream& os, const btf_line_info_t& line_info) {
os << "; " << line_info.file_name << ":" << line_info.line_number << "\n";
os << "; " << line_info.source_line << "\n";
Expand Down
Loading
Loading