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

Alan-Jowett / ebpf-verifier / 30559636792

27 Jul 2026 07:29AM UTC coverage: 86.764% (+0.1%) from 86.649%
30559636792

push

github

elazarg
Fix 32-bit soundness holes that let unsafe programs pass verification

Two independent classes of ALU32/JMP32 unsoundness allowed a crafted program to
be accepted while performing an out-of-bounds stack access.

MOVSX: the no-op fast path returned early for `wX sN= wX`, skipping the
zero-extension applied at the end of the function. The verifier kept a
sign-extended 64-bit value where the CPU produces a zero-extended 32-bit one, so
`r1 = r10; r2 = -1; w2 s8= r2; r1 += r2` was analyzed as r10 - 1 while the CPU
computes r10 + 0xFFFFFFFF, and the resulting out-of-bounds store was proved
in-bounds. The verifier already contradicted itself here: with r1 = -1,
`w2 s8= r1` yielded 0xFFFFFFFF but `w2 s8= r2` yielded -1 for the same operation
and input. Guard the fast path with bin.is64; the 64-bit form remains a no-op.

32-bit compares: the helpers emitted constraints over the 64-bit svalue/uvalue
variables after testing only the truncated 32-bit views. When a register's
64-bit value falls outside the 32-bit range those views say nothing about how the
64-bit variables are ordered, so branches that concrete executions do take were
proved dead, and unsafe code on them was never checked. Test the 64-bit intervals
instead, falling back to the existing no-constraint case. Precision is unchanged
for registers holding genuine 32-bit values, since the comparison itself pins the
operand's sign in the branches that assert one.

assume_unsigned_32bit_lt already guarded every branch this way, which is why only
the >, >=, s<, s<=, s> and s>= paths were affected.

Also drop the interval parameters these helpers no longer read.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Elazar Gershuni <elazarg@gmail.com>

19 of 19 new or added lines in 2 files covered. (100.0%)

107 existing lines in 5 files now uncovered.

9328 of 10751 relevant lines covered (86.76%)

6280124.66 hits per line

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

88.61
/src/ir/cfg_builder.cpp
1
// Copyright (c) Prevail Verifier contributors.
2
// SPDX-License-Identifier: MIT
3
#include <algorithm>
4
#include <cassert>
5
#include <limits>
6
#include <map>
7
#include <optional>
8
#include <set>
9
#include <string>
10
#include <vector>
11

12
#include "cfg/cfg.hpp"
13
#include "cfg/wto.hpp"
14
#include "config.hpp"
15
#include "ir/call_resolver.hpp"
16
#include "ir/program.hpp"
17
#include "ir/syntax.hpp"
18
#include "platform.hpp"
19

20
using std::optional;
21
using std::set;
22
using std::string;
23
using std::to_string;
24
using std::vector;
25

