• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Alan-Jowett / ebpf-verifier / 27778108035

07 Jun 2026 06:51PM UTC coverage: 86.386% (-2.5%) from 88.93%
27778108035

push

github

elazarg
Release v0.2.5

Bump project version to 0.2.5 and add a CHANGELOG entry covering ELF loader hardening, numeric-domain soundness fixes, and the writable helper output initialization documentation update since v0.2.4. Also updates the using_installed_package example version requirement.

Signed-off-by: Elazar Gershuni <elazarg@gmail.com>

9125 of 10563 relevant lines covered (86.39%)

6334294.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.71
/src/result.cpp
1
// Copyright (c) Prevail Verifier contributors.
2
// SPDX-License-Identifier: MIT
3
#include <algorithm>
4
#include <map>
5
#include <ranges>
6
#include <regex>
7
#include <sstream>
8
#include <type_traits>
9

10
#include "config.hpp"
11
#include "crab/ebpf_domain.hpp"
12
#include "ir/program.hpp"
13
#include "result.hpp"
14
#include "spec/ebpf_base.h"
15
#include "spec/vm_isa.hpp"
16

17
namespace prevail {
18

19
template <typename>
20
inline constexpr bool always_false_v = false;
21

22
// Stream-local storage index for the invariant filter (per-stream pword slot).
23
static int invariant_filter_index() {
30✔
24
    static const int index = std::ios_base::xalloc();
30✔
25
    return index;
30✔
26
}
27

28
const RelevantState* get_invariant_filter(std::ostream& os) {
18✔
29
    return static_cast<const RelevantState*>(os.pword(invariant_filter_index()));
18✔
30
}
31

32
invariant_filter::invariant_filter(std::ostream& os, const RelevantState* state)
6✔
33
    : os_(os), previous_(get_invariant_filter(os)) {
6✔
34
    os.pword(invariant_filter_index()) = const_cast<void*>(static_cast<const void*>(state));
6✔
35
}
6✔
36

37
invariant_filter::~invariant_filter() {
6✔
38
    os_.pword(invariant_filter_index()) = const_cast<void*>(static_cast<const void*>(previous_));
6✔
39
}
6✔
40

41
bool RelevantState::is_relevant_constraint(const std::string& constraint) const {
82✔
42
    // Extract the register or stack reference from the constraint.
43
    // Constraints look like: "r1.type=number", "r2.svalue=[0, 100]", "s[4088...4095].type=ctx"
44
    // Relational constraints: "r1.svalue-r8.svalue<=-100", "r0.svalue=r3.svalue+2"
45
    // Also handle: "packet_size=54", "meta_offset=[-4098, 0]"
46

47
    // Track whether we matched any known pattern.  When at least one pattern
48
    // fires but nothing is relevant we return false; the conservative
49
    // `return true` at the end only applies to truly unparseable constraints.
50
    bool parsed_any_pattern = false;
82✔
51

52
    // Check for ANY register mentioned in the constraint (not just at start).
53
    // This handles relational constraints like "r1.svalue-r8.svalue<=100".
54
    static const std::regex reg_pattern(R"(r(\d+)\.)");
82✔
55
    std::sregex_iterator it(constraint.begin(), constraint.end(), reg_pattern);
82✔
56
    std::sregex_iterator end;
82✔
57

58
    while (it != end) {
132✔
59
        parsed_any_pattern = true;
58✔
60
        const uint8_t reg_num = static_cast<uint8_t>(std::stoi((*it)[1].str()));
87✔
61
        if (registers.contains(Reg{reg_num})) {
58✔
62
            return true; // At least one relevant register is mentioned
4✔
63
        }
64
        ++it;
50✔
65
    }
66

67
    // Precompute absolute stack offsets once to avoid repeated conversion in loops
68
    std::vector<int64_t> abs_stack_offsets;
74✔
69
    abs_stack_offsets.reserve(stack_offsets.size());
74✔
70
    for (const auto& rel_offset : stack_offsets) {
74✔
71
        abs_stack_offsets.push_back(total_stack_size + rel_offset);
×
72
    }
73

74
    // Check for stack range pattern: s[start...end]
75
    static const std::regex stack_range_pattern(R"(s\[(\d+)\.\.\.(\d+)\])");
74✔
76
    std::sregex_iterator stack_it(constraint.begin(), constraint.end(), stack_range_pattern);
74✔
77
    while (stack_it != end) {
86✔
78
        parsed_any_pattern = true;
12✔
79
        const int64_t abs_start = std::stoll((*stack_it)[1].str());
18✔
80
        const int64_t abs_end = std::stoll((*stack_it)[2].str());
18✔
81
        for (const auto& abs_offset : abs_stack_offsets) {
12✔
82
            if (abs_offset >= abs_start && abs_offset <= abs_end) {
×
83
                return true; // Overlaps within the constraint range
37✔
84
            }
85
        }
86
        ++stack_it;
12✔
87
    }
88

89
    // Check for single-offset stack pattern: s[offset]
90
    static const std::regex stack_single_pattern(R"(s\[(\d+)\]\.)");
74✔
91
    std::sregex_iterator single_it(constraint.begin(), constraint.end(), stack_single_pattern);
74✔
92
    while (single_it != end) {
74✔
93
        parsed_any_pattern = true;
×
94
        const int64_t abs_pos = std::stoll((*single_it)[1].str());
×
95
        for (const auto& abs_offset : abs_stack_offsets) {
×
96
            if (abs_offset == abs_pos) {
×
97
                return true;
6✔
98
            }
99
        }
100
        ++single_it;
×
101
    }
102

103
    // Global constraints (packet_size, meta_offset) - always show for context
104
    if (constraint.starts_with("packet_size") || constraint.starts_with("meta_offset")) {
108✔
105
        return true;
12✔
106
    }
107

108
    // A known pattern was parsed but nothing matched — the constraint is irrelevant.
109
    if (parsed_any_pattern) {
62✔
110
        return false;
31✔
111
    }
112

113
    // If we can't parse it at all, show it (conservative)
114
    return true;
115
}
156✔
116

117
ObservationCheckResult AnalysisResult::check_observation_at_label(const Label& label, const InvariantPoint point,
14✔
118
                                                                  const EbpfDomain& observation,
119
                                                                  const ObservationCheckMode mode) const {
120
    const auto it = invariants.find(label);
14✔
121
    if (it == invariants.end()) {
14✔
122
        return {.ok = false, .message = "No invariant available for label " + to_string(label)};
×
123
    }
124
    const auto& inv_pair = it->second;
14✔
125
    const EbpfDomain& abstract_state = (point == InvariantPoint::pre) ? inv_pair.pre : inv_pair.post;
14✔
126

127
    if (observation.is_bottom()) {
14✔
128
        return {.ok = false, .message = "Observation constraints are unsatisfiable (domain is bottom)"};
2✔
129
    }
130

131
    if (abstract_state.is_bottom()) {
12✔
132
        return {.ok = false, .message = "Invariant at label is bottom (unreachable)"};
×
133
    }
134

135
    switch (mode) {
12✔
136
    case ObservationCheckMode::entailed:
4✔
137
        if (observation <= abstract_state) {
4✔
138
            return {.ok = true, .message = ""};
2✔
139
        }
140
        return {.ok = false, .message = "Invariant does not entail the observation (C ⊑ A is false)"};
2✔
141

142
    case ObservationCheckMode::consistent:
8✔
143
        // Default: consistency / satisfiability.
144
        if ((abstract_state & observation).is_bottom()) {
8✔
145
            return {.ok = false, .message = "Observation contradicts invariant (meet is bottom)"};
×
146
        }
147
        return {.ok = true, .message = ""};
8✔
148
    }
149

150
    return {.ok = false, .message = "Unsupported observation check mode"};
×
151
}
28✔
152

153
bool AnalysisResult::is_consistent_before(const Label& label, const EbpfDomain& observation) const {
×
154
    return check_observation_at_label(label, InvariantPoint::pre, observation, ObservationCheckMode::consistent).ok;
×
155
}
156

157
bool AnalysisResult::is_consistent_after(const Label& label, const EbpfDomain& observation) const {
×
158
    return check_observation_at_label(label, InvariantPoint::post, observation, ObservationCheckMode::consistent).ok;
×
159
}
160

161
StringInvariant AnalysisResult::invariant_at(const Label& label) const { return invariants.at(label).post.to_set(); }
1,480✔
162

163
std::optional<VerificationError> AnalysisResult::find_first_error() const {
1,480✔
164
    for (const auto& inv_pair : invariants | std::views::values) {
6,492✔
165
        if (inv_pair.pre.is_bottom()) {
5,350✔
166
            continue;
344✔
167
        }
168
        if (inv_pair.error) {
5,006✔
169
            return inv_pair.error;
338✔
170
        }
171
    }
172
    return {};
1,142✔
173
}
174

175
std::map<Label, std::vector<std::string>> AnalysisResult::find_unreachable(const Program& prog) const {
1,480✔
176
    std::map<Label, std::vector<std::string>> unreachable;
1,480✔
177
    for (const auto& [label, inv_pair] : invariants) {
7,436✔
178
        if (inv_pair.pre.is_bottom()) {
5,956✔
179
            continue;
850✔
180
        }
181
        if (const auto passume = std::get_if<Assume>(&prog.instruction_at(label))) {
8,084✔
182
            if (inv_pair.post.is_bottom() && !inv_pair.error) {
612✔
183
                const auto s = to_string(*passume);
184✔
184
                unreachable[label].emplace_back(to_string(label) + ": Code becomes unreachable (" + s + ")");
368✔
185
            }
184✔
186
        }
187
    }
188
    return unreachable;
1,480✔
189
}
×
190

191
/// Extract the registers and stack offsets read/written by an instruction.
192
/// Used to populate InstructionDeps during forward analysis.
193
InstructionDeps extract_instruction_deps(const Instruction& ins, const EbpfDomain& pre_state,
460✔
194
                                         const int total_stack_size) {
195
    InstructionDeps deps;
460✔
196

197
    std::visit(
460✔
198
        [&](const auto& v) {
896✔
199
            using T = std::decay_t<decltype(v)>;
200

201
            if constexpr (std::is_same_v<T, Bin>) {
202
                // dst = dst op src (or dst = src for MOV/MOVSX)
203
                deps.regs_written.insert(v.dst);
152✔
204
                if (v.op != Bin::Op::MOV && v.op != Bin::Op::MOVSX8 && v.op != Bin::Op::MOVSX16 &&
152✔
205
                    v.op != Bin::Op::MOVSX32) {
32✔
206
                    // Non-move ops also read dst
207
                    deps.regs_read.insert(v.dst);
64✔
208
                }
209
                if (const auto* reg = std::get_if<Reg>(&v.v)) {
152✔
210
                    deps.regs_read.insert(*reg);
64✔
211
                }
212
            } else if constexpr (std::is_same_v<T, Un>) {
213
                // dst = op(dst)
214
                deps.regs_read.insert(v.dst);
×
215
                deps.regs_written.insert(v.dst);
×
216
            } else if constexpr (std::is_same_v<T, LoadMapFd> || std::is_same_v<T, LoadMapAddress>) {
217
                deps.regs_written.insert(v.dst);
20✔
218
            } else if constexpr (std::is_same_v<T, Mem>) {
219
                deps.regs_read.insert(v.access.basereg);
124✔
220
                if (v.is_load) {
124✔
221
                    // Load: value = *(basereg + offset)
222
                    if (const auto* dst_reg = std::get_if<Reg>(&v.value)) {
22✔
223
                        deps.regs_written.insert(*dst_reg);
22✔
224
                    }
225
                    // Track stack read if base is a stack pointer (R10 or derived)
226
                    if (v.access.basereg.v == R10_STACK_POINTER) {
22✔
227
                        deps.stack_read.insert(v.access.offset);
2✔
228
                    } else if (const auto stack_off = pre_state.get_stack_offset(v.access.basereg)) {
20✔
229
                        // basereg is a stack pointer with known offset from R10
230
                        // Compute the actual stack slot: stack_off is absolute (e.g., 4096),
231
                        // convert to relative from R10:
232
                        //  relative = offset + (stack_off - total_stack_size)
233
                        deps.stack_read.insert(v.access.offset + (*stack_off - total_stack_size));
×
234
                    }
235
                } else {
236
                    // Store: *(basereg + offset) = value
237
                    if (const auto* src_reg = std::get_if<Reg>(&v.value)) {
102✔
238
                        deps.regs_read.insert(*src_reg);
102✔
239
                    }
240
                    // Track stack write if base is a stack pointer (R10 or derived)
241
                    if (v.access.basereg.v == R10_STACK_POINTER) {
102✔
242
                        deps.stack_written.insert(v.access.offset);
88✔
243
                    } else if (const auto stack_off = pre_state.get_stack_offset(v.access.basereg)) {
14✔
244
                        deps.stack_written.insert(v.access.offset + (*stack_off - total_stack_size));
×
245
                    }
246
                }
247
            } else if constexpr (std::is_same_v<T, Atomic>) {
248
                deps.regs_read.insert(v.access.basereg);
×
249
                deps.regs_read.insert(v.valreg);
×
250
                if (v.fetch) {
×
251
                    deps.regs_written.insert(v.valreg);
×
252
                }
253
                // Track stack read/write if base is a stack pointer (R10 or derived)
254
                if (v.access.basereg.v == R10_STACK_POINTER) {
×
255
                    deps.stack_read.insert(v.access.offset);
×
256
                    deps.stack_written.insert(v.access.offset);
×
257
                } else if (const auto stack_off = pre_state.get_stack_offset(v.access.basereg)) {
×
258
                    const auto adjusted_off = v.access.offset + (*stack_off - total_stack_size);
×
259
                    deps.stack_read.insert(adjusted_off);
×
260
                    deps.stack_written.insert(adjusted_off);
×
261
                }
262
            } else if constexpr (std::is_same_v<T, Call>) {
263
                // Calls read R1-R5 as arguments, write R0 as return value,
264
                // and clobber R1-R5 (caller-saved registers are killed).
265
                // Separate clobbers from writes so backward slicing can:
266
                // - Stop propagation for post-call uses of R1-R5 (they're killed)
267
                // - Still trace R1-R5 as read-deps when R0 is relevant-after (args affect return)
268
                for (uint8_t i = 1; i <= 5; ++i) {
132✔
269
                    deps.regs_read.insert(Reg{i});      // Arguments feed into return value
110✔
270
                    deps.regs_clobbered.insert(Reg{i}); // Killed after call
110✔
271
                }
272
                deps.regs_written.insert(Reg{0}); // Return value
22✔
273
            } else if constexpr (std::is_same_v<T, CallLocal>) {
274
                // Local (macro-inlined) calls:
275
                // - read R1-R5 as arguments
276
                // - write R0 as return value
277
                // - clobber R1-R5 (caller-saved)
278
                // - adjust frame pointer R10 (save/restore callee-saved regs)
279
                for (uint8_t i = 1; i <= 5; ++i) {
×
280
                    deps.regs_read.insert(Reg{i});      // Arguments
×
281
                    deps.regs_clobbered.insert(Reg{i}); // Killed after call
×
282
                }
283
                deps.regs_written.insert(Reg{0});  // Return value
×
284
                deps.regs_read.insert(Reg{10});    // Frame pointer read
×
285
                deps.regs_written.insert(Reg{10}); // Frame pointer adjusted
×
286
            } else if constexpr (std::is_same_v<T, Callx>) {
287
                // Indirect call: reads the function pointer and R1-R5,
288
                // writes R0, and clobbers R1-R5.
289
                deps.regs_read.insert(v.func);
×
290
                for (uint8_t i = 1; i <= 5; ++i) {
×
291
                    deps.regs_read.insert(Reg{i});      // Arguments
×
292
                    deps.regs_clobbered.insert(Reg{i}); // Killed after call
×
293
                }
294
                deps.regs_written.insert(Reg{0});
×
295
            } else if constexpr (std::is_same_v<T, Exit>) {
296
                deps.regs_read.insert(Reg{0}); // Return value
14✔
297
                if (!v.stack_frame_prefix.empty()) {
14✔
298
                    // Subprogram return restores callee-saved registers and frame pointer.
299
                    for (uint8_t i = R6; i <= R9; ++i) {
×
300
                        deps.regs_written.insert(Reg{i});
×
301
                    }
302
                    deps.regs_written.insert(Reg{R10_STACK_POINTER});
×
303
                }
304
            } else if constexpr (std::is_same_v<T, Jmp>) {
305
                if (v.cond) {
46✔
306
                    deps.regs_read.insert(v.cond->left);
32✔
307
                    if (const auto* reg = std::get_if<Reg>(&v.cond->right)) {
32✔
308
                        deps.regs_read.insert(*reg);
2✔
309
                    }
310
                }
311
            } else if constexpr (std::is_same_v<T, Assume>) {
312
                deps.regs_read.insert(v.cond.left);
54✔
313
                if (const auto* reg = std::get_if<Reg>(&v.cond.right)) {
54✔
314
                    deps.regs_read.insert(*reg);
4✔
315
                }
316
            } else if constexpr (std::is_same_v<T, Packet>) {
317
                if (v.regoffset) {
×
318
                    deps.regs_read.insert(*v.regoffset);
×
319
                }
320
                deps.regs_written.insert(Reg{0}); // Result in R0
×
321
                // Packet clobbers caller-saved registers R1-R5 without reading them
322
                for (uint8_t i = 1; i <= 5; ++i) {
×
323
                    deps.regs_clobbered.insert(Reg{i});
×
324
                }
325
            }
326
            // Undefined and IncrementLoopCounter have no register deps
327
        },
436✔
328
        ins);
329

330
    return deps;
460✔
331
}
×
332

333
/// Extract the registers referenced by an assertion.
334
/// Used to seed backward slicing from the point of failure.
335
std::set<Reg> extract_assertion_registers(const Assertion& assertion) {
20✔
336
    return std::visit(
10✔
337
        [](const auto& a) -> std::set<Reg> {
40✔
338
            using T = std::decay_t<decltype(a)>;
339

340
            if constexpr (std::is_same_v<T, Comparable>) {
341
                return {a.r1, a.r2};
4✔
342
            } else if constexpr (std::is_same_v<T, Addable>) {
343
                return {a.ptr, a.num};
×
344
            } else if constexpr (std::is_same_v<T, ValidDivisor>) {
345
                return {a.reg};
4✔
346
            } else if constexpr (std::is_same_v<T, ValidAccess>) {
347
                std::set<Reg> regs{a.reg};
9✔
348
                if (const auto* r = std::get_if<Reg>(&a.width)) {
6✔
349
                    regs.insert(*r);
×
350
                }
351
                return regs;
9✔
352
            } else if constexpr (std::is_same_v<T, ValidStore>) {
6✔
353
                return {a.mem, a.val};
×
354
            } else if constexpr (std::is_same_v<T, ValidSize>) {
355
                return {a.reg};
×
356
            } else if constexpr (std::is_same_v<T, ValidMapKeyValue>) {
357
                return {a.access_reg, a.map_fd_reg};
4✔
358
            } else if constexpr (std::is_same_v<T, ValidCallbackTarget>) {
359
                return {a.reg};
×
360
            } else if constexpr (std::is_same_v<T, TypeConstraint>) {
361
                return {a.reg};
12✔
362
            } else if constexpr (std::is_same_v<T, FuncConstraint>) {
363
                return {a.reg};
×
364
            } else if constexpr (std::is_same_v<T, ZeroCtxOffset>) {
365
                return {a.reg};
×
366
            } else if constexpr (std::is_same_v<T, BoundedLoopCount>) {
367
                // Loop counter, not a register
368
                return {};
2✔
369
            } else if constexpr (std::is_same_v<T, ValidArgZero>) {
370
                return {a.reg};
×
371
            } else if constexpr (std::is_same_v<T, ValidMapType>) {
372
                return {a.map_fd_reg};
×
373
            } else {
374
                static_assert(always_false_v<T>, "Unhandled Assertion type in extract_assertion_registers");
375
            }
376
        },
377
        assertion);
25✔
378
}
379

380
FailureSlice AnalysisResult::compute_slice_from_label(const Program& prog, const Label& label,
10✔
381
                                                      const RelevantState& seed_relevance, size_t max_steps) const {
382
    FailureSlice slice{
5✔
383
        .failing_label = label,
384
        .error = VerificationError(""),
10✔
385
        .relevance = {},
386
    };
20✔
387

388
    // Copy error if present at this label.
389
    const auto label_it = invariants.find(label);
10✔
390
    if (label_it != invariants.end() && label_it->second.error) {
10✔
391
        slice.error = *label_it->second.error;
10✔
392
    }
393

394
    // `visited` tracks all explored labels for deduplication during backward traversal.
395
    // `slice_labels` tracks only labels that interact with relevant registers (the output slice).
396
    std::map<Label, RelevantState> visited;
10✔
397
    std::set<Label> conservative_visited; // Dedup for empty-relevance labels in conservative mode
10✔
398
    std::map<Label, RelevantState> slice_labels;
10✔
399

400
    // Template for inserting new empty entries into `visited` / `slice_labels`.
401
    // Copy-constructed from the seed so it carries the seed's filter data, then
402
    // cleared. Used with try_emplace: the template is copied on insert, ignored
403
    // when the key is already present.
404
    RelevantState empty_template = seed_relevance;
10✔
405
    empty_template.registers.clear();
10✔
406
    empty_template.stack_offsets.clear();
10✔
407

408
    // Worklist: (label, relevant_state_after_this_label)
409
    std::vector<std::pair<Label, RelevantState>> worklist;
10✔
410
    worklist.emplace_back(label, seed_relevance);
10✔
411

412
    // When the seed has no register/stack deps (e.g., BoundedLoopCount),
413
    // perform a conservative backward walk so the slice still shows the
414
    // loop structure and control flow leading to the failure.
415
    const bool conservative_mode = seed_relevance.registers.empty() && seed_relevance.stack_offsets.empty();
10✔
416

417
    size_t steps = 0;
10✔
418

419
    // Hoist the parent lookup for the target label outside the hot loop;
420
    // it is invariant and parents_of() may return a temporary.
421
    const auto parents_of_target = prog.cfg().parents_of(label);
10✔
422

423
    while (!worklist.empty() && steps < max_steps) {
108✔
424
        auto [current_label, relevant_after] = worklist.back();
98✔
425
        worklist.pop_back();
98✔
426

427
        // Skip if nothing is relevant — unless we're in conservative mode
428
        // (empty-seed assertions like BoundedLoopCount) or this is the target label.
429
        if (!conservative_mode && current_label != label && relevant_after.registers.empty() &&
100✔
430
            relevant_after.stack_offsets.empty()) {
4✔
431
            continue;
4✔
432
        }
433

434
        // Merge with existing relevance at this label (for deduplication).
435
        // try_emplace copy-constructs from empty_template on first visit;
436
        // subsequent visits merge into the existing entry.
437
        auto [visited_it, _] = visited.try_emplace(current_label, empty_template);
94✔
438
        auto& existing = visited_it->second;
94✔
439
        const size_t prev_size = existing.size();
94✔
440
        existing.merge(relevant_after);
94✔
441
        const size_t new_size = existing.size();
94✔
442

443
        // If no new relevance was added, skip (already processed with same or broader relevance).
444
        // In conservative mode with empty relevance, use a separate visited set for dedup.
445
        if (new_size == prev_size) {
94✔
446
            if (prev_size > 0) {
2✔
447
                continue;
2✔
448
            }
449
            // Empty relevance (conservative mode): skip if we already visited this label
450
            if (!conservative_visited.insert(current_label).second) {
×
451
                continue;
×
452
            }
453
        }
454

455
        // Compute what's relevant BEFORE this instruction using deps.
456
        // Starts as a copy of relevant_after; the two branches below either
457
        // modify it or leave it unchanged.
458
        RelevantState relevant_before = relevant_after;
92✔
459
        const auto inv_it = invariants.find(current_label);
92✔
460
        if (inv_it != invariants.end() && inv_it->second.deps) {
92✔
461
            const auto& deps = *inv_it->second.deps;
92✔
462

463
            // Remove registers that are written by this instruction
464
            // (they weren't relevant before their definition)
465
            for (const auto& reg : deps.regs_written) {
158✔
466
                relevant_before.registers.erase(reg);
66✔
467
            }
468

469
            // Remove registers that are clobbered (killed without reading).
470
            // These stop propagation for post-instruction uses but don't add read-deps.
471
            for (const auto& reg : deps.regs_clobbered) {
112✔
472
                relevant_before.registers.erase(reg);
20✔
473
            }
474

475
            // Determine if this instruction contributes to the slice.
476
            // An instruction contributes when it writes to a relevant register/stack slot,
477
            // or when it is a control-flow decision (Jmp/Assume) that reads relevant registers.
478
            bool instruction_contributes = false;
92✔
479
            for (const auto& reg : deps.regs_written) {
112✔
480
                if (relevant_after.registers.contains(reg)) {
66✔
481
                    instruction_contributes = true;
23✔
482
                    break;
23✔
483
                }
484
            }
485
            for (const auto& offset : deps.stack_written) {
98✔
486
                if (relevant_after.stack_offsets.contains(offset)) {
6✔
487
                    instruction_contributes = true;
488
                    break;
489
                }
490
            }
491

492
            // Control-flow instructions (Jmp/Assume) that read relevant registers
493
            // contribute to the slice because they shape the path to the target.
494
            if (!instruction_contributes) {
92✔
495
                const auto& ins = prog.instruction_at(current_label);
46✔
496
                if (std::holds_alternative<Jmp>(ins) || std::holds_alternative<Assume>(ins)) {
46✔
497
                    for (const auto& reg : deps.regs_read) {
30✔
498
                        if (relevant_after.registers.contains(reg)) {
20✔
499
                            instruction_contributes = true;
1✔
500
                            break;
1✔
501
                        }
502
                    }
503
                }
504
            }
505

506
            // Immediate path guard: when the current label is a direct predecessor
507
            // of the target label and is an Assume instruction, its condition
508
            // registers are causally relevant — they determine reachability.
509
            if (std::holds_alternative<Assume>(prog.instruction_at(current_label))) {
92✔
510
                if (std::find(parents_of_target.begin(), parents_of_target.end(), current_label) !=
6✔
511
                    parents_of_target.end()) {
9✔
512
                    instruction_contributes = true;
47✔
513
                }
514
            }
515

516
            // At the target label, the assertion depends on registers the instruction
517
            // reads (e.g., base pointer r3 in a store). Since stores write to memory
518
            // not registers, instruction_contributes would be false without this.
519
            if (current_label == label) {
92✔
520
                instruction_contributes = true;
10✔
521
            }
522

523
            // In conservative mode (empty seed, e.g., BoundedLoopCount), include all
524
            // reachable labels so the slice shows the loop structure and control flow.
525
            if (conservative_mode) {
92✔
526
                instruction_contributes = true;
527
            }
528

529
            if (instruction_contributes) {
92✔
530
                for (const auto& reg : deps.regs_read) {
114✔
531
                    relevant_before.registers.insert(reg);
58✔
532
                }
533
                for (const auto& offset : deps.stack_read) {
56✔
534
                    relevant_before.stack_offsets.insert(offset);
×
535
                }
536
            }
537

538
            // Remove stack locations that are written by this instruction,
539
            // but preserve offsets that are also read (read-modify-write, e.g., Atomic).
540
            for (const auto& offset : deps.stack_written) {
98✔
541
                if (!deps.stack_read.contains(offset)) {
6✔
542
                    relevant_before.stack_offsets.erase(offset);
6✔
543
                }
544
            }
545

546
            if (instruction_contributes) {
92✔
547
                // Only include contributing labels in the output slice.
548
                // Merge (not assign) because a label may be revisited from
549
                // a different successor with additional relevance.
550
                auto [it, _] = slice_labels.try_emplace(current_label, empty_template);
56✔
551
                it->second.merge(relevant_before);
56✔
552
            }
553
        } else {
554
            // No deps available: conservatively treat this label as contributing
555
            // and propagate all current relevance to predecessors.
556
            auto [it, _] = slice_labels.try_emplace(current_label, empty_template);
×
557
            it->second.merge(relevant_before);
×
558
        }
559

560
        // Add predecessors to worklist
561
        for (const auto& parent : prog.cfg().parents_of(current_label)) {
180✔
562
            worklist.emplace_back(parent, relevant_before);
88✔
563
        }
564

565
        ++steps;
92✔
566
    }
98✔
567

568
    // Expand join points: for any traversed label that is a join point
569
    // (≥2 predecessors) where at least one predecessor is already in the slice,
570
    // add the join-point label itself and all predecessors that have invariants.
571
    // This ensures the causal trace shows converging paths that cause precision loss.
572
    // Note: predecessors may not be in `visited` if the worklist budget was exhausted
573
    // before reaching them, so we check the invariant map directly.
574
    std::map<Label, RelevantState> join_expansion;
10✔
575
    for (const auto& [v_label, v_relevance] : visited) {
86✔
576
        const auto& parents = prog.cfg().parents_of(v_label);
76✔
577
        if (parents.size() < 2) {
76✔
578
            continue;
74✔
579
        }
580
        // Check that at least one predecessor is in the slice (this join is relevant)
581
        bool has_slice_parent = false;
2✔
582
        for (const auto& parent : parents) {
4✔
583
            if (slice_labels.contains(parent)) {
4✔
584
                has_slice_parent = true;
1✔
585
                break;
1✔
586
            }
587
        }
588
        if (!has_slice_parent) {
2✔
589
            continue;
×
590
        }
591
        // Include the join-point label itself so the printing code can display
592
        // per-predecessor state at this join.
593
        if (!slice_labels.contains(v_label)) {
2✔
594
            join_expansion.emplace(v_label, v_relevance);
×
595
        }
596
        // Include all predecessors so the join display is complete.
597
        // Use visited relevance if available, otherwise use the join-point's relevance.
598
        for (const auto& parent : parents) {
6✔
599
            if (slice_labels.contains(parent) || join_expansion.contains(parent)) {
4✔
600
                continue;
2✔
601
            }
602
            const auto& rel = visited.contains(parent) ? visited.at(parent) : v_relevance;
2✔
603
            join_expansion.emplace(parent, rel);
2✔
604
        }
605
    }
606
    slice_labels.insert(join_expansion.begin(), join_expansion.end());
10✔
607

608
    // Build the slice from contributing labels only
609
    slice.relevance = std::move(slice_labels);
10✔
610
    return slice;
15✔
611
}
10✔
612

613
std::vector<FailureSlice> AnalysisResult::compute_failure_slices(const SliceParams params,
14✔
614
                                                                 const AnalysisContext& context) const {
615
    const Program& prog = context.program;
14✔
616
    const auto max_slices = params.max_slices;
14✔
617
    std::vector<FailureSlice> slices;
14✔
618

619
    for (const auto& [label, inv_pair] : invariants) {
296✔
620
        if (inv_pair.pre.is_bottom()) {
282✔
621
            continue;
272✔
622
        }
623
        if (!inv_pair.error) {
232✔
624
            continue;
222✔
625
        }
626
        if (max_slices > 0 && slices.size() >= max_slices) {
10✔
627
            break;
628
        }
629

630
        // Seed relevant registers from the actual failing assertion.
631
        // Forward analysis stops at the first failing assertion, which may not be
632
        // assertions[0]. Replay the checks against the pre-state to identify
633
        // the actual failing assertion and seed relevance from it.
634
        RelevantState initial_relevance(context);
10✔
635
        const auto& assertions = prog.assertions_at(label);
10✔
636
        bool found_failing = false;
10✔
637
        for (const auto& assertion : assertions) {
22✔
638
            if (ebpf_domain_check(inv_pair.pre, assertion, label, context)) {
22✔
639
                for (const auto& reg : extract_assertion_registers(assertion)) {
22✔
640
                    initial_relevance.registers.insert(reg);
12✔
641
                }
5✔
642
                found_failing = true;
10✔
643
                break;
10✔
644
            }
645
        }
646
        // Fallback: if no failing assertion was identified (shouldn't happen),
647
        // aggregate all assertions. When the failing assertion was found but has
648
        // no register deps, leave the seed empty so compute_slice_from_label
649
        // enters conservative mode.
650
        if (!found_failing) {
10✔
651
            for (const auto& assertion : assertions) {
×
652
                for (const auto& reg : extract_assertion_registers(assertion)) {
×
653
                    initial_relevance.registers.insert(reg);
×
654
                }
×
655
            }
656
        }
657

658
        slices.push_back(compute_slice_from_label(prog, label, initial_relevance, params.max_steps));
15✔
659
    }
10✔
660

661
    return slices;
14✔
662
}
×
663

664
} // namespace prevail
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc