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

Alan-Jowett / ebpf-verifier / 22235569093

20 Feb 2026 02:12AM UTC coverage: 88.002% (-0.2%) from 88.157%
22235569093

push

github

web-flow
Handle Call builtins: fix handling of Falco tests  (#1025)

* falco: fix raw_tracepoint privilege and group expected failures

Mark raw_tracepoint/raw_tracepoint_writable as privileged program types so Falco raw-tracepoint sections are not treated as unprivileged argument checks.

Update Falco sample matrix to move now-passing sections out of TEST_SECTION_FAIL and group the remaining expected failures by root-cause class (offset lower-bound loss vs size lower-bound loss at correlated joins).

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

* elf/unmarshal: gate builtin relocations via platform call model

Add platform hooks to resolve builtin symbols and provide builtin call contracts, thread relocation-gated builtin call offsets through ProgramInfo, and only treat static helper IDs as builtins at gated call sites.

Also extend platform-table, marshal, and YAML-platform tests to cover builtin resolver wiring and call unmarshal behavior.
* crab: canonicalize unsigned intervals in bitwise_and
When uvalue intervals temporarily carry signed lower bounds (e.g. after joins), Interval::bitwise_and asserted in debug builds. Canonicalize both operands via zero_extend(64) before unsigned bitwise reasoning, preserving soundness and avoiding debug aborts.

Validated by reproducing SIGABRT on reverted code in [falco][verify] and confirming the patched build completes with expected 73 pass / 20 failed-as-expected.

* Fix unsound bitwise_and case for non-singleton all-ones rhs

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

239 of 252 new or added lines in 9 files covered. (94.84%)

602 existing lines in 16 files now uncovered.

11743 of 13344 relevant lines covered (88.0%)

3262592.78 hits per line

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

78.03
/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/program.hpp"
16
#include "ir/syntax.hpp"
17
#include "platform.hpp"
18

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

25
namespace prevail {
26
struct CfgBuilder final {
1✔
27
    Program prog;
28

29
    // TODO: ins should be inserted elsewhere
30
    void insert_after(const Label& prev_label, const Label& new_label, const Instruction& ins) {
40✔
31
        if (prev_label == new_label) {
40✔
UNCOV
32
            CRAB_ERROR("Cannot insert after the same label ", to_string(new_label));
×
33
        }
34
        std::set<Label> prev_children;
40✔
35
        std::swap(prev_children, prog.m_cfg.get_node(prev_label).children);
40✔
36

37
        for (const Label& next_label : prev_children) {
88✔
38
            prog.m_cfg.get_node(next_label).parents.erase(prev_label);
48✔
39
        }
40

41
        insert(new_label, ins);
40✔
42
        for (const Label& next_label : prev_children) {
88✔
43
            add_child(prev_label, new_label);
48✔
44
            add_child(new_label, next_label);
48✔
45
        }
46
    }
40✔
47

48
    // TODO: ins should be inserted elsewhere
49
    void insert(const Label& _label, const Instruction& ins) {
658,962✔
50
        if (const auto it = prog.m_cfg.neighbours.find(_label); it != prog.m_cfg.neighbours.end()) {
658,962✔
UNCOV
51
            CRAB_ERROR("Label ", to_string(_label), " already exists");
×
52
        }
53
        prog.m_cfg.neighbours.emplace(_label, Cfg::Adjacent{});
658,962✔
54
        prog.m_instructions.emplace(_label, ins);
658,962✔
55
    }
658,962✔
56

57
    // TODO: ins should be inserted elsewhere
58
    Label insert_jump(const Label& from, const Label& to, const Instruction& ins) {
95,864✔
59
        const Label jump_label = Label::make_jump(from, to);
95,864✔
60
        if (prog.m_cfg.contains(jump_label)) {
95,864✔
UNCOV
61
            CRAB_ERROR("Jump label ", to_string(jump_label), " already exists");
×
62
        }
63
        insert(jump_label, ins);
95,864✔
64
        add_child(from, jump_label);
95,864✔
65
        add_child(jump_label, to);
95,864✔
66
        return jump_label;
95,864✔
UNCOV
67
    }
×
68

69
    void add_child(const Label& a, const Label& b) {
710,152✔
70
        assert(b != Label::entry);
710,152✔
71
        assert(a != Label::exit);
710,152✔
72
        prog.m_cfg.neighbours.at(a).children.insert(b);
710,152✔
73
        prog.m_cfg.neighbours.at(b).parents.insert(a);
710,152✔
74
    }
710,152✔
75

76
    void remove_child(const Label& a, const Label& b) {
140✔
77
        prog.m_cfg.get_node(a).children.erase(b);
140✔
78
        prog.m_cfg.get_node(b).parents.erase(a);
140✔
79
    }
140✔
80

81
    void set_assertions(const Label& label, const std::vector<Assertion>& assertions) {
664,750✔
82
        if (!prog.m_cfg.contains(label)) {
664,750✔
UNCOV
83
            CRAB_ERROR("Label ", to_string(label), " not found in the CFG: ");
×
84
        }
85
        prog.m_assertions.insert_or_assign(label, assertions);
664,750✔
86
    }
664,750✔
87
};
88

89
/// Get the inverse of a given comparison operation.
90
static Condition::Op reverse(const Condition::Op op) {
47,932✔
91
    switch (op) {
47,932✔
92
    case Condition::Op::EQ: return Condition::Op::NE;
11,233✔
93
    case Condition::Op::NE: return Condition::Op::EQ;
5,923✔
94

95
    case Condition::Op::GE: return Condition::Op::LT;
193✔
96
    case Condition::Op::LT: return Condition::Op::GE;
383✔
97

98
    case Condition::Op::SGE: return Condition::Op::SLT;
54✔
99
    case Condition::Op::SLT: return Condition::Op::SGE;
825✔
100

101
    case Condition::Op::LE: return Condition::Op::GT;
47✔
102
    case Condition::Op::GT: return Condition::Op::LE;
2,970✔
103

104
    case Condition::Op::SLE: return Condition::Op::SGT;
21✔
105
    case Condition::Op::SGT: return Condition::Op::SLE;
2,298✔
106

107
    case Condition::Op::SET: return Condition::Op::NSET;
17✔
108
    case Condition::Op::NSET: return Condition::Op::SET;
2✔
109
    }
110
    assert(false);
111
    return {};
112
}
113

114
/// Get the inverse of a given comparison condition.
115
static Condition reverse(const Condition& cond) {
47,932✔
116
    return {.op = reverse(cond.op), .left = cond.left, .right = cond.right, .is64 = cond.is64};
47,932✔
117
}
118

119
static bool has_fall(const Instruction& ins) {
506,220✔
120
    if (std::holds_alternative<Exit>(ins)) {
506,220✔
121
        return false;
1,782✔
122
    }
123

124
    if (const auto pins = std::get_if<Jmp>(&ins)) {
502,656✔
125
        if (!pins->cond) {
262✔
126
            return false;
131✔
127
        }
128
    }
129

130
    return true;
251,197✔
131
}
132

133
enum class RejectKind {
134
    NotImplemented,
135
    Capability,
136
};
137

138
struct RejectionReason {
42✔
139
    RejectKind kind{};
140
    std::string detail;
141
};
142

143
static bool supports(const ebpf_platform_t& platform, const bpf_conformance_groups_t group) {
851,836✔
144
    return platform.supports_group(group);
849,929✔
145
}
146

147
static bool un_requires_base64(const Un& un) {
4,597✔
148
    switch (un.op) {
4,597✔
149
    case Un::Op::BE64:
35✔
150
    case Un::Op::LE64:
151
    case Un::Op::SWAP64: return true;
35✔
152
    default: return false;
3,029✔
153
    }
154
}
155

156
static std::optional<Call> resolve_kfunc_call(const CallBtf& call_btf, const ProgramInfo& info, std::string* why_not) {
36✔
157
    if (!info.platform || !info.platform->resolve_kfunc_call) {
36✔
UNCOV
158
        if (why_not) {
×
UNCOV
159
            *why_not = "kfunc resolution is unavailable on this platform";
×
160
        }
UNCOV
161
        return std::nullopt;
×
162
    }
163
    return info.platform->resolve_kfunc_call(call_btf.btf_id, &info, why_not);
36✔
164
}
165

166
using ResolvedKfuncCalls = std::map<Label, Call>;
167

168
static std::optional<RejectionReason> check_instruction_feature_support(const Instruction& ins,
847,609✔
169
                                                                        const ebpf_platform_t& platform) {
170
    auto reject_not_implemented = [](std::string detail) {
283,187✔
UNCOV
171
        return RejectionReason{.kind = RejectKind::NotImplemented, .detail = std::move(detail)};
×
172
    };
173
    auto reject_capability = [](std::string detail) {
283,229✔
174
        return RejectionReason{.kind = RejectKind::Capability, .detail = std::move(detail)};
42✔
175
    };
176

177
    if (const auto p = std::get_if<Call>(&ins)) {
847,609✔
178
        if (!p->is_supported) {
44,100✔
179
            return reject_capability(p->unsupported_reason);
3✔
180
        }
181
    }
182
    if (std::holds_alternative<Callx>(ins) && !supports(platform, bpf_conformance_groups_t::callx)) {
847,607✔
UNCOV
183
        return reject_capability("requires conformance group callx");
×
184
    }
185
    if ((std::holds_alternative<Call>(ins) || std::holds_alternative<CallLocal>(ins) ||
1,917,617✔
186
         std::holds_alternative<Callx>(ins) || std::holds_alternative<CallBtf>(ins) ||
1,873,181✔
187
         std::holds_alternative<Exit>(ins)) &&
2,215,289✔
188
        !supports(platform, bpf_conformance_groups_t::base32)) {
47,449✔
UNCOV
189
        return reject_capability("requires conformance group base32");
×
190
    }
191
    if (const auto p = std::get_if<Bin>(&ins)) {
998,846✔
192
        if (!supports(platform, p->is64 ? bpf_conformance_groups_t::base64 : bpf_conformance_groups_t::base32)) {
478,712✔
UNCOV
193
            return reject_capability(p->is64 ? "requires conformance group base64"
×
UNCOV
194
                                             : "requires conformance group base32");
×
195
        }
196
        if ((p->op == Bin::Op::MUL || p->op == Bin::Op::UDIV || p->op == Bin::Op::UMOD || p->op == Bin::Op::SDIV ||
450,816✔
197
             p->op == Bin::Op::SMOD) &&
754,425✔
198
            !supports(platform, p->is64 ? bpf_conformance_groups_t::divmul64 : bpf_conformance_groups_t::divmul32)) {
5,768✔
UNCOV
199
            return reject_capability(p->is64 ? "requires conformance group divmul64"
×
UNCOV
200
                                             : "requires conformance group divmul32");
×
201
        }
202
    }
203
    if (const auto p = std::get_if<Un>(&ins)) {
847,606✔
204
        const bool need_base64 = p->is64 || un_requires_base64(*p);
6,505✔
205
        if (!supports(platform, need_base64 ? bpf_conformance_groups_t::base64 : bpf_conformance_groups_t::base32)) {
4,878✔
206
            return reject_capability(need_base64 ? "requires conformance group base64"
6✔
207
                                                 : "requires conformance group base32");
2✔
208
        }
209
    }
210
    if (const auto p = std::get_if<Jmp>(&ins)) {
847,605✔
211
        if (!supports(platform,
137,549✔
212
                      p->cond ? (p->cond->is64 ? bpf_conformance_groups_t::base64 : bpf_conformance_groups_t::base32)
137,549✔
213
                              : bpf_conformance_groups_t::base32)) {
UNCOV
214
            return reject_capability((p->cond && p->cond->is64) ? "requires conformance group base64"
×
215
                                                                : "requires conformance group base32");
×
216
        }
217
    }
218
    if (const auto p = std::get_if<LoadPseudo>(&ins)) {
847,616✔
219
        if (!supports(platform, bpf_conformance_groups_t::base64)) {
33✔
220
            return reject_capability("requires conformance group base64");
×
221
        }
222
        switch (p->addr.kind) {
33✔
223
        case PseudoAddress::Kind::VARIABLE_ADDR:
22✔
224
        case PseudoAddress::Kind::CODE_ADDR:
225
        case PseudoAddress::Kind::MAP_BY_IDX:
226
        case PseudoAddress::Kind::MAP_VALUE_BY_IDX: break; // Resolved during CFG construction.
22✔
UNCOV
227
        default: return reject_not_implemented("lddw unknown pseudo");
×
228
        }
229
    }
230
    if ((std::holds_alternative<LoadMapFd>(ins) || std::holds_alternative<LoadMapAddress>(ins)) &&
863,126✔
231
        !supports(platform, bpf_conformance_groups_t::base64)) {
23,295✔
UNCOV
232
        return reject_capability("requires conformance group base64");
×
233
    }
234
    if (const auto p = std::get_if<Mem>(&ins)) {
847,605✔
235
        if (!supports(platform,
228,062✔
236
                      (p->access.width == 8) ? bpf_conformance_groups_t::base64 : bpf_conformance_groups_t::base32)) {
228,062✔
UNCOV
237
            return reject_capability((p->access.width == 8) ? "requires conformance group base64"
×
UNCOV
238
                                                            : "requires conformance group base32");
×
239
        }
240
        if (p->is_signed && !supports(platform, bpf_conformance_groups_t::base64)) {
228,062✔
241
            return reject_capability("requires conformance group base64");
×
242
        }
243
    }
244
    if (std::holds_alternative<Packet>(ins) && !supports(platform, bpf_conformance_groups_t::packet)) {
847,605✔
245
        return reject_capability("requires conformance group packet");
76✔
246
    }
247
    if (const auto p = std::get_if<Atomic>(&ins)) {
847,567✔
248
        const auto group =
1,076✔
249
            (p->access.width == 8) ? bpf_conformance_groups_t::atomic64 : bpf_conformance_groups_t::atomic32;
807✔
250
        if (!supports(platform, group)) {
807✔
UNCOV
251
            return reject_capability((group == bpf_conformance_groups_t::atomic64)
×
252
                                         ? "requires conformance group atomic64"
UNCOV
253
                                         : "requires conformance group atomic32");
×
254
        }
255
    }
256
    return {};
847,567✔
257
}
258

259
// Validate instruction-level feature support before CFG construction.
260
// This is the user-facing rejection point for unsupported or unavailable features.
261
static void validate_instruction_feature_support(const InstructionSeq& insts, const ProgramInfo& info,
3,080✔
262
                                                 ResolvedKfuncCalls* resolved_kfunc_calls) {
263
    const auto& platform = *info.platform;
3,080✔
264
    for (const auto& [label, inst, _] : insts) {
569,404✔
265
        if (const auto reason = check_instruction_feature_support(inst, platform)) {
566,374✔
266
            if (reason->kind == RejectKind::NotImplemented) {
42✔
UNCOV
267
                throw InvalidControlFlow{"not implemented: " + reason->detail + " (at " + to_string(label) + ")"};
×
268
            }
269
            throw InvalidControlFlow{"rejected: " + reason->detail + " (at " + to_string(label) + ")"};
84✔
270
        }
283,208✔
271
        if (const auto* call_btf = std::get_if<CallBtf>(&inst)) {
566,346✔
272
            std::string why_not;
36✔
273
            const auto call = resolve_kfunc_call(*call_btf, info, &why_not);
36✔
274
            if (!call) {
36✔
275
                throw InvalidControlFlow{"not implemented: " + why_not + " (at " + to_string(label) + ")"};
16✔
276
            }
277
            resolved_kfunc_calls->insert_or_assign(label, *call);
28✔
278
        }
40✔
279
    }
280
}
3,030✔
281

282
/// Update a control-flow graph to inline function macros.
283
static void add_cfg_nodes(CfgBuilder& builder, const Label& caller_label, const Label& entry_label) {
144✔
284
    bool first = true;
144✔
285

286
    // Get the label of the node to go to on returning from the macro.
287
    Label exit_to_label = builder.prog.cfg().get_child(caller_label);
144✔
288

289
    // Construct the variable prefix to use for the new stack frame
290
    // and store a copy in the CallLocal instruction since the instruction-specific
291
    // labels may only exist until the CFG is simplified.
292
    const std::string stack_frame_prefix = to_string(caller_label);
144✔
293
    if (const auto pcall = std::get_if<CallLocal>(&builder.prog.instruction_at(caller_label))) {
216✔
294
        pcall->stack_frame_prefix = stack_frame_prefix;
144✔
295
    }
296

297
    // Walk the transitive closure of CFG nodes starting at entry_label and ending at
298
    // any exit instruction.
299
    std::set macro_labels{entry_label};
377✔
300
    std::set seen_labels{entry_label};
377✔
301
    while (!macro_labels.empty()) {
684✔
302
        Label macro_label = *macro_labels.begin();
544✔
303
        macro_labels.erase(macro_label);
544✔
304

305
        if (stack_frame_prefix == macro_label.stack_frame_prefix) {
544✔
306
            throw InvalidControlFlow{stack_frame_prefix + ": illegal recursion"};
6✔
307
        }
308

309
        // Clone the macro block into a new block with the new stack frame prefix.
310
        const Label label{macro_label.from, macro_label.to, stack_frame_prefix};
812✔
311
        auto inst = builder.prog.instruction_at(macro_label);
540✔
312
        if (const auto pexit = std::get_if<Exit>(&inst)) {
540✔
313
            pexit->stack_frame_prefix = label.stack_frame_prefix;
146✔
314
        } else if (const auto pcall = std::get_if<Call>(&inst)) {
470✔
315
            pcall->stack_frame_prefix = label.stack_frame_prefix;
6✔
316
        }
317
        builder.insert(label, inst);
540✔
318

319
        if (first) {
540✔
320
            // Add an edge from the caller to the new block.
321
            first = false;
144✔
322
            builder.add_child(caller_label, label);
144✔
323
        }
324

325
        // Add an edge from any other predecessors.
326
        for (const auto& prev_macro_nodes = builder.prog.cfg().parents_of(macro_label);
810✔
327
             const auto& prev_macro_label : prev_macro_nodes) {
1,214✔
328
            const Label prev_label(prev_macro_label.from, prev_macro_label.to, to_string(caller_label));
404✔
329
            if (const auto& labels = builder.prog.cfg().labels();
404✔
330
                std::ranges::find(labels, prev_label) != labels.end()) {
404✔
331
                builder.add_child(prev_label, label);
398✔
332
            }
333
        }
404✔
334

335
        // Walk all successor nodes.
336
        for (const auto& next_macro_nodes = builder.prog.cfg().children_of(macro_label);
1,083✔
337
             const auto& next_macro_label : next_macro_nodes) {
1,356✔
338
            if (next_macro_label == builder.prog.cfg().exit_label()) {
819✔
339
                // This is an exit transition, so add edge to the block to execute
340
                // upon returning from the macro.
341
                builder.add_child(label, exit_to_label);
144✔
342
            } else if (!seen_labels.contains(next_macro_label)) {
402✔
343
                // Push any other unprocessed successor label onto the list to be processed.
344
                if (!macro_labels.contains(next_macro_label)) {
400✔
345
                    macro_labels.insert(next_macro_label);
400✔
346
                }
347
                seen_labels.insert(next_macro_label);
400✔
348
            }
349
        }
350
    }
1,084✔
351

352
    // Remove the original edge from the caller node to its successor,
353
    // since processing now goes through the function macro instead.
354
    builder.remove_child(caller_label, exit_to_label);
140✔
355

356
    // Finally, recurse to replace any nested function macros.
357
    string caller_label_str = to_string(caller_label);
140✔
358
    const long stack_frame_depth = std::ranges::count(caller_label_str, STACK_FRAME_DELIMITER) + 2;
140✔
359
    for (const auto& macro_label : seen_labels) {
608✔
360
        const Label label{macro_label.from, macro_label.to, caller_label_str};
762✔
361
        if (const auto pins = std::get_if<CallLocal>(&builder.prog.instruction_at(label))) {
732✔
362
            if (stack_frame_depth >= MAX_CALL_STACK_FRAMES) {
82✔
363
                throw InvalidControlFlow{"too many call stack frames"};
10✔
364
            }
365
            add_cfg_nodes(builder, label, pins->target);
78✔
366
        }
367
    }
498✔
368
}
602✔
369

370
struct Imm64Parts {
371
    int32_t lo{};
372
    int32_t hi{};
373
};
374

375
static uint64_t merge_imm32_to_u64(const Imm64Parts parts) {
2✔
376
    return static_cast<uint64_t>(static_cast<uint32_t>(parts.lo)) |
2✔
377
           (static_cast<uint64_t>(static_cast<uint32_t>(parts.hi)) << 32);
2✔
378
}
379

380
/// Resolve a LoadPseudo to a concrete instruction before abstract interpretation.
381
/// VARIABLE_ADDR and CODE_ADDR are lowered to immediate scalar loads.
382
static Instruction resolve_pseudo_load(const LoadPseudo& pseudo) {
8✔
383
    if (pseudo.addr.kind == PseudoAddress::Kind::VARIABLE_ADDR || pseudo.addr.kind == PseudoAddress::Kind::CODE_ADDR) {
8✔
384
        return Bin{
5✔
385
            .op = Bin::Op::MOV,
386
            .dst = pseudo.dst,
387
            .v = Imm{merge_imm32_to_u64({.lo = pseudo.addr.imm, .hi = pseudo.addr.next_imm})},
3✔
388
            .is64 = true,
389
            .lddw = true,
390
        };
2✔
391
    }
392

393
    const auto& descriptors = thread_local_program_info->map_descriptors;
6✔
394
    if (pseudo.addr.imm < 0 || static_cast<size_t>(pseudo.addr.imm) >= descriptors.size()) {
6✔
395
        throw InvalidControlFlow{"invalid map index " + std::to_string(pseudo.addr.imm) + " (have " +
5✔
396
                                 std::to_string(descriptors.size()) + " maps)"};
6✔
397
    }
398
    const auto map_idx = static_cast<size_t>(pseudo.addr.imm);
4✔
399
    const int mapfd = descriptors.at(map_idx).original_fd;
4✔
400
    switch (pseudo.addr.kind) {
4✔
401
    case PseudoAddress::Kind::MAP_BY_IDX: return LoadMapFd{.dst = pseudo.dst, .mapfd = mapfd};
2✔
402
    case PseudoAddress::Kind::MAP_VALUE_BY_IDX:
2✔
403
        return LoadMapAddress{.dst = pseudo.dst, .mapfd = mapfd, .offset = pseudo.addr.next_imm};
2✔
UNCOV
404
    default: CRAB_ERROR("Invalid address kind: ", static_cast<int>(pseudo.addr.kind));
×
405
    }
406
}
407

408
/// Convert an instruction sequence to a control-flow graph (CFG).
409
static CfgBuilder instruction_seq_to_cfg(const InstructionSeq& insts, const bool must_have_exit,
3,030✔
410
                                         const ResolvedKfuncCalls& resolved_kfunc_calls) {
411
    CfgBuilder builder;
3,030✔
412
    assert(thread_local_program_info->platform != nullptr && "platform must be set before CFG construction");
3,030✔
413

414
    // First, add all instructions to the CFG without connecting
415
    for (const auto& [label, inst, _] : insts) {
565,498✔
416
        assert(!check_instruction_feature_support(inst, *thread_local_program_info->platform).has_value() &&
562,470✔
417
               "instruction support must be validated before CFG construction");
418
        if (std::holds_alternative<Undefined>(inst)) {
562,470✔
UNCOV
419
            continue;
×
420
        }
421
        if (std::holds_alternative<CallBtf>(inst)) {
562,470✔
422
            const auto it = resolved_kfunc_calls.find(label);
28✔
423
            if (it == resolved_kfunc_calls.end()) {
28✔
UNCOV
424
                throw InvalidControlFlow{"internal error: missing validated kfunc resolution (at " + to_string(label) +
×
UNCOV
425
                                         ")"};
×
426
            }
427
            builder.insert(label, it->second);
42✔
428
        } else if (const auto* pseudo = std::get_if<LoadPseudo>(&inst)) {
562,442✔
429
            if (pseudo->addr.kind == PseudoAddress::Kind::CODE_ADDR) {
22✔
430
                // Keep CODE_ADDR as LoadPseudo so abstract transformation can type it as T_FUNC.
431
                builder.insert(label, inst);
14✔
432
            } else {
433
                builder.insert(label, resolve_pseudo_load(*pseudo));
11✔
434
            }
435
        } else {
436
            builder.insert(label, inst);
562,420✔
437
        }
438
    }
439

440
    if (insts.empty()) {
3,028✔
UNCOV
441
        throw InvalidControlFlow{"empty instruction sequence"};
×
442
    } else {
443
        const auto& [label, inst, _0] = insts[0];
3,028✔
444
        builder.add_child(builder.prog.cfg().entry_label(), label);
3,033✔
445
    }
446

447
    // Do a first pass ignoring all function macro calls.
448
    for (size_t i = 0; i < insts.size(); i++) {
565,496✔
449
        const auto& [label, inst, _0] = insts[i];
562,468✔
450

451
        if (std::holds_alternative<Undefined>(inst)) {
562,468✔
452
            continue;
18✔
453
        }
454
        Label fallthrough{builder.prog.cfg().exit_label()};
562,468✔
455
        if (i + 1 < insts.size()) {
562,468✔
456
            fallthrough = std::get<0>(insts[i + 1]);
559,440✔
457
        } else {
458
            if (has_fall(inst) && must_have_exit) {
3,793✔
UNCOV
459
                throw InvalidControlFlow{"fallthrough in last instruction"};
×
460
            }
461
        }
462
        if (const auto jmp = std::get_if<Jmp>(&inst)) {
562,468✔
463
            if (const auto cond = jmp->cond) {
59,276✔
464
                Label target_label = jmp->target;
47,950✔
465
                if (target_label == fallthrough) {
47,950✔
466
                    builder.add_child(label, fallthrough);
18✔
467
                    continue;
18✔
468
                }
469
                if (!builder.prog.cfg().contains(target_label)) {
47,932✔
UNCOV
470
                    throw InvalidControlFlow{"jump to undefined label " + to_string(target_label)};
×
471
                }
472
                builder.insert_jump(label, target_label, Assume{.cond = *cond, .is_implicit = true});
71,898✔
473
                builder.insert_jump(label, fallthrough, Assume{.cond = reverse(*cond), .is_implicit = true});
95,864✔
474
            } else {
47,950✔
475
                builder.add_child(label, jmp->target);
11,326✔
476
            }
477
        } else {
478
            if (has_fall(inst)) {
503,192✔
479
                builder.add_child(label, fallthrough);
501,126✔
480
            }
481
        }
482
        if (std::holds_alternative<Exit>(inst)) {
562,450✔
483
            builder.add_child(label, builder.prog.cfg().exit_label());
3,099✔
484
        }
485
    }
562,468✔
486

487
    // Now replace macros. We have to do this as a second pass so that
488
    // we only add new nodes that are actually reachable, based on the
489
    // results of the first pass.
490
    for (const auto& [label, inst, _] : insts) {
565,440✔
491
        if (const auto pins = std::get_if<CallLocal>(&inst)) {
562,449✔
492
            add_cfg_nodes(builder, label, pins->target);
66✔
493
        }
494
    }
495

496
    return builder;
3,020✔
497
}
10✔
498

499
static bool is_tail_call_helper(const Call& call, const ebpf_platform_t& platform) {
29,328✔
500
    if (call.kind != CallKind::helper) {
29,328✔
501
        return false;
14✔
502
    }
503
    if (!platform.is_helper_usable(call.func)) {
29,300✔
504
        return false;
1,224✔
505
    }
506
    return platform.get_helper_prototype(call.func).return_type == EBPF_RETURN_TYPE_INTEGER_OR_NO_RETURN_IF_SUCCEED;
26,852✔
507
}
508

509
static bool is_tail_call_site(const Instruction& ins, const ebpf_platform_t& platform) {
664,412✔
510
    if (const auto* call = std::get_if<Call>(&ins)) {
664,412✔
511
        return is_tail_call_helper(*call, platform);
29,328✔
512
    }
513
    if (std::holds_alternative<Callx>(ins)) {
635,084✔
514
        // At CFG-construction time, callx target ids are not available.
515
        // Conservatively treat callx as a potential tail-call site.
516
        return true;
48✔
517
    }
518
    return false;
317,518✔
519
}
520

521
static void collect_wto_labels(const CycleOrLabel& component, std::set<Label>& labels) {
664,462✔
522
    if (const auto plabel = std::get_if<Label>(&component)) {
664,462✔
523
        labels.insert(*plabel);
664,412✔
524
        return;
664,412✔
525
    }
526
    for (const auto& nested_component : *std::get<std::shared_ptr<WtoCycle>>(component)) {
292✔
527
        collect_wto_labels(nested_component, labels);
242✔
528
    }
529
}
530

531
/// Enforce a global upper bound on tail-call chain length.
532
/// Count tail-call sites over the reachable maximal-SCC DAG so cycles do not inflate depth.
533
/// Maximal SCCs are obtained from WTO nesting: all labels in the same outermost WTO cycle
534
/// are mutually reachable and therefore belong to the same maximal SCC.
535
static void validate_tail_call_chain_depth(const Program& prog, const Wto& wto, const ebpf_platform_t& platform) {
3,020✔
536
    constexpr int tail_call_chain_limit = 33;
3,020✔
537

538
    // WTO only covers labels reachable from entry.
539
    std::set<Label> reachable;
3,020✔
540
    for (const auto& component : wto) {
667,240✔
541
        collect_wto_labels(component, reachable);
664,220✔
542
    }
543

544
    // Partition reachable labels by maximal SCC representative:
545
    // the outermost containing WTO head, or the label itself if not in a cycle.
546
    std::map<Label, Label> maximal_scc_of;
3,020✔
547
    std::set<Label> maximal_scc_ids;
3,020✔
548
    for (const auto& label : reachable) {
667,432✔
549
        const Label scc_id = wto.nesting(label).outermost_head().value_or(label);
1,328,825✔
550
        maximal_scc_of.emplace(label, scc_id);
664,412✔
551
        maximal_scc_ids.insert(scc_id);
664,412✔
552
    }
664,412✔
553

554
    std::map<Label, int> tail_sites_per_scc;
3,020✔
555
    std::map<Label, std::optional<Label>> representative_tail_label;
3,020✔
556
    std::map<Label, std::set<Label>> dag_successors;
3,020✔
557
    std::map<Label, int> indegree;
3,020✔
558
    for (const auto& scc_id : maximal_scc_ids) {
667,240✔
559
        tail_sites_per_scc.emplace(scc_id, 0);
664,220✔
560
        representative_tail_label.emplace(scc_id, std::nullopt);
664,220✔
561
        dag_successors.emplace(scc_id, std::set<Label>{});
664,220✔
562
        indegree.emplace(scc_id, 0);
664,221✔
563
    }
564

565
    for (const auto& label : reachable) {
667,432✔
566
        const Label src_scc = maximal_scc_of.at(label);
664,412✔
567
        if (is_tail_call_site(prog.instruction_at(label), platform)) {
664,412✔
568
            ++tail_sites_per_scc.at(src_scc);
850✔
569
            auto& representative = representative_tail_label.at(src_scc);
850✔
570
            if (!representative.has_value()) {
850✔
571
                representative = label;
850✔
572
            }
573
        }
574
        for (const auto& child : prog.cfg().children_of(label)) {
1,373,738✔
575
            if (!reachable.contains(child)) {
709,326✔
576
                continue;
×
577
            }
578
            const Label dst_scc = maximal_scc_of.at(child);
709,326✔
579
            if (src_scc != dst_scc && dag_successors[src_scc].insert(dst_scc).second) {
709,326✔
580
                ++indegree.at(dst_scc);
709,082✔
581
            }
582
        }
709,326✔
583
    }
664,412✔
584

585
    std::map<Label, int> indegree_for_sources = indegree;
3,020✔
586
    std::vector<Label> topo_order;
3,020✔
587
    topo_order.reserve(maximal_scc_ids.size());
3,020✔
588
    for (const auto& scc_id : maximal_scc_ids) {
667,240✔
589
        if (indegree.at(scc_id) == 0) {
664,220✔
590
            topo_order.push_back(scc_id);
3,020✔
591
        }
592
    }
593
    for (size_t index = 0; index < topo_order.size(); ++index) {
667,240✔
594
        const Label scc_id = topo_order[index];
664,220✔
595
        for (const auto& succ : dag_successors.at(scc_id)) {
1,373,302✔
596
            --indegree.at(succ);
709,082✔
597
            if (indegree.at(succ) == 0) {
709,082✔
598
                topo_order.push_back(succ);
661,200✔
599
            }
600
        }
601
    }
664,220✔
602
    if (topo_order.size() != maximal_scc_ids.size()) {
3,020✔
UNCOV
603
        CRAB_ERROR("WTO-derived SCC graph must be acyclic");
×
604
    }
605

606
    // Longest path over maximal SCC DAG by tail-call site count.
607
    constexpr int uninitialized_depth = std::numeric_limits<int>::min();
3,020✔
608
    std::map<Label, int> max_tail_depth;
3,020✔
609
    std::map<Label, std::optional<Label>> depth_label;
3,020✔
610
    for (const auto& scc_id : maximal_scc_ids) {
667,240✔
611
        max_tail_depth.emplace(scc_id, uninitialized_depth);
664,220✔
612
        depth_label.emplace(scc_id, std::nullopt);
664,220✔
613
        if (indegree_for_sources.at(scc_id) == 0) {
664,220✔
614
            max_tail_depth.at(scc_id) = tail_sites_per_scc.at(scc_id);
3,020✔
615
            depth_label.at(scc_id) = representative_tail_label.at(scc_id);
335,130✔
616
        }
617
    }
618

619
    for (const auto& scc_id : topo_order) {
667,234✔
620
        const int current_depth = max_tail_depth.at(scc_id);
664,216✔
621
        if (current_depth == uninitialized_depth) {
664,216✔
UNCOV
622
            continue;
×
623
        }
624
        if (current_depth > tail_call_chain_limit) {
664,216✔
625
            const Label at = depth_label.at(scc_id).value_or(scc_id);
2✔
626
            throw InvalidControlFlow{"tail call chain depth exceeds " + std::to_string(tail_call_chain_limit) +
4✔
627
                                     " (at " + to_string(at) + ")"};
7✔
628
        }
2✔
629
        for (const auto& succ : dag_successors.at(scc_id)) {
1,373,292✔
630
            const int candidate_depth = current_depth + tail_sites_per_scc.at(succ);
709,078✔
631
            if (candidate_depth > max_tail_depth.at(succ)) {
709,078✔
632
                max_tail_depth.at(succ) = candidate_depth;
661,570✔
633
                depth_label.at(succ) = representative_tail_label.at(succ).has_value()
1,323,140✔
634
                                           ? representative_tail_label.at(succ)
852✔
635
                                           : depth_label.at(scc_id);
1,346,894✔
636
            }
637
        }
638
    }
639
}
3,038✔
640

641
Program Program::from_sequence(const InstructionSeq& inst_seq, const ProgramInfo& info,
3,080✔
642
                               const ebpf_verifier_options_t& options) {
643
    thread_local_program_info.set(info);
4,620✔
644
    thread_local_options = options;
3,080✔
645
    assert(info.platform != nullptr && "platform must be set before instruction feature validation");
3,080✔
646
    ResolvedKfuncCalls resolved_kfunc_calls;
3,080✔
647
    validate_instruction_feature_support(inst_seq, info, &resolved_kfunc_calls);
3,080✔
648

649
    // Convert the instruction sequence to a deterministic control-flow graph.
650
    CfgBuilder builder = instruction_seq_to_cfg(inst_seq, options.cfg_opts.must_have_exit, resolved_kfunc_calls);
3,030✔
651

652
    const Wto wto{builder.prog.cfg()};
3,020✔
653
    validate_tail_call_chain_depth(builder.prog, wto, *info.platform);
3,020✔
654

655
    // Record valid callback targets for PTR_TO_FUNC: top-level concrete instruction labels
656
    // (no stack-frame prefix, not synthetic jump labels, and not Exit instructions).
657
    thread_local_program_info->callback_target_labels.clear();
3,018✔
658
    thread_local_program_info->callback_targets_with_exit.clear();
3,018✔
659
    for (const Label& label : builder.prog.labels()) {
667,728✔
660
        if (label == Label::entry || label == Label::exit || label.isjump() || !label.stack_frame_prefix.empty()) {
664,710✔
661
            continue;
102,368✔
662
        }
663
        if (std::holds_alternative<Exit>(builder.prog.instruction_at(label))) {
562,342✔
664
            continue;
2,036✔
665
        }
666
        thread_local_program_info->callback_target_labels.insert(label.from);
560,306✔
667
    }
668

669
    // Basic callback body check: callback target must be able to reach a top-level Exit.
670
    const auto has_reachable_top_level_exit = [&](const Label& start) {
561,815✔
671
        std::set<Label> seen;
560,306✔
672
        std::vector<Label> worklist{start};
1,400,765✔
673
        while (!worklist.empty()) {
145,887,332✔
674
            Label label = worklist.back();
145,887,326✔
675
            worklist.pop_back();
145,887,326✔
676
            if (seen.contains(label)) {
145,887,326✔
677
                continue;
54✔
678
            }
679
            seen.insert(label);
145,887,272✔
680
            if (label == Label::exit) {
145,887,272✔
681
                return true;
770✔
682
            }
683
            if (label != Label::entry && builder.prog.cfg().contains(label) &&
364,714,330✔
684
                std::holds_alternative<Exit>(builder.prog.instruction_at(label)) && label.stack_frame_prefix.empty()) {
364,714,330✔
685
                return true;
279,380✔
686
            }
687
            for (const Label& child : builder.prog.cfg().children_of(label)) {
299,123,804✔
688
                worklist.push_back(child);
153,796,832✔
689
            }
690
        }
145,887,326✔
691
        return false;
3✔
692
    };
1,120,612✔
693
    for (const int32_t label_num : thread_local_program_info->callback_target_labels) {
563,324✔
694
        const Label label{gsl::narrow<int>(label_num)};
560,306✔
695
        if (has_reachable_top_level_exit(label)) {
560,306✔
696
            thread_local_program_info->callback_targets_with_exit.insert(label_num);
560,300✔
697
        }
698
    }
560,306✔
699

700
    // Detect loops using Weak Topological Ordering (WTO) and insert counters at loop entry points. WTO provides a
701
    // hierarchical decomposition of the CFG that identifies all strongly connected components (cycles) and their entry
702
    // points. These entry points serve as natural locations for loop counters that help verify program termination.
703
    if (options.cfg_opts.check_for_termination) {
3,018✔
704
        wto.for_each_loop_head([&](const Label& label) -> void {
42✔
705
            builder.insert_after(label, Label::make_increment_counter(label), IncrementLoopCounter{label});
80✔
706
        });
40✔
707
    }
708

709
    // Annotate the CFG by explicitly adding in assertions before every memory instruction.
710
    for (const auto& label : builder.prog.labels()) {
667,768✔
711
        builder.set_assertions(label, get_assertions(builder.prog.instruction_at(label), info, label));
997,125✔
712
    }
713
    return std::move(builder.prog);
4,527✔
714
}
3,053✔
715

716
std::set<BasicBlock> BasicBlock::collect_basic_blocks(const Cfg& cfg, const bool simplify) {
2✔
717
    if (!simplify) {
2✔
718
        std::set<BasicBlock> res;
2✔
719
        for (const Label& label : cfg.labels()) {
32✔
720
            if (label != cfg.entry_label() && label != cfg.exit_label()) {
73✔
721
                res.insert(BasicBlock{label});
39✔
722
            }
723
        }
724
        return res;
2✔
725
    }
2✔
726

UNCOV
727
    std::set<BasicBlock> res;
×
UNCOV
728
    std::set<Label> worklist;
×
UNCOV
729
    for (const Label& label : cfg.labels()) {
×
UNCOV
730
        worklist.insert(label);
×
731
    }
UNCOV
732
    std::set<Label> seen;
×
UNCOV
733
    while (!worklist.empty()) {
×
UNCOV
734
        Label label = *worklist.begin();
×
UNCOV
735
        worklist.erase(label);
×
UNCOV
736
        if (seen.contains(label)) {
×
UNCOV
737
            continue;
×
738
        }
UNCOV
739
        seen.insert(label);
×
740

UNCOV
741
        if (cfg.in_degree(label) == 1 && cfg.num_siblings(label) == 1) {
×
UNCOV
742
            continue;
×
743
        }
UNCOV
744
        BasicBlock bb{label};
×
UNCOV
745
        while (cfg.out_degree(label) == 1) {
×
UNCOV
746
            const Label& next_label = cfg.get_child(bb.last_label());
×
747

UNCOV
748
            if (seen.contains(next_label) || next_label == cfg.exit_label() || cfg.in_degree(next_label) != 1) {
×
749
                break;
750
            }
751

UNCOV
752
            if (bb.first_label() == cfg.entry_label()) {
×
753
                // Entry instruction is Undefined. We want to start with 0
UNCOV
754
                bb.m_ts.clear();
×
755
            }
UNCOV
756
            bb.m_ts.push_back(next_label);
×
757

UNCOV
758
            worklist.erase(next_label);
×
UNCOV
759
            seen.insert(next_label);
×
760

UNCOV
761
            label = next_label;
×
UNCOV
762
        }
×
UNCOV
763
        res.emplace(std::move(bb));
×
UNCOV
764
    }
×
UNCOV
765
    return res;
×
UNCOV
766
}
×
767

768
/// Get the type of given Instruction.
769
/// Most of these type names are also statistics header labels.
UNCOV
770
static std::string instype(Instruction ins) {
×
UNCOV
771
    if (const auto pcall = std::get_if<Call>(&ins)) {
×
UNCOV
772
        if (pcall->is_map_lookup) {
×
UNCOV
773
            return "call_1";
×
774
        }
UNCOV
775
        if (pcall->pairs.empty()) {
×
UNCOV
776
            if (std::ranges::all_of(pcall->singles,
×
777
                                    [](const ArgSingle kr) { return kr.kind == ArgSingle::Kind::ANYTHING; })) {
UNCOV
778
                return "call_nomem";
×
779
            }
780
        }
UNCOV
781
        return "call_mem";
×
782
    } else if (std::holds_alternative<Callx>(ins)) {
UNCOV
783
        return "callx";
×
784
    } else if (std::holds_alternative<CallBtf>(ins)) {
UNCOV
785
        return "call_btf";
×
UNCOV
786
    } else if (const auto pimm = std::get_if<Mem>(&ins)) {
×
UNCOV
787
        return pimm->is_load ? "load" : "store";
×
788
    } else if (std::holds_alternative<Atomic>(ins)) {
UNCOV
789
        return "load_store";
×
790
    } else if (std::holds_alternative<Packet>(ins)) {
UNCOV
791
        return "packet_access";
×
UNCOV
792
    } else if (const auto pins = std::get_if<Bin>(&ins)) {
×
UNCOV
793
        switch (pins->op) {
×
UNCOV
794
        case Bin::Op::MOV:
×
795
        case Bin::Op::MOVSX8:
796
        case Bin::Op::MOVSX16:
UNCOV
797
        case Bin::Op::MOVSX32: return "assign";
×
UNCOV
798
        default: return "arith";
×
799
        }
800
    } else if (std::holds_alternative<Un>(ins)) {
UNCOV
801
        return "arith";
×
802
    } else if (std::holds_alternative<LoadMapFd>(ins)) {
UNCOV
803
        return "assign";
×
804
    } else if (std::holds_alternative<LoadMapAddress>(ins)) {
UNCOV
805
        return "assign";
×
806
    } else if (std::holds_alternative<LoadPseudo>(ins)) {
UNCOV
807
        return "assign";
×
808
    } else if (std::holds_alternative<Assume>(ins)) {
UNCOV
809
        return "assume";
×
810
    } else {
UNCOV
811
        return "other";
×
812
    }
813
}
814

UNCOV
815
std::vector<std::string> stats_headers() {
×
816
    return {
817
        "instructions", "joins",      "other",      "jumps",         "assign",  "arith",
818
        "load",         "store",      "load_store", "packet_access", "call_1",  "call_mem",
819
        "call_nomem",   "reallocate", "map_in_map", "arith64",       "arith32",
UNCOV
820
    };
×
821
}
822

UNCOV
823
std::map<std::string, int> collect_stats(const Program& prog) {
×
UNCOV
824
    std::map<std::string, int> res;
×
UNCOV
825
    for (const auto& h : stats_headers()) {
×
UNCOV
826
        res[h] = 0;
×
UNCOV
827
    }
×
UNCOV
828
    for (const auto& label : prog.labels()) {
×
UNCOV
829
        res["instructions"]++;
×
UNCOV
830
        const auto& cmd = prog.instruction_at(label);
×
UNCOV
831
        if (const auto pins = std::get_if<LoadMapFd>(&cmd)) {
×
UNCOV
832
            if (pins->mapfd == -1) {
×
UNCOV
833
                res["map_in_map"] = 1;
×
834
            }
835
        }
UNCOV
836
        if (const auto pins = std::get_if<Call>(&cmd)) {
×
UNCOV
837
            if (pins->reallocate_packet) {
×
UNCOV
838
                res["reallocate"] = 1;
×
839
            }
840
        }
UNCOV
841
        if (const auto pins = std::get_if<Bin>(&cmd)) {
×
UNCOV
842
            res[pins->is64 ? "arith64" : "arith32"]++;
×
843
        }
UNCOV
844
        res[instype(cmd)]++;
×
UNCOV
845
        if (prog.cfg().in_degree(label) > 1) {
×
UNCOV
846
            res["joins"]++;
×
847
        }
UNCOV
848
        if (prog.cfg().out_degree(label) > 1) {
×
UNCOV
849
            res["jumps"]++;
×
850
        }
851
    }
UNCOV
852
    return res;
×
UNCOV
853
}
×
854

855
Cfg cfg_from_adjacency_list(const std::map<Label, std::vector<Label>>& AdjList) {
8✔
856
    CfgBuilder builder;
8✔
857
    for (const auto& label : std::views::keys(AdjList)) {
66✔
858
        if (label == Label::entry || label == Label::exit) {
58✔
859
            continue;
8✔
860
        }
861
        builder.insert(label, Undefined{});
75✔
862
    }
863
    for (const auto& [label, children] : AdjList) {
66✔
864
        for (const auto& child : children) {
136✔
865
            builder.add_child(label, child);
78✔
866
        }
867
    }
868
    return builder.prog.cfg();
16✔
869
}
8✔
870
} // 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