26
namespace prevail {
27
struct CallbackMetadata {
2,098✔
28
    std::set<int32_t> target_labels;
29
    std::set<int32_t> targets_with_exit;
30
};
31

32
struct CfgBuilder final {
8✔
33
    Program prog;
34

35
    // TODO: ins should be inserted elsewhere
36
    void insert_after(const Label& prev_label, const Label& new_label, const Instruction& ins) {
48✔
37
        if (prev_label == new_label) {
48✔
38
            CRAB_ERROR("Cannot insert after the same label ", to_string(new_label));
×
39
        }
40
        std::set<Label> prev_children;
48✔
41
        std::swap(prev_children, prog.m_cfg.get_node(prev_label).children);
48✔
42

43
        for (const Label& next_label : prev_children) {
104✔
44
            prog.m_cfg.get_node(next_label).parents.erase(prev_label);
56✔
45
        }
46

47
        insert(new_label, ins);
48✔
48
        for (const Label& next_label : prev_children) {
104✔
49
            add_child(prev_label, new_label);
56✔
50
            add_child(new_label, next_label);
56✔
51
        }
52
    }
48✔
53

54
    // TODO: ins should be inserted elsewhere
55
    void insert(const Label& _label, const Instruction& ins) {
1,318,228✔
56
        if (const auto it = prog.m_cfg.neighbours.find(_label); it != prog.m_cfg.neighbours.end()) {
1,318,228✔
57
            CRAB_ERROR("Label ", to_string(_label), " already exists");
×
58
        }
59
        prog.m_cfg.neighbours.emplace(_label, Cfg::Adjacent{});
1,318,228✔
60
        prog.m_instructions.emplace(_label, ins);
1,318,228✔
61
    }
1,318,228✔
62

63
    // TODO: ins should be inserted elsewhere
64
    Label insert_jump(const Label& from, const Label& to, const Instruction& ins) {
198,772✔
65
        const Label jump_label = Label::make_jump(from, to);
198,772✔
66
        if (prog.m_cfg.contains(jump_label)) {
198,772✔
67
            CRAB_ERROR("Jump label ", to_string(jump_label), " already exists");
×
68
        }
69
        insert(jump_label, ins);
198,772✔
70
        add_child(from, jump_label);
198,772✔
71
        add_child(jump_label, to);
198,772✔
72
        return jump_label;
198,772✔
73
    }
×
74

75
    void add_child(const Label& a, const Label& b) {
1,426,226✔
76
        assert(b != Label::entry);
1,426,226✔
77
        assert(a != Label::exit);
1,426,226✔
78
        prog.m_cfg.neighbours.at(a).children.insert(b);
1,426,226✔
79
        prog.m_cfg.neighbours.at(b).parents.insert(a);
1,426,226✔
80
    }
1,426,226✔
81

82
    void remove_child(const Label& a, const Label& b) {
714✔
83
        prog.m_cfg.get_node(a).children.erase(b);
714✔
84
        prog.m_cfg.get_node(b).parents.erase(a);
714✔
85
    }
714✔
86

87
    void set_assertions(const Label& label, const std::vector<Assertion>& assertions) {
1,326,366✔
88
        if (!prog.m_cfg.contains(label)) {
1,326,366✔
89
            CRAB_ERROR("Label ", to_string(label), " not found in the CFG: ");
×
90
        }
91
        prog.m_assertions.insert_or_assign(label, assertions);
1,326,366✔
92
    }
1,326,366✔
93

94
    void set_callback_metadata(CallbackMetadata md) {
4,196✔
95
        prog.m_callback_target_labels = std::move(md.target_labels);
4,196✔
96
        prog.m_callback_targets_with_exit = std::move(md.targets_with_exit);
4,196✔
97
    }
4,196✔
98
};
99

100
/// Get the inverse of a given comparison operation.
101
static Condition::Op reverse(const Condition::Op op) {
99,386✔
102
    switch (op) {
99,386✔
103
    case Condition::Op::EQ: return Condition::Op::NE;
22,662✔
104
    case Condition::Op::NE: return Condition::Op::EQ;
13,859✔
105

106
    case Condition::Op::GE: return Condition::Op::LT;
366✔
107
    case Condition::Op::LT: return Condition::Op::GE;
1,317✔
108

109
    case Condition::Op::SGE: return Condition::Op::SLT;
56✔
110
    case Condition::Op::SLT: return Condition::Op::SGE;
3,210✔
111

112
    case Condition::Op::LE: return Condition::Op::GT;
141✔
113
    case Condition::Op::GT: return Condition::Op::LE;
4,696✔
114

115
    case Condition::Op::SLE: return Condition::Op::SGT;
22✔
116
    case Condition::Op::SGT: return Condition::Op::SLE;
3,345✔
117

118
    case Condition::Op::SET: return Condition::Op::NSET;
17✔
119
    case Condition::Op::NSET: return Condition::Op::SET;
2✔
120
    }
121
    std::unreachable();
122
}
123

124
/// Get the inverse of a given comparison condition.
125
static Condition reverse(const Condition& cond) {
99,386✔
126
    return {.op = reverse(cond.op), .left = cond.left, .right = cond.right, .is64 = cond.is64};
99,386✔
127
}
128

129
static bool has_fall(const Instruction& ins) {
971,888✔
130
    if (std::holds_alternative<Exit>(ins)) {
971,888✔
131
        return false;
3,042✔
132
    }
133

134
    if (const auto pins = std::get_if<Jmp>(&ins)) {
965,804✔
135
        if (!pins->cond) {
502✔
136
            return false;
251✔
137
        }
138
    }
139

140
    return true;
482,651✔
141
}
142

143
enum class RejectKind {
144
    NotImplemented,
145
    Capability,
146
};
147

148
struct RejectionReason {
46✔
149
    RejectKind kind{};
150
    std::string detail;
151
};
152

153
static bool supports(const ebpf_platform_t& platform, const bpf_conformance_groups_t group) {
1,656,602✔
154
    return platform.supports_group(group);
1,652,026✔
155
}
156

157
static bool un_requires_base64(const Un& un) {
11,412✔
158
    switch (un.op) {
11,412✔
159
    case Un::Op::BE64:
49✔
160
    case Un::Op::LE64:
161
    case Un::Op::SWAP64: return true;
49✔
162
    default: return false;
7,558✔
163
    }
164
}
165

166
static std::optional<ResolvedCall> resolve_kfunc_call(const CallBtf& call_btf, const ProgramInfo& info,
64✔
167
                                                      std::string* why_not) {
168
    if (!info.platform || !info.platform->resolve_kfunc_call) {
64✔
169
        if (why_not) {
×
170
            *why_not = "kfunc resolution is unavailable on this platform";
×
171
        }
172
        return std::nullopt;
×
173
    }
174
    return info.platform->resolve_kfunc_call(call_btf.btf_id, call_btf.module, info.type, why_not);
64✔
175
}
176

177
// CallBtf in the instruction stream is replaced by a key-only Call{func,
178
// kind=kfunc}; the ResolvedCall is reproduced on demand via resolve(call, info).
179
using ResolvedKfuncCalls = std::map<Label, Call>;
180

181
[[nodiscard]]
182
static std::optional<RejectionReason> check_instruction_feature_support(const Instruction& ins,
1,645,798✔
183
                                                                        const ProgramInfo& info) {
184
    const ebpf_platform_t& platform = *info.platform;
1,645,798✔
185
    auto reject_not_implemented = [](std::string detail) {
550,546✔
186
        return RejectionReason{.kind = RejectKind::NotImplemented, .detail = std::move(detail)};
×
187
    };
188
    auto reject_capability = [](std::string detail) {
550,592✔
189
        return RejectionReason{.kind = RejectKind::Capability, .detail = std::move(detail)};
46✔
190
    };
191

192
    if (const auto p = std::get_if<Call>(&ins)) {
1,645,798✔
193
        const auto resolved = resolve(*p, info);
89,846✔
194
        if (!resolved.is_supported) {
89,846✔
195
            return reject_capability(resolved.unsupported_reason);
9✔
196
        }
197
    }
89,846✔
198
    if (std::holds_alternative<Callx>(ins) && !supports(platform, bpf_conformance_groups_t::callx)) {
1,645,792✔
199
        return reject_capability("requires conformance group callx");
×
200
    }
201
    if ((std::holds_alternative<Call>(ins) || std::holds_alternative<CallLocal>(ins) ||
3,715,759✔
202
         std::holds_alternative<Callx>(ins) || std::holds_alternative<CallBtf>(ins) ||
3,623,654✔
203
         std::holds_alternative<Exit>(ins)) &&
4,295,523✔
204
        !supports(platform, bpf_conformance_groups_t::base32)) {
97,302✔
205
        return reject_capability("requires conformance group base32");
×
206
    }
207
    if (const auto p = std::get_if<Bin>(&ins)) {
1,935,085✔
208
        if (!supports(platform, p->is64 ? bpf_conformance_groups_t::base64 : bpf_conformance_groups_t::base32)) {
955,138✔
209
            return reject_capability(p->is64 ? "requires conformance group base64"
×
210
                                             : "requires conformance group base32");
×
211
        }
212
        if ((p->op == Bin::Op::MUL || p->op == Bin::Op::UDIV || p->op == Bin::Op::UMOD || p->op == Bin::Op::SDIV ||
860,740✔
213
             p->op == Bin::Op::SMOD) &&
1,441,435✔
214
            !supports(platform, p->is64 ? bpf_conformance_groups_t::divmul64 : bpf_conformance_groups_t::divmul32)) {
13,618✔
215
            return reject_capability(p->is64 ? "requires conformance group divmul64"
×
216
                                             : "requires conformance group divmul32");
×
217
        }
218
    }
219
    if (const auto p = std::get_if<Un>(&ins)) {
1,645,791✔
220
        const bool need_base64 = p->is64 || un_requires_base64(*p);
15,624✔
221
        if (!supports(platform, need_base64 ? bpf_conformance_groups_t::base64 : bpf_conformance_groups_t::base32)) {
11,717✔
222
            return reject_capability(need_base64 ? "requires conformance group base64"
6✔
223
                                                 : "requires conformance group base32");
2✔
224
        }
225
    }
226
    if (const auto p = std::get_if<Jmp>(&ins)) {
1,645,790✔
227
        if (!supports(platform,
283,745✔
228
                      p->cond ? (p->cond->is64 ? bpf_conformance_groups_t::base64 : bpf_conformance_groups_t::base32)
283,745✔
229
                              : bpf_conformance_groups_t::base32)) {
230
            return reject_capability((p->cond && p->cond->is64) ? "requires conformance group base64"
×
231
                                                                : "requires conformance group base32");
×
232
        }
233
    }
234
    if (const auto p = std::get_if<LoadPseudo>(&ins)) {
1,645,804✔
235
        if (!supports(platform, bpf_conformance_groups_t::base64)) {
40✔
236
            return reject_capability("requires conformance group base64");
×
237
        }
238
        switch (p->addr.kind) {
40✔
239
        case PseudoAddress::Kind::VARIABLE_ADDR:
26✔
240
        case PseudoAddress::Kind::CODE_ADDR:
241
        case PseudoAddress::Kind::MAP_BY_IDX:
242
        case PseudoAddress::Kind::MAP_VALUE_BY_IDX: break; // Resolved during CFG construction.
26✔
243
        default: return reject_not_implemented("lddw unknown pseudo");
×
244
        }
245
    }
246
    if ((std::holds_alternative<LoadMapFd>(ins) || std::holds_alternative<LoadMapAddress>(ins)) &&
1,686,607✔
247
        !supports(platform, bpf_conformance_groups_t::base64)) {
61,582✔
248
        return reject_capability("requires conformance group base64");
×
249
    }
250
    if (const auto p = std::get_if<Mem>(&ins)) {
1,645,790✔
251
        if (!supports(platform,
423,069✔
252
                      (p->access.width == 8) ? bpf_conformance_groups_t::base64 : bpf_conformance_groups_t::base32)) {
423,069✔
253
            return reject_capability((p->access.width == 8) ? "requires conformance group base64"
×
254
                                                            : "requires conformance group base32");
×
255
        }
256
        if (p->is_signed && !supports(platform, bpf_conformance_groups_t::base64)) {
423,069✔
257
            return reject_capability("requires conformance group base64");
×
258
        }
259
    }
260
    if (std::holds_alternative<Packet>(ins) && !supports(platform, bpf_conformance_groups_t::packet)) {
1,645,790✔
261
        return reject_capability("requires conformance group packet");
76✔
262
    }
263
    if (const auto p = std::get_if<Atomic>(&ins)) {
1,645,752✔
264
        const auto group =
2,620✔
265
            (p->access.width == 8) ? bpf_conformance_groups_t::atomic64 : bpf_conformance_groups_t::atomic32;
1,965✔
266
        if (!supports(platform, group)) {
1,965✔
267
            return reject_capability((group == bpf_conformance_groups_t::atomic64)
×
268
                                         ? "requires conformance group atomic64"
269
                                         : "requires conformance group atomic32");
×
270
        }
271
    }
272
    return {};
1,645,752✔
273
}
274

275
// Pass: ValidateInstructionSupport
276
// Throwing wrapper around the value-returning check_instruction_feature_support.
277
// Single named place that converts a rejection into the throw shape; the
278
// value-returning checker is preserved for sites that want to query without
279
// throwing (see the assert at pass_populate_nodes).
280
[[noreturn]]
281
static void throw_unsupported(const RejectionReason& reason, const Label& label) {
46✔
282
    const std::string prefix = (reason.kind == RejectKind::NotImplemented) ? "not implemented: " : "rejected: ";
69✔
283
    throw InvalidControlFlow{prefix + reason.detail + " (at " + to_string(label) + ")"};
92✔
284
}
46✔
285

286
static void enforce_instruction_feature_support(const Instruction& ins, const Label& label, const ProgramInfo& info) {
1,101,092✔
287
    if (const auto reason = check_instruction_feature_support(ins, info)) {
1,101,092✔
288
        throw_unsupported(*reason, label);
46✔
289
    }
1,101,092✔
290
}
1,101,046✔
291

292
// Reads    : instruction sequence, platform conformance groups.
293
// Writes   : nothing.
294
// Throws   : InvalidControlFlow on any instruction the platform cannot run.
295
// Invariant: must run before CFG construction; pass_populate_nodes assumes
296
//            every instruction has been vetted here.
297
static void pass_validate_instruction_support(const InstructionSeq& insts, const ProgramInfo& info) {
4,288✔
298
    for (const auto& [label, inst, _] : insts) {
1,105,334✔
299
        enforce_instruction_feature_support(inst, label, info);
1,101,092✔
300
    }
301
}
4,242✔
302

303
// Pass: ResolveKfuncCalls
304
// Reads    : instruction sequence, platform kfunc resolver.
305
// Writes   : returns a Label -> Call map for every CallBtf in the sequence.
306
// Throws   : InvalidControlFlow if any CallBtf cannot be resolved for this platform.
307
// Invariant: pass_populate_nodes consults this map to replace CallBtf with the resolved Call.
308
static ResolvedKfuncCalls pass_resolve_kfunc_calls(const InstructionSeq& insts, const ProgramInfo& info) {
4,242✔
309
    ResolvedKfuncCalls resolved;
4,242✔
310
    for (const auto& [label, inst, _] : insts) {
1,093,882✔
311
        const auto* call_btf = std::get_if<CallBtf>(&inst);
1,089,666✔
312
        if (!call_btf) {
1,089,666✔
313
            continue;
1,089,602✔
314
        }
315
        std::string why_not;
64✔
316
        const auto r = resolve_kfunc_call(*call_btf, info, &why_not);
64✔
317
        if (!r) {
64✔
318
            throw InvalidControlFlow{"not implemented: " + why_not + " (at " + to_string(label) + ")"};
52✔
319
        }
320
        // Build the lowered Call key directly from the source CallBtf rather
321
        // than trusting any field the platform returned. The lowered IR's
322
        // identity is the pre-resolution (btf_id, module) pair plus the fixed
323
        // CallKind::kfunc tag; nothing else can soundly change it. Constructing
324
        // the key here makes that invariant audit-visible and removes a class
325
        // of bugs where a misbehaving resolver mis-keys the lowered IR — in
326
        // particular, two kfuncs sharing a BTF id across modules must remain
327
        // distinguishable.
328
        const Call lowered{
38✔
329
            .func = call_btf->btf_id,
38✔
330
            .kind = CallKind::kfunc,
331
            .module = call_btf->module,
38✔
332
        };
38✔
333
        resolved.insert_or_assign(label, lowered);
38✔
334
    }
90✔
335
    return resolved;
4,216✔
336
}
26✔
337

338
/// Update a control-flow graph to inline function macros.
339
static void add_cfg_nodes(CfgBuilder& builder, const Label& caller_label, const Label& entry_label,
722✔
340
                          const int max_call_stack_frames) {
341
    const string caller_label_str = to_string(caller_label);
722✔
342
    const long stack_frame_depth = std::ranges::count(caller_label_str, STACK_FRAME_DELIMITER) + 2;
722✔
343
    if (stack_frame_depth > max_call_stack_frames) {
722✔
344
        throw InvalidControlFlow{"too many call stack frames"};
10✔
345
    }
346

347
    bool first = true;
718✔
348

349
    // Get the label of the node to go to on returning from the macro.
350
    Label exit_to_label = builder.prog.cfg().get_child(caller_label);
718✔
351

352
    // Construct the variable prefix to use for the new stack frame
353
    // and store a copy in the CallLocal instruction since the instruction-specific
354
    // labels may only exist until the CFG is simplified.
355
    const std::string stack_frame_prefix = to_string(caller_label);
718✔
356
    if (const auto pcall = std::get_if<CallLocal>(&builder.prog.instruction_at(caller_label))) {
1,077✔
357
        pcall->stack_frame_prefix = stack_frame_prefix;
718✔
358
    }
359

360
    // Walk the transitive closure of CFG nodes starting at entry_label and ending at
361
    // any exit instruction.
362
    std::set macro_labels{entry_label};
1,812✔
363
    std::set seen_labels{entry_label};
1,812✔
364
    while (!macro_labels.empty()) {
30,664✔
365
        Label macro_label = *macro_labels.begin();
29,950✔
366
        macro_labels.erase(macro_label);
29,950✔
367

368
        if (stack_frame_prefix == macro_label.stack_frame_prefix) {
29,950✔
369
            throw InvalidControlFlow{stack_frame_prefix + ": illegal recursion"};
6✔
370
        }
371

372
        // Clone the macro block into a new block with the new stack frame prefix.
373
        const Label label{macro_label.from, macro_label.to, stack_frame_prefix};
44,921✔
374
        auto inst = builder.prog.instruction_at(macro_label);
29,946✔
375
        if (const auto pexit = std::get_if<Exit>(&inst)) {
30,306✔
376
            pexit->stack_frame_prefix = label.stack_frame_prefix;
720✔
377
        }
378
        builder.insert(label, inst);
29,946✔
379

380
        if (first) {
29,946✔
381
            // Add an edge from the caller to the new block.
382
            first = false;
718✔
383
            builder.add_child(caller_label, label);
718✔
384
        }
385

386
        // Walk all successor nodes, enqueuing any not-yet-cloned macro block.
387
        for (const auto& next_macro_label : builder.prog.cfg().children_of(macro_label)) {
63,506✔
388
            if (next_macro_label != builder.prog.cfg().exit_label() && !seen_labels.contains(next_macro_label)) {
50,340✔
389
                macro_labels.insert(next_macro_label);
29,232✔
390
                seen_labels.insert(next_macro_label);
29,232✔
391
            }
392
        }
393
    }
59,896✔
394

395
    // Reconstruct the cloned subprogram's internal edges now that every macro block
396
    // has been cloned. This must run as a second pass: reconstructing edges while
397
    // cloning (from already-cloned predecessors) loses any back-edge, because a loop
398
    // latch is cloned after its head, so the latch->head edge is never re-added. The
399
    // clone would then be analyzed straight-line, without widening or loop-counter
400
    // insertion, missing a pointer that walks out of bounds across iterations (#1203).
401
    for (const Label& macro_label : seen_labels) {
30,648✔
402
        const Label label{macro_label.from, macro_label.to, stack_frame_prefix};
44,901✔
403
        for (const auto& next_macro_label : builder.prog.cfg().children_of(macro_label)) {
63,478✔
404
            if (next_macro_label == builder.prog.cfg().exit_label()) {
50,316✔
405
                // This is an exit transition, so add an edge to the block to execute
406
                // upon returning from the macro.
407
                builder.add_child(label, exit_to_label);
714✔
408
            } else {
409
                const Label next_label{next_macro_label.from, next_macro_label.to, stack_frame_prefix};
49,245✔
410
                builder.add_child(label, next_label);
32,830✔
411
            }
32,830✔
412
        }
413
    }
29,934✔
414

415
    // Remove the original edge from the caller node to its successor,
416
    // since processing now goes through the function macro instead.
417
    builder.remove_child(caller_label, exit_to_label);
714✔
418

419
    // Finally, recurse to replace any nested function macros.
420
    for (const auto& macro_label : seen_labels) {
30,588✔
421
        const Label label{macro_label.from, macro_label.to, caller_label_str};
44,873✔
422
        if (const auto pins = std::get_if<CallLocal>(&builder.prog.instruction_at(label))) {
44,841✔
423
            add_cfg_nodes(builder, label, pins->target, max_call_stack_frames);
132✔
424
        }
425
    }
29,904✔
426
}
2,617✔
427

428
struct Imm64Parts {
429
    int32_t lo{};
430
    int32_t hi{};
431
};
432

433
static uint64_t merge_imm32_to_u64(const Imm64Parts parts) {
4✔
434
    return static_cast<uint64_t>(static_cast<uint32_t>(parts.lo)) |
4✔
435
           (static_cast<uint64_t>(static_cast<uint32_t>(parts.hi)) << 32);
4✔
436
}
437

438
/// Lower a single LoadPseudo to a concrete instruction.
439
/// VARIABLE_ADDR is lowered to an immediate scalar MOV; MAP_BY_IDX / MAP_VALUE_BY_IDX are
440
/// rewritten against the current map descriptor table. CODE_ADDR is kept as LoadPseudo by
441
/// pass_lower_pseudo_loads so the abstract transformer can type it as T_FUNC; this helper
442
/// is never called for CODE_ADDR.
443
static Instruction lower_pseudo_load(const LoadPseudo& pseudo, const ProgramInfo& info) {
12✔
444
    if (pseudo.addr.kind == PseudoAddress::Kind::VARIABLE_ADDR) {
12✔
445
        return Bin{
10✔
446
            .op = Bin::Op::MOV,
447
            .dst = pseudo.dst,
448
            .v = Imm{merge_imm32_to_u64({.lo = pseudo.addr.imm, .hi = pseudo.addr.next_imm})},
6✔
449
            .is64 = true,
450
            .lddw = true,
451
        };
4✔
452
    }
453

454
    const auto& descriptors = info.map_descriptors;
8✔
455
    if (pseudo.addr.imm < 0 || static_cast<size_t>(pseudo.addr.imm) >= descriptors.size()) {
8✔
456
        throw InvalidControlFlow{"invalid map index " + std::to_string(pseudo.addr.imm) + " (have " +
10✔
457
                                 std::to_string(descriptors.size()) + " maps)"};
12✔
458
    }
459
    const auto map_idx = static_cast<size_t>(pseudo.addr.imm);
4✔
460
    const int mapfd = descriptors.at(map_idx).original_fd;
4✔
461
    switch (pseudo.addr.kind) {
4✔
462
    case PseudoAddress::Kind::MAP_BY_IDX: return LoadMapFd{.dst = pseudo.dst, .mapfd = mapfd};
2✔
463
    case PseudoAddress::Kind::MAP_VALUE_BY_IDX:
2✔
464
        return LoadMapAddress{.dst = pseudo.dst, .mapfd = mapfd, .offset = pseudo.addr.next_imm};
2✔
UNCOV
465
    default: CRAB_ERROR("Invalid address kind: ", static_cast<int>(pseudo.addr.kind));
×
466
    }
467
}
468

469
using LoweredPseudoLoads = std::map<Label, Instruction>;
470

471
// Pass: LowerPseudoLoads
472
// Reads    : instruction sequence, program info (map_descriptors).
473
// Writes   : returns a Label -> Instruction map with the concrete replacement for every
474
//            LoadPseudo that is lowered. CODE_ADDR LoadPseudo instructions are intentionally
475
//            excluded so they remain observable to the abstract transformer (which types
476
//            them as T_FUNC); every other kind is replaced.
477
// Throws   : InvalidControlFlow if a MAP_BY_IDX / MAP_VALUE_BY_IDX references an out-of-range
478
//            map descriptor.
479
// Invariant: pass_populate_nodes consults this map to substitute LoadPseudo with its lowered
480
//            form while inserting CFG nodes.
481
static LoweredPseudoLoads pass_lower_pseudo_loads(const InstructionSeq& insts, const ProgramInfo& info) {
4,216✔
482
    LoweredPseudoLoads lowered;
4,216✔
483
    for (const auto& [label, inst, _] : insts) {
1,093,628✔
484
        const auto* pseudo = std::get_if<LoadPseudo>(&inst);
1,089,416✔
485
        if (!pseudo || pseudo->addr.kind == PseudoAddress::Kind::CODE_ADDR) {
544,722✔
486
            continue;
1,089,404✔
487
        }
488
        lowered.insert_or_assign(label, lower_pseudo_load(*pseudo, info));
16✔
489
    }
490
    return lowered;
4,212✔
491
}
4✔
492

493
// Pass: BuildInitialCfg -- populate_nodes step.
494
// Reads    : instruction sequence, program info, resolved kfunc map, lowered pseudo-load map.
495
// Writes   : inserts one CFG node per live instruction into builder (labels + instructions).
496
//            CallBtf is replaced with the resolved Call; non-CODE_ADDR LoadPseudo is replaced
497
//            with its lowered form; every other instruction is inserted verbatim.
498
// Throws   : InvalidControlFlow if either substitution map is inconsistent with the sequence
499
//            (internal error; indicates a missing prior pass).
500
// Invariant: pass_validate_instruction_support, pass_resolve_kfunc_calls and
501
//            pass_lower_pseudo_loads have been applied on the same instruction sequence.
502
static void pass_populate_nodes(CfgBuilder& builder, const InstructionSeq& insts, const ProgramInfo& info,
4,212✔
503
                                const ResolvedKfuncCalls& resolved_kfunc_calls,
504
                                const LoweredPseudoLoads& lowered_pseudo_loads) {
505
    for (const auto& [label, inst, _] : insts) {
1,093,624✔
506
        assert(!check_instruction_feature_support(inst, info).has_value() &&
1,089,412✔
507
               "instruction support must be validated before CFG construction");
508
        if (std::holds_alternative<Undefined>(inst)) {
1,089,412✔
UNCOV
509
            continue;
×
510
        }
511
        if (std::holds_alternative<CallBtf>(inst)) {
1,089,412✔
512
            const auto it = resolved_kfunc_calls.find(label);
38✔
513
            if (it == resolved_kfunc_calls.end()) {
38✔
UNCOV
514
                CRAB_ERROR("missing validated kfunc resolution at ", to_string(label));
×
515
            }
516
            builder.insert(label, it->second);
38✔
517
            continue;
38✔
518
        }
38✔
519
        if (const auto it = lowered_pseudo_loads.find(label); it != lowered_pseudo_loads.end()) {
1,089,374✔
520
            builder.insert(label, it->second);
8✔
521
            continue;
8✔
522
        }
523
        builder.insert(label, inst);
1,089,366✔
524
    }
525
}
4,212✔
526

527
// Pass: BuildInitialCfg -- connect_edges step (also performs InsertAssumeEdges).
528
// Reads    : instruction sequence, must_have_exit flag.
529
// Writes   : CFG edges from entry, and for every populated node to its successors.
530
//            Conditional Jmp instructions are materialised as two synthetic Assume
531
//            jump-labels (insert_jump) carrying the positive and negated conditions.
532
// Throws   : InvalidControlFlow on empty sequence, fallthrough past the final instruction,
533
//            or a jump whose target label is not in the CFG.
534
// Invariant: pass_populate_nodes has been applied (all nodes exist before edges are added).
535
static void pass_connect_edges(CfgBuilder& builder, const InstructionSeq& insts, const bool must_have_exit) {
4,212✔
536
    if (insts.empty()) {
4,212✔
537
        throw InvalidControlFlow{"empty instruction sequence"};
5✔
538
    }
539
    // Ordering check: pass_populate_nodes must run first so that every non-Undefined label
540
    // referenced below (the entry's target, jump targets, fallthrough labels) already exists.
541
    assert(std::holds_alternative<Undefined>(std::get<1>(insts[0])) ||
4,210✔
542
           builder.prog.cfg().contains(std::get<0>(insts[0])));
543
    builder.add_child(builder.prog.cfg().entry_label(), std::get<0>(insts[0]));
4,210✔
544

545
    for (size_t i = 0; i < insts.size(); i++) {
1,093,616✔
546
        const auto& [label, inst, _0] = insts[i];
1,089,410✔
547

548
        if (std::holds_alternative<Undefined>(inst)) {
1,089,410✔
549
            continue;
172✔
550
        }
551
        Label fallthrough{builder.prog.cfg().exit_label()};
1,089,410✔
552
        if (i + 1 < insts.size()) {
1,089,410✔
553
            fallthrough = std::get<0>(insts[i + 1]);
1,085,202✔
554
        } else {
555
            if (has_fall(inst) && must_have_exit) {
5,129✔
556
                throw InvalidControlFlow{"fallthrough in last instruction"};
5✔
557
            }
558
        }
559
        if (const auto jmp = std::get_if<Jmp>(&inst)) {
1,089,408✔
560
            if (const auto cond = jmp->cond) {
121,728✔
561
                Label target_label = jmp->target;
99,560✔
562
                if (target_label == fallthrough) {
99,560✔
563
                    builder.add_child(label, fallthrough);
172✔
564
                    continue;
172✔
565
                }
566
                if (!builder.prog.cfg().contains(target_label)) {
99,388✔
567
                    throw InvalidControlFlow{"jump to undefined label " + to_string(target_label)};
3✔
568
                }
569
                builder.insert_jump(label, target_label, Assume{.cond = *cond, .is_implicit = true});
149,079✔
570
                builder.insert_jump(label, fallthrough, Assume{.cond = reverse(*cond), .is_implicit = true});
149,080✔
571
            } else {
99,560✔
572
                builder.add_child(label, jmp->target);
22,168✔
573
            }
574
        } else {
575
            if (has_fall(inst)) {
967,680✔
576
                builder.add_child(label, fallthrough);
963,962✔
577
            }
578
        }
579
        if (std::holds_alternative<Exit>(inst)) {
1,089,234✔
580
            builder.add_child(label, builder.prog.cfg().exit_label());
5,579✔
581
        }
582
    }
1,089,408✔
583
}
4,206✔
584

585
// Pass: InlineLocalCalls
586
// Reads    : instruction sequence, max_call_stack_frames bound.
587
// Writes   : for every CallLocal in the sequence, clones the callee region into the CFG
588
//            under a unique stack-frame prefix. Recurses into nested calls.
589
// Throws   : InvalidControlFlow on illegal recursion or exceeding the call-stack frame bound.
590
// Invariant: pass_connect_edges has been applied -- inlining walks existing parents/children.
591
//            Restricted to callees that are reachable after edge connection, which is why
592
//            this runs as a separate second pass rather than during population.
593
static void pass_inline_local_calls(CfgBuilder& builder, const InstructionSeq& insts, const int max_call_stack_frames) {
4,206✔
594
    // Ordering check: pass_connect_edges must have run. When insts is non-empty, its first
595
    // label has been wired as a child of Label::entry, so entry has at least one successor.
596
    assert(insts.empty() || !builder.prog.cfg().children_of(Label::entry).empty());
4,206✔
597
    for (const auto& [label, inst, _] : insts) {
1,093,556✔
598
        if (const auto pins = std::get_if<CallLocal>(&inst)) {
1,089,649✔
599
            add_cfg_nodes(builder, label, pins->target, max_call_stack_frames);
590✔
600
        }
601
    }
602
}
4,198✔
603

604
static bool is_tail_call_helper(const Call& call, const ebpf_platform_t& platform,
59,848✔
605
                                const EbpfProgramType& program_type) {
606
    if (call.kind != CallKind::helper) {
59,848✔
607
        return false;
1,248✔
608
    }
609
    if (!platform.is_helper_usable(call.func, program_type)) {
57,352✔
610
        return false;
611
    }
612
    return platform.get_helper_prototype(call.func, program_type).return_type ==
57,352✔
613
           EBPF_RETURN_TYPE_INTEGER_OR_NO_RETURN_IF_SUCCEED;
57,352✔
614
}
615

616
static bool is_tail_call_site(const Instruction& ins, const ebpf_platform_t& platform,
1,293,716✔
617
                              const EbpfProgramType& program_type) {
618
    if (const auto* call = std::get_if<Call>(&ins)) {
1,293,716✔
619
        return is_tail_call_helper(*call, platform, program_type);
59,848✔
620
    }
621
    if (std::holds_alternative<Callx>(ins)) {
1,233,868✔
622
        // At CFG-construction time, callx target ids are not available.
623
        // Conservatively treat callx as a potential tail-call site.
624
        return true;
50✔
625
    }
626
    return false;
616,909✔
627
}
628

629
static void collect_wto_labels(const CycleOrLabel& component, std::set<Label>& labels) {
1,293,070✔
630
    // Iterative (explicit work-stack) rather than recursive on cycle-nesting depth.
631
    // This runs unconditionally for every program via pass_validate_tail_call_depth,
632
    // so a crafted deeply-nested CFG must not be able to overflow the C++ stack here.
633
    std::vector<const CycleOrLabel*> stack{&component};
1,939,605✔
634
    while (!stack.empty()) {
2,586,874✔
635
        const CycleOrLabel* const current = stack.back();
1,293,804✔
636
        stack.pop_back();
1,293,804✔
637
        if (const auto plabel = std::get_if<Label>(current)) {
1,293,804✔
638
            labels.insert(*plabel);
1,293,716✔
639
            continue;
1,293,716✔
640
        }
641
        for (const auto& nested_component : *std::get<std::shared_ptr<WtoCycle>>(*current)) {
822✔
642
            stack.push_back(&nested_component);
734✔
643
        }
644
    }
645
}
1,293,070✔
646

647
// Pass: ValidateTailCallDepth
648
// Reads    : Program (CFG + instructions), Wto, platform, program type.
649
// Writes   : nothing.
650
// Throws   : InvalidControlFlow if the reachable tail-call chain exceeds the fixed limit.
651
// Notes    : Counts tail-call sites along the longest path through the reachable maximal-SCC DAG
652
//            so cycles do not inflate depth. Maximal SCCs are derived from WTO nesting: labels in
653
//            the same outermost WTO cycle are mutually reachable and form one maximal SCC.
654
static void pass_validate_tail_call_depth(const Program& prog, const Wto& wto, const ebpf_platform_t& platform,
4,198✔
655
                                          const EbpfProgramType& program_type) {
656
    constexpr int tail_call_chain_limit = 33;
4,198✔
657

658
    // WTO only covers labels reachable from entry.
659
    std::set<Label> reachable;
4,198✔
660
    for (const auto& component : wto) {
1,297,268✔
661
        collect_wto_labels(component, reachable);
1,293,070✔
662
    }
663

664
    // Partition reachable labels by maximal SCC representative:
665
    // the outermost containing WTO head, or the label itself if not in a cycle.
666
    std::map<Label, Label> maximal_scc_of;
4,198✔
667
    std::set<Label> maximal_scc_ids;
4,198✔
668
    for (const auto& label : reachable) {
1,297,914✔
669
        const Label scc_id = wto.nesting(label).outermost_head().value_or(label);
2,587,433✔
670
        maximal_scc_of.emplace(label, scc_id);
1,293,716✔
671
        maximal_scc_ids.insert(scc_id);
1,293,716✔
672
    }
1,293,716✔
673

674
    std::map<Label, int> tail_sites_per_scc;
4,198✔
675
    std::map<Label, std::optional<Label>> representative_tail_label;
4,198✔
676
    std::map<Label, std::set<Label>> dag_successors;
4,198✔
677
    std::map<Label, int> indegree;
4,198✔
678
    for (const auto& scc_id : maximal_scc_ids) {
1,297,268✔
679
        tail_sites_per_scc.emplace(scc_id, 0);
1,293,070✔
680
        representative_tail_label.emplace(scc_id, std::nullopt);
1,293,070✔
681
        dag_successors.emplace(scc_id, std::set<Label>{});
1,293,070✔
682
        indegree.emplace(scc_id, 0);
1,293,071✔
683
    }
684

685
    for (const auto& label : reachable) {
1,297,914✔
686
        const Label src_scc = maximal_scc_of.at(label);
1,293,716✔
687
        if (is_tail_call_site(prog.instruction_at(label), platform, program_type)) {
1,293,716✔
688
            ++tail_sites_per_scc.at(src_scc);
1,648✔
689
            auto& representative = representative_tail_label.at(src_scc);
1,648✔
690
            if (!representative.has_value()) {
1,648✔
691
                representative = label;
1,648✔
692
            }
693
        }
694
        for (const auto& child : prog.cfg().children_of(label)) {
2,682,700✔
695
            if (!reachable.contains(child)) {
1,388,984✔
UNCOV
696
                continue;
×
697
            }
698
            const Label dst_scc = maximal_scc_of.at(child);
1,388,984✔
699
            if (src_scc != dst_scc && dag_successors[src_scc].insert(dst_scc).second) {
1,388,984✔
700
                ++indegree.at(dst_scc);
1,388,232✔
701
            }
702
        }
1,388,984✔
703
    }
1,293,716✔
704

705
    std::map<Label, int> indegree_for_sources = indegree;
4,198✔
706
    std::vector<Label> topo_order;
4,198✔
707
    topo_order.reserve(maximal_scc_ids.size());
4,198✔
708
    for (const auto& scc_id : maximal_scc_ids) {
1,297,268✔
709
        if (indegree.at(scc_id) == 0) {
1,293,070✔
710
            topo_order.push_back(scc_id);
4,198✔
711
        }
712
    }
713
    for (size_t index = 0; index < topo_order.size(); ++index) {
1,297,268✔
714
        const Label scc_id = topo_order[index];
1,293,070✔
715
        for (const auto& succ : dag_successors.at(scc_id)) {
2,681,302✔
716
            --indegree.at(succ);
1,388,232✔
717
            if (indegree.at(succ) == 0) {
1,388,232✔
718
                topo_order.push_back(succ);
1,288,872✔
719
            }
720
        }
721
    }
1,293,070✔
722
    if (topo_order.size() != maximal_scc_ids.size()) {
4,198✔
UNCOV
723
        CRAB_ERROR("WTO-derived SCC graph must be acyclic");
×
724
    }
725

726
    // Longest path over maximal SCC DAG by tail-call site count.
727
    constexpr int uninitialized_depth = std::numeric_limits<int>::min();
4,198✔
728
    std::map<Label, int> max_tail_depth;
4,198✔
729
    std::map<Label, std::optional<Label>> depth_label;
4,198✔
730
    for (const auto& scc_id : maximal_scc_ids) {
1,297,268✔
731
        max_tail_depth.emplace(scc_id, uninitialized_depth);
1,293,070✔
732
        depth_label.emplace(scc_id, std::nullopt);
1,293,070✔
733
        if (indegree_for_sources.at(scc_id) == 0) {
1,293,070✔
734
            max_tail_depth.at(scc_id) = tail_sites_per_scc.at(scc_id);
4,198✔
735
            depth_label.at(scc_id) = representative_tail_label.at(scc_id);
650,733✔
736
        }
737
    }
738

739
    for (const auto& scc_id : topo_order) {
1,297,262✔
740
        const int current_depth = max_tail_depth.at(scc_id);
1,293,066✔
741
        if (current_depth == uninitialized_depth) {
1,293,066✔
UNCOV
742
            continue;
×
743
        }
744
        if (current_depth > tail_call_chain_limit) {
1,293,066✔
745
            const Label at = depth_label.at(scc_id).value_or(scc_id);
2✔
746
            throw InvalidControlFlow{"tail call chain depth exceeds " + std::to_string(tail_call_chain_limit) +
4✔
747
                                     " (at " + to_string(at) + ")"};
7✔
748
        }
2✔
749
        for (const auto& succ : dag_successors.at(scc_id)) {
2,681,292✔
750
            const int candidate_depth = current_depth + tail_sites_per_scc.at(succ);
1,388,228✔
751
            if (candidate_depth > max_tail_depth.at(succ)) {
1,388,228✔
752
                max_tail_depth.at(succ) = candidate_depth;
1,289,822✔
753
                depth_label.at(succ) = representative_tail_label.at(succ).has_value()
2,579,644✔
754
                                           ? representative_tail_label.at(succ)
1,650✔
755
                                           : depth_label.at(scc_id);
2,628,847✔
756
            }
757
        }
758
    }
759
}
4,216✔
760

761
// Pass: ComputeCallbackMetadata
762
// Reads    : Program (CFG + instructions).
763
// Writes   : builder.prog's callback metadata, via CfgBuilder::set_callback_metadata: the set
764
//            of top-level concrete-instruction labels eligible as PTR_TO_FUNC targets, and the
765
//            subset whose body can reach a top-level Exit.
766
// Notes    : Excludes Label::entry/Label::exit, synthetic jump labels, labels under an inlined
767
//            stack-frame prefix, and Exit instructions themselves.
768
static void pass_compute_callback_metadata(CfgBuilder& builder) {
4,196✔
769
    const Program& prog = builder.prog;
4,196✔
770
    CallbackMetadata md;
4,196✔
771
    for (const Label& label : prog.labels()) {
1,330,514✔
772
        if (label == Label::entry || label == Label::exit || label.isjump() || !label.stack_frame_prefix.empty()) {
1,326,318✔
773
            continue;
237,038✔
774
        }
775
        if (std::holds_alternative<Exit>(prog.instruction_at(label))) {
1,089,280✔
776
            continue;
3,688✔
777
        }
778
        md.target_labels.insert(label.from);
1,085,592✔
779
    }
780

781
    const auto has_reachable_top_level_exit = [&](const Label& start) {
1,087,690✔
782
        std::set<Label> seen;
1,085,592✔
783
        std::vector<Label> worklist{start};
2,713,980✔
784
        while (!worklist.empty()) {
303,232,004✔
785
            Label label = worklist.back();
303,231,990✔
786
            worklist.pop_back();
303,231,990✔
787
            if (seen.contains(label)) {
303,231,990✔
788
                continue;
264✔
789
            }
790
            seen.insert(label);
303,231,726✔
791
            if (label == Label::exit) {
303,231,726✔
792
                return true;
812✔
793
            }
794
            if (label != Label::entry && prog.cfg().contains(label) &&
758,075,255✔
795
                std::holds_alternative<Exit>(prog.instruction_at(label)) && label.stack_frame_prefix.empty()) {
758,075,255✔
796
                return true;
541,977✔
797
            }
798
            for (const Label& child : prog.cfg().children_of(label)) {
624,199,978✔
799
                worklist.push_back(child);
322,053,830✔
800
            }
801
        }
303,231,990✔
802
        return false;
7✔
803
    };
2,171,184✔
804
    for (const int32_t label_num : md.target_labels) {
1,089,788✔
805
        const Label label{gsl::narrow<int>(label_num)};
1,085,592✔
806
        if (has_reachable_top_level_exit(label)) {
1,085,592✔
807
            md.targets_with_exit.insert(label_num);
1,085,578✔
808
        }
809
    }
1,085,592✔
810
    builder.set_callback_metadata(std::move(md));
4,196✔
811
}
4,196✔
812

813
// Pass: InsertTerminationCounters
814
// Reads    : WTO of the current CFG.
815
// Writes   : For each WTO loop head, inserts an IncrementLoopCounter at a synthetic
816
//            increment-counter label placed between the head and its successors (CFG edges and
817
//            instructions are both mutated via CfgBuilder::insert_after).
818
// Notes    : WTO identifies every strongly connected component and its entry point(s), which
819
//            are the natural locations for counters that help verify program termination.
820
static void pass_insert_termination_counters(CfgBuilder& builder, const Wto& wto) {
50✔
821
    wto.for_each_loop_head([&](const Label& label) -> void {
99✔
822
        builder.insert_after(label, Label::make_increment_counter(label), IncrementLoopCounter{label});
96✔
823
    });
48✔
824
}
50✔
825

826
// Pass: ExtractAssertions
827
// Reads    : Program instructions, ProgramInfo, options.
828
// Writes   : Populates builder.prog.m_assertions with the per-label precondition vector
829
//            (memory bounds, type guards, etc.) produced by get_assertions.
830
// Notes    : Runs for every label in the CFG, including synthetic ones (Assume / counters).
831
static void pass_extract_assertions(CfgBuilder& builder, const ProgramInfo& info, const VerifierOptions& options) {
4,196✔
832
    for (const auto& label : builder.prog.labels()) {
1,330,562✔
833
        builder.set_assertions(label, get_assertions(builder.prog.instruction_at(label), info, options.runtime, label));
1,989,549✔
834
    }
835
}
4,196✔
836

837
// from_sequence orchestrates the preparation pipeline. Each pass has a documented
838
// pre/postcondition; this function's job is just to sequence them and hand the
839
// result off as a finalised Program.
840
Program Program::from_sequence(const InstructionSeq& inst_seq, const ProgramInfo& info,
4,288✔
841
                               const VerifierOptions& options) {
842
    // --- Pass: ValidateOptions --------------------------------------------
843
    options.runtime.validate();
4,288✔
844
    // Preserves the platform-non-null invariant for every subsequent pass in this pipeline.
845
    assert(info.platform != nullptr && "info.platform must be set before Program::from_sequence");
4,288✔
846

847
    // --- Pass: ValidateInstructionSupport ---------------------------------
848
    pass_validate_instruction_support(inst_seq, info);
4,288✔
849

850
    // --- Pass: ResolveKfuncCalls ------------------------------------------
851
    const ResolvedKfuncCalls resolved_kfunc_calls = pass_resolve_kfunc_calls(inst_seq, info);
4,242✔
852

853
    // --- Pass: LowerPseudoLoads -------------------------------------------
854
    const LoweredPseudoLoads lowered_pseudo_loads = pass_lower_pseudo_loads(inst_seq, info);
4,216✔
855

856
    // --- Pass: BuildInitialCfg (nodes, then edges with InsertAssumeEdges) -
857
    CfgBuilder builder;
4,212✔
858
    // Populate m_info up front so any pass that reads builder.prog.info() sees the real
859
    // ProgramInfo rather than a default-constructed (null-platform) one.
860
    builder.prog.m_info = info;
4,212✔
861
    pass_populate_nodes(builder, inst_seq, info, resolved_kfunc_calls, lowered_pseudo_loads);
4,212✔
862
    pass_connect_edges(builder, inst_seq, options.must_have_exit);
4,212✔
863

864
    // --- Pass: InlineLocalCalls -------------------------------------------
865
    pass_inline_local_calls(builder, inst_seq, options.runtime.max_call_stack_frames);
4,206✔
866

867
    // --- Pass: ValidateTailCallDepth --------------------------------------
868
    const Wto wto{builder.prog.cfg()};
4,198✔
869
    pass_validate_tail_call_depth(builder.prog, wto, *info.platform, info.type);
4,198✔
870

871
    // --- Pass: ComputeCallbackMetadata ------------------------------------
872
    pass_compute_callback_metadata(builder);
4,196✔
873

874
    // --- Pass: InsertTerminationCounters ----------------------------------
875
    if (options.runtime.check_for_termination) {
4,196✔
876
        pass_insert_termination_counters(builder, wto);
50✔
877
    }
878

879
    // --- Pass: ExtractAssertions ------------------------------------------
880
    pass_extract_assertions(builder, info, options);
4,196✔
881

882
    return std::move(builder.prog);
6,294✔
883
}
4,240✔
884

885
std::set<BasicBlock> BasicBlock::collect_basic_blocks(const Cfg& cfg, const bool simplify) {
2✔
886
    if (!simplify) {
2✔
887
        std::set<BasicBlock> res;
2✔
888
        for (const Label& label : cfg.labels()) {
32✔
889
            if (label != cfg.entry_label() && label != cfg.exit_label()) {
73✔
890
                res.insert(BasicBlock{label});
39✔
891
            }
892
        }
893
        return res;
2✔
894
    }
2✔
895

896
    std::set<BasicBlock> res;
×
897
    std::set<Label> worklist;
×
UNCOV
898
    for (const Label& label : cfg.labels()) {
×
899
        worklist.insert(label);
×
900
    }
901
    std::set<Label> seen;
×
902
    while (!worklist.empty()) {
×
903
        Label label = *worklist.begin();
×
904
        worklist.erase(label);
×
UNCOV
905
        if (seen.contains(label)) {
×
906
            continue;
×
907
        }
908
        seen.insert(label);
×
909

UNCOV
910
        if (cfg.in_degree(label) == 1 && cfg.num_siblings(label) == 1) {
×
911
            continue;
×
912
        }
913
        BasicBlock bb{label};
×
UNCOV
914
        while (cfg.out_degree(label) == 1) {
×
915
            const Label& next_label = cfg.get_child(bb.last_label());
×
916

UNCOV
917
            if (seen.contains(next_label) || next_label == cfg.exit_label() || cfg.in_degree(next_label) != 1) {
×
918
                break;
919
            }
920

921
            if (bb.first_label() == cfg.entry_label()) {
×
922
                // Entry instruction is Undefined. We want to start with 0
923
                bb.m_ts.clear();
×
924
            }
925
            bb.m_ts.push_back(next_label);
×
926

UNCOV
927
            worklist.erase(next_label);
×
928
            seen.insert(next_label);
×
929

930
            label = next_label;
×
931
        }
×
932
        res.emplace(std::move(bb));
×
933
    }
×
UNCOV
934
    return res;
×
UNCOV
935
}
×
936

937
/// Get the type of given Instruction.
938
/// Most of these type names are also statistics header labels.
939
Cfg cfg_from_adjacency_list(const std::map<Label, std::vector<Label>>& AdjList) {
8✔
940
    CfgBuilder builder;
8✔
941
    for (const auto& label : std::views::keys(AdjList)) {
66✔
942
        if (label == Label::entry || label == Label::exit) {
58✔
943
            continue;
8✔
944
        }
945
        builder.insert(label, Undefined{});
75✔
946
    }
947
    for (const auto& [label, children] : AdjList) {
66✔
948
        for (const auto& child : children) {
136✔
949
            builder.add_child(label, child);
78✔
950
        }
951
    }
952
    return builder.prog.cfg();
16✔
953
}
8✔
954
} // namespace prevail
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc