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
24 changes: 24 additions & 0 deletions src/fwd_analyzer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// SPDX-License-Identifier: Apache-2.0
#include <cassert>
#include <ranges>
#include <set>
#include <utility>
#include <variant>
#include <vector>

#include "analysis_context.hpp"
#include "cfg/cfg.hpp"
Expand Down Expand Up @@ -142,6 +144,27 @@ class InterleavedFwdFixpointIterator final {
return std::numeric_limits<int>::max();
}

[[nodiscard]]
int max_call_depth() const {
int max_call_depth = 0;
std::set<Label> visited;
std::vector<Label> worklist{_cfg.entry_label()};
while (!worklist.empty()) {
const Label label = worklist.back();
worklist.pop_back();
if (!visited.insert(label).second) {
continue;
}

// Label depth includes the entry frame; callers need only BPF-to-BPF calls.
max_call_depth = std::max(max_call_depth, label.call_stack_depth() - 1);
for (const Label& child : _cfg.children_of(label)) {
worklist.push_back(child);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return max_call_depth;
}

public:
void operator()(const Label& node);

Expand Down Expand Up @@ -223,6 +246,7 @@ AnalysisResult InterleavedFwdFixpointIterator::run(const AnalysisContext& contex
const Program& prog = context.program;
AnalysisResult result;
InterleavedFwdFixpointIterator analyzer(context, result);
result.max_call_depth = analyzer.max_call_depth();
if (context.runtime().check_for_termination) {
analyzer._wto.for_each_loop_head(
[&](const Label& label) { ebpf_domain_initialize_loop_counter(entry_inv, label, context); });
Expand Down
1 change: 1 addition & 0 deletions src/result.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ struct AnalysisResult {
std::map<Label, InvariantMapPair> invariants;
bool failed = false;
int max_loop_count{};
int max_call_depth{};
Interval exit_value = Interval::top();

[[nodiscard]]
Expand Down
60 changes: 60 additions & 0 deletions src/test/test_cfg_builder_passes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "ir/program.hpp"
#include "ir/syntax.hpp"
#include "platform.hpp"
#include "verifier.hpp"

using namespace prevail;

Expand Down Expand Up @@ -207,6 +208,65 @@ TEST_CASE("pass_insert_termination_counters is off by default", "[passes]") {
}
}

TEST_CASE("AnalysisResult reports maximum nested BPF-to-BPF call depth", "[passes][stats]") {
const ProgramInfo info = default_info();

SECTION("does not count the entry frame") {
InstructionSeq seq;
seq.push_back(at(0, Exit{}));

const Program prog = Program::from_sequence(seq, info, {});
REQUIRE(analyze(prog, {}).max_call_depth == 0);
}

SECTION("counts nested local calls") {
InstructionSeq seq;
// entry -> call subprogram 1 -> call subprogram 2 -> exit.
seq.push_back(at(0, CallLocal{.target = Label{2}}));
seq.push_back(at(1, Exit{}));
seq.push_back(at(2, CallLocal{.target = Label{4}}));
seq.push_back(at(3, Exit{}));
seq.push_back(at(4, Exit{}));

const Program prog = Program::from_sequence(seq, info, {});
REQUIRE(analyze(prog, {}).max_call_depth == 2);
}

SECTION("excludes unreachable nested local calls") {
InstructionSeq seq;
seq.push_back(at(0, Exit{}));
// These calls are retained and inlined during CFG preparation, but are unreachable from entry.
seq.push_back(at(1, CallLocal{.target = Label{3}}));
seq.push_back(at(2, Exit{}));
seq.push_back(at(3, CallLocal{.target = Label{5}}));
seq.push_back(at(4, Exit{}));
seq.push_back(at(5, Exit{}));

const Program prog = Program::from_sequence(seq, info, {});
REQUIRE(analyze(prog, {}).max_call_depth == 0);
}

SECTION("preserves call depth after verification failure") {
InstructionSeq seq;
seq.push_back(at(0, CallLocal{.target = Label{2}}));
seq.push_back(at(1, Exit{}));
seq.push_back(at(2, CallLocal{.target = Label{4}}));
seq.push_back(at(3, Exit{}));
// This is one byte below the innermost 512-byte frame.
seq.push_back(at(4, Mem{
.access = {.width = 1, .basereg = Reg{R10_STACK_POINTER}, .offset = -513},
.value = Imm{0},
.is_load = false,
}));
seq.push_back(at(5, Exit{}));

const Program prog = Program::from_sequence(seq, info, {});
const AnalysisResult result = analyze(prog, {});
REQUIRE(result.failed);
REQUIRE(result.max_call_depth == 2);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

TEST_CASE("pass_extract_assertions populates assertions for every label in the CFG", "[passes]") {
const ProgramInfo info = default_info();
InstructionSeq seq;
Expand Down
Loading