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

Alan-Jowett / ebpf-verifier / 29279923487

12 Jul 2026 11:38AM UTC coverage: 86.649% (+0.3%) from 86.386%
29279923487

push

github

web-flow
Drive sample verification tests from the inventory; parallelize CI (#1195)

Full-program verification of the ebpf-samples corpus was 17 C++ files
generated by scripts/generate_verify_project_tests.py from
test-data/elf_inventory.json, then compiled into the test binary. The
generated files duplicated the inventory and could drift from it (there
was no check keeping them in sync), and the whole corpus ran in one
single-threaded Catch2 process.

Data-driven tests (dedup)
-------------------------
- Add src/test/test_verify_samples.cpp, which reads elf_inventory.json at
  runtime (via yaml-cpp; JSON is a subset of YAML) and registers one
  Catch2 DYNAMIC_SECTION per (object, section[, program]). Adding or
  retagging a sample is now a JSON edit: no code generation, no recompile.
- Delete the 17 generated test_verify_<project>.cpp files and
  scripts/generate_verify_project_tests.py. The inventory is the single
  source of truth. A coverage-guard test asserts the registered projects
  match the inventory exactly, so a new project cannot be left untested.
- Trim test_verify.hpp to the ELF-parse cache and the VerifyIssueKind
  taxonomy the driver and the multithreading test still use.

  Catch2's [!shouldfail] is a compile-time per-TEST_CASE tag and cannot be
  applied per data-driven entry, so an expected_failure (a safe program
  the verifier is currently too imprecise to accept) is asserted as
  *still rejected* -- an xfail/golden marker. A verifier improvement that
  starts accepting it fails the case, prompting an inventory update; a
  regression on a passing program fails it too.

Parallel CI (ctest)
-------------------
- Register ctest entries: one per project (sharded by tag) plus a "unit"
  entry for everything else. The shard list is derived from the inventory
  via string(JSON), so it cannot drift from the corpus.
- CI runs `ctest -j` instead of a single `bin/tests` process. Locally, a
  full run drops from ~104s to ~42s on 16 cores (bounded by t... (continued)

9313 of 10748 relevant lines covered (86.65%)

6391896.2 hits per line

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

89.29
/src/crab/ebpf_checker.cpp
1
// Copyright (c) Prevail Verifier contributors.
2
// SPDX-License-Identifier: MIT
3

4
// This file is eBPF-specific, not derived from CRAB.
5

6
#include <array>
7
#include <bitset>
8
#include <optional>
9
#include <utility>
10
#include <variant>
11

12
#include "arith/dsl_syntax.hpp"
13
#include "config.hpp"
14
#include "crab/array_domain.hpp"
15
#include "crab/ebpf_domain.hpp"
16
#include "crab/region_semantics.hpp"
17
#include "crab/var_registry.hpp"
18
#include "ir/program.hpp"
19
#include "ir/syntax.hpp"
20
#include "platform.hpp"
21

22
namespace prevail {
23

24
namespace {
25
// Internal control-flow signal used by EbpfChecker to abort the current
26
// assertion check. Caught only inside ebpf_domain_check, where it is
27
// converted into a VerificationError value. Not part of the public API.
28
struct VerificationFailureSignal final : std::runtime_error {
29
    using std::runtime_error::runtime_error;
1,848✔
30
};
31

32
bool is_power_of_two(const int size) { return size > 0 && (size & (size - 1)) == 0; }
196✔
33

34
bool field_is_present(const EbpfStructFieldDescriptor& field) { return field.offset >= 0 && field.span > 0; }
10,371✔
35

36
bool field_allows_access(const EbpfStructFieldDescriptor& field, const int offset, const int size,
224✔
37
                         const AccessType access_type) {
38
    if (!field_is_present(field)) {
224✔
39
        return false;
40
    }
41
    if (access_type == AccessType::write && field.permission == EbpfStructFieldPermission::read_only) {
224✔
42
        return false;
43
    }
44
    if (field.allow_narrow_access) {
224✔
45
        if (size <= field.max_access_width && is_power_of_two(size) && offset >= field.offset &&
354✔
46
            offset + size <= field.offset + field.span) {
138✔
47
            return true;
10✔
48
        }
49
    } else if (offset == field.offset && size == field.max_access_width) {
28✔
50
        return true;
51
    }
52
    return access_type == AccessType::read && field.extra_read_width_at_start > 0 &&
204✔
53
           size == field.extra_read_width_at_start && is_power_of_two(size) && offset == field.offset;
315✔
54
}
55

56
bool is_valid_struct_access(const EbpfStructDescriptor& descriptor, const int offset, const int size,
28✔
57
                            const AccessType access_type) {
58
    if (!descriptor.fields || size <= 0 || offset < 0 || offset >= descriptor.size || offset + size > descriptor.size ||
28✔
59
        offset % size != 0) {
28✔
60
        return false;
61
    }
62
    for (size_t i = 0; i < descriptor.field_count; ++i) {
230✔
63
        if (field_allows_access(descriptor.fields[i], offset, size, access_type)) {
224✔
64
            return true;
11✔
65
        }
66
    }
67
    return false;
3✔
68
}
69

70
template <size_t N>
71
bool write_may_touch_readonly_field(const NumAbsDomain& values, const LinearExpression& lb, const LinearExpression& ub,
3,390✔
72
                                    const std::array<EbpfStructFieldDescriptor, N>& fields) {
73
    using namespace dsl_syntax;
74
    for (const EbpfStructFieldDescriptor& field : fields) {
13,532✔
75
        if (!field_is_present(field) || field.permission != EbpfStructFieldPermission::read_only) {
10,147✔
76
            continue;
10✔
77
        }
78
        if (values.intersect(ub > LinearExpression{field.offset}) &&
25,399✔
79
            values.intersect(lb < LinearExpression{field.offset + field.span})) {
10,230✔
80
            return true;
5✔
81
        }
82
    }
83
    return false;
1,690✔
84
}
85
} // namespace
86

87
class EbpfChecker final {
935✔
88
  public:
89
    explicit EbpfChecker(const EbpfDomain& dom, Assertion assertion, const AnalysisContext& context)
761,106✔
90
        : assertion{std::move(assertion)}, dom(dom), context(context) {}
1,141,659✔
91

92
    void visit() { std::visit(*this, assertion); }
761,106✔
93

94
    void operator()(const Addable&) const;
95
    void operator()(const BoundedLoopCount&) const;
96
    void operator()(const Comparable&) const;
97
    void operator()(const FuncConstraint&) const;
98
    void operator()(const ValidDivisor&) const;
99
    void operator()(const TypeConstraint&) const;
100
    void operator()(const ValidAccess&) const;
101
    void operator()(const ValidCallbackTarget&) const;
102
    void operator()(const ValidMapKeyValue&) const;
103
    void operator()(const ValidMapType&) const;
104
    void operator()(const ValidSize&) const;
105
    void operator()(const ValidArgZero&) const;
106
    void operator()(const ValidStore&) const;
107
    void operator()(const ZeroCtxOffset&) const;
108

109
  private:
110
    void require_value(const TypeToNumDomain& inv, const LinearConstraint& cst, const std::string& msg) const {
398,676✔
111
        if (!inv.values.entail(cst)) {
598,014✔
112
            throw_fail(msg);
1,144✔
113
        }
114
    }
397,532✔
115

116
    [[noreturn]]
117
    void throw_fail(const std::string& msg) const {
1,848✔
118
        throw VerificationFailureSignal(msg + " (" + to_string(assertion) + ")");
2,772✔
119
    }
120

121
    // Per-region bounds checks compose two primitives at each call site, so
122
    // the floor and ceiling for a given access are spelled out where they
123
    // are checked rather than picked by a dispatcher.
124
    void require_lower_bound(const LinearExpression& access_lb, const LinearExpression& floor,
142,862✔
125
                             const std::string& msg) const {
126
        using namespace dsl_syntax;
71,431✔
127
        require_value(dom.state, access_lb >= floor, msg);
142,862✔
128
    }
142,836✔
129
    void require_upper_bound(const LinearExpression& access_ub, const LinearExpression& ceiling,
142,836✔
130
                             const std::string& msg) const {
131
        using namespace dsl_syntax;
71,418✔
132
        require_value(dom.state, access_ub <= ceiling, msg);
142,836✔
133
    }
142,010✔
134

135
    const Assertion assertion;
136

137
    const EbpfDomain& dom;
138
    const AnalysisContext& context;
139
};
140

141
std::optional<VerificationError> ebpf_domain_check(const EbpfDomain& dom, const Assertion& assertion,
1,098,154✔
142
                                                   const Label& where, const AnalysisContext& context) {
143
    if (dom.is_bottom()) {
1,098,154✔
144
        return {};
337,258✔
145
    }
146
    try {
380,448✔
147
        EbpfChecker{dom, assertion, context}.visit();
1,145,040✔
148
    } catch (const VerificationFailureSignal& signal) {
1,848✔
149
        VerificationError error(signal.what());
2,772✔
150
        error.where = where;
1,848✔
151
        return {std::move(error)};
1,848✔
152
    }
1,848✔
153
    return {};
759,048✔
154
}
155

156
void EbpfChecker::operator()(const Comparable& s) const {
11,702✔
157
    using namespace dsl_syntax;
5,851✔
158
    if (dom.state.same_type(s.r1, s.r2)) {
11,702✔
159
        // Same type. If both are numbers, that's okay. Otherwise:
160
        TypeDomain non_number_types = dom.state.types;
11,686✔
161
        non_number_types.remove_type(reg_type(s.r2), T_NUM);
11,686✔
162
        // We must check that they belong to a singleton region:
163
        if (!non_number_types.is_in_group(s.r1, TS_SINGLETON_PTR) && !non_number_types.is_in_group(s.r1, TS_MAP)) {
11,686✔
164
            throw_fail("Cannot subtract pointers to non-singleton regions");
4✔
165
        }
166
        // And, to avoid wraparound errors, they must be within bounds.
167
        this->operator()(ValidAccess{context.runtime().max_call_stack_frames, s.r1, 0, Imm{0}, false});
11,684✔
168
        this->operator()(ValidAccess{context.runtime().max_call_stack_frames, s.r2, 0, Imm{0}, false});
11,682✔
169
    } else {
11,686✔
170
        // _Maybe_ different types, so r2 must be a number.
171
        // We checked in a previous assertion that r1 is a pointer or a number.
172
        if (!dom.state.entail_type(reg_type(s.r2), T_NUM)) {
16✔
173
            throw_fail("Cannot subtract pointers to different regions");
9✔
174
        }
175
    }
176
}
11,692✔
177

178
void EbpfChecker::operator()(const Addable& s) const {
24,694✔
179
    if (!dom.state.implies_superset(s.ptr, TS_POINTER, s.num, TS_NUM)) {
24,694✔
180
        throw_fail("Only numbers can be added to pointers");
21✔
181
    }
182
}
24,680✔
183

184
void EbpfChecker::operator()(const ValidDivisor& s) const {
332✔
185
    using namespace dsl_syntax;
166✔
186
    if (!dom.state.implies_superset(s.reg, TS_POINTER, s.reg, TS_NUM)) {
332✔
187
        throw_fail("Only numbers can be used as divisors");
24✔
188
    }
189
    if (!context.runtime().allow_division_by_zero) {
320✔
190
        const auto reg = reg_pack(s.reg);
64✔
191
        const auto v = s.is_signed ? reg.svalue : reg.uvalue;
64✔
192
        require_value(dom.state, v != 0, "Possible division by zero");
256✔
193
    }
194
}
256✔
195

196
void EbpfChecker::operator()(const ValidStore& s) const {
3,132✔
197
    if (!dom.state.implies_not_type(s.mem, T_STACK, s.val, TS_NUM)) {
3,132✔
198
        throw_fail("Only numbers can be stored to externally-visible regions");
3✔
199
    }
200
}
3,130✔
201

202
void EbpfChecker::operator()(const TypeConstraint& s) const {
510,542✔
203
    if (!dom.state.is_in_group(s.reg, to_typeset(s.types))) {
510,542✔
204
        throw_fail("Invalid type");
951✔
205
    }
206
}
509,908✔
207

208
void EbpfChecker::operator()(const BoundedLoopCount& s) const {
42✔
209
    // Enforces an upper bound on loop iterations by checking that the loop counter
210
    // does not exceed the specified limit
211
    using namespace dsl_syntax;
21✔
212
    const auto counter = variable_registry.loop_counter(to_string(s.name));
42✔
213
    require_value(dom.state, counter <= BoundedLoopCount::limit, "Loop counter is too large");
118✔
214
}
20✔
215

216
void EbpfChecker::operator()(const FuncConstraint& s) const {
50✔
217
    // Look up the helper function id.
218
    if (dom.state.is_bottom()) {
50✔
219
        return;
220
    }
221
    const auto src_interval = dom.state.values.eval_interval(reg_pack(s.reg).uvalue);
50✔
222
    if (const auto sn = src_interval.singleton()) {
50✔
223
        if (sn->fits<int32_t>()) {
48✔
224
            // We can now process it as if the id was immediate.
225
            const int32_t imm = sn->cast_to<int32_t>();
48✔
226
            if (!context.is_helper_usable(imm)) {
48✔
227
                throw_fail("invalid helper function id " + std::to_string(imm));
12✔
228
            }
229
            const Call call{.func = imm, .kind = CallKind::helper};
44✔
230
            for (const Assertion& sub_assertion : get_assertions(call, context.program_info(), context.runtime(), {})) {
232✔
231
                // TODO: create explicit sub assertions elsewhere
232
                EbpfChecker{dom, sub_assertion, context}.visit();
359✔
233
            }
44✔
234
            return;
22✔
235
        }
236
    }
237
    throw_fail("callx helper function id is not a valid singleton");
3✔
238
}
239

240
void EbpfChecker::operator()(const ValidSize& s) const {
14,684✔
241
    using namespace dsl_syntax;
7,342✔
242
    const auto r = reg_pack(s.reg);
14,684✔
243
    require_value(dom.state, s.can_be_zero ? r.svalue >= 0 : r.svalue > 0, "Invalid size");
37,210✔
244
}
14,584✔
245

246
void EbpfChecker::operator()(const ValidArgZero& s) const {
2✔
247
    using namespace dsl_syntax;
1✔
248
    const auto r = reg_pack(s.reg);
2✔
249
    require_value(dom.state, r.svalue == 0, "Argument must be zero");
3✔
250
}
2✔
251

252
void EbpfChecker::operator()(const ValidCallbackTarget& s) const {
8✔
253
    const auto callback_interval = dom.state.values.eval_interval(reg_pack(s.reg).uvalue);
8✔
254
    const auto callback_target = callback_interval.singleton();
8✔
255
    if (!callback_target.has_value() || !callback_target->fits<int32_t>()) {
8✔
256
        throw_fail("callback function pointer must be a singleton code address");
×
257
    }
258

259
    const int32_t callback_label = callback_target->cast_to<int32_t>();
8✔
260
    if (!context.program.callback_target_labels().contains(callback_label)) {
8✔
261
        throw_fail("callback function pointer does not reference a valid callback entry");
4✔
262
    }
263
    if (!context.program.callback_targets_with_exit().contains(callback_label)) {
6✔
264
        throw_fail("callback function does not have a reachable exit");
3✔
265
    }
266
}
4✔
267

268
void EbpfChecker::operator()(const ValidMapKeyValue& s) const {
13,824✔
269
    using namespace dsl_syntax;
6,912✔
270

271
    const auto fd_type = dom.get_map_type(s.map_fd_reg, context);
13,824✔
272

273
    const auto access_reg = reg_pack(s.access_reg);
13,824✔
274
    Number width;
13,824✔
275
    if (s.key) {
13,824✔
276
        const auto key_size = dom.get_map_key_size(s.map_fd_reg, context).singleton();
10,774✔
277
        if (!key_size.has_value()) {
10,774✔
278
            throw_fail("Map key size is not singleton");
×
279
        }
280
        if (!key_size->fits<uint32_t>()) {
10,774✔
281
            throw_fail("Map key size is out of supported range");
×
282
        }
283
        width = *key_size;
10,774✔
284
    } else {
285
        const auto value_size = dom.get_map_value_size(s.map_fd_reg, context).singleton();
3,050✔
286
        if (!value_size.has_value()) {
3,050✔
287
            throw_fail("Map value size is not singleton");
×
288
        }
289
        if (!value_size->fits<uint32_t>()) {
3,050✔
290
            throw_fail("Map value size is out of supported range");
×
291
        }
292
        width = *value_size;
3,050✔
293
    }
294

295
    for (const auto access_reg_type : dom.state.enumerate_types(s.access_reg)) {
27,590✔
296
        switch (access_reg_type) {
13,824✔
297
        case T_STACK: {
13,556✔
298
            Interval offset = dom.state.values.eval_interval(access_reg.stack_offset);
13,556✔
299
            if (!dom.stack->all_num_width(offset, Interval{width})) {
13,556✔
300
                auto lb_is = offset.lb().number();
42✔
301
                std::string lb_s = lb_is && lb_is->fits<int32_t>() ? std::to_string(lb_is->narrow<int32_t>()) : "-oo";
44✔
302
                Interval ub = offset + Interval{width};
42✔
303
                auto ub_is = ub.ub().number();
42✔
304
                std::string ub_s = ub_is && ub_is->fits<int32_t>() ? std::to_string(ub_is->narrow<int32_t>()) : "oo";
65✔
305
                require_value(dom.state, LinearConstraint::false_const(),
84✔
306
                              "Illegal map update with a non-numerical value [" + lb_s + "-" + ub_s + ")");
273✔
307
            } else if (context.runtime().strict && fd_type.has_value()) {
13,598✔
308
                EbpfMapType map_type = context.platform().get_map_type(*fd_type);
×
309
                if (map_type.is_array) {
×
310
                    // Get offset value.
311
                    Variable key_ptr = access_reg.stack_offset;
×
312
                    std::optional<Number> offset_num = dom.state.values.eval_interval(key_ptr).singleton();
×
313
                    if (!offset_num.has_value()) {
×
314
                        throw_fail("Pointer must be a singleton");
×
315
                    } else if (s.key) {
×
316
                        // Look up the value pointed to by the key pointer.
317
                        Variable key_value =
318
                            variable_registry.cell_var(DataKind::svalues, offset_num.value(), sizeof(uint32_t));
×
319

320
                        if (auto max_entries = dom.get_map_max_entries(s.map_fd_reg, context).lb().number()) {
×
321
                            require_value(dom.state, key_value < *max_entries, "Array index overflow");
×
322
                        } else {
323
                            throw_fail("Max entries is not finite");
×
324
                        }
325
                        require_value(dom.state, key_value >= 0, "Array index underflow");
×
326
                    }
327
                }
328
            }
×
329
            break;
13,514✔
330
        }
331
        case T_PACKET: {
140✔
332
            Variable lb = access_reg.packet_offset;
140✔
333
            LinearExpression ub = LinearExpression{lb} + LinearExpression{width};
210✔
334
            require_lower_bound(lb, variable_registry.meta_offset(), "Lower bound must be at least meta_offset");
280✔
335
            require_upper_bound(ub, variable_registry.packet_size(), "Upper bound must be at most packet_size");
210✔
336
            // Packet memory is both readable and writable.
337
            break;
140✔
338
        }
140✔
339
        case T_SHARED: {
128✔
340
            Variable lb = access_reg.shared_offset;
128✔
341
            LinearExpression ub = LinearExpression{lb} + LinearExpression{width};
192✔
342
            require_lower_bound(lb, LinearExpression{0}, "Lower bound must be at least 0");
192✔
343
            require_upper_bound(ub, access_reg.shared_region_size,
196✔
344
                                "Upper bound must be at most " + variable_registry.name(access_reg.shared_region_size));
268✔
345
            require_value(dom.state, access_reg.uvalue > 0, "Possible null access");
264✔
346
            // Shared memory is zero-initialized when created so is safe to read and write.
347
            break;
112✔
348
        }
128✔
349
        default: throw_fail("Only stack, packet, or shared can be used as a parameter");
29✔
350
        }
351
    }
13,824✔
352
}
13,766✔
353

354
void EbpfChecker::operator()(const ValidMapType& s) const {
1,560✔
355
    if (dom.state.is_bottom()) {
1,560✔
356
        return;
16✔
357
    }
358
    const auto map_type = dom.get_map_type(s.map_fd_reg, context);
1,560✔
359
    if (!map_type.has_value() || *map_type == 0) {
1,560✔
360
        return;
8✔
361
    }
362
    if (*map_type >= 64) {
1,544✔
363
        throw_fail("map type " + std::to_string(*map_type) + " is out of supported range for " + s.helper_name);
×
364
    }
365
    if ((s.allowed_map_types & (uint64_t{1} << *map_type)) == 0) {
1,544✔
366
        throw_fail("map type " + std::to_string(*map_type) + " is not allowed for " + s.helper_name);
×
367
    }
368
}
369

370
static std::tuple<LinearExpression, LinearExpression> lb_ub_access_pair(const ValidAccess& s,
142,604✔
371
                                                                        const Variable offset_var) {
372
    using namespace dsl_syntax;
71,302✔
373
    LinearExpression lb = offset_var + s.offset;
213,906✔
374
    LinearExpression ub = std::holds_alternative<Imm>(s.width) ? lb + std::get<Imm>(s.width).v
206,878✔
375
                                                               : lb + reg_pack(std::get<Reg>(s.width)).svalue;
271,152✔
376
    return {lb, ub};
213,906✔
377
}
142,604✔
378

379
void EbpfChecker::operator()(const ValidAccess& s) const {
192,716✔
380
    using namespace dsl_syntax;
96,358✔
381

382
    const bool is_comparison_check = s.width == Value{Imm{0}};
192,716✔
383

384
    const auto reg = reg_pack(s.reg);
192,716✔
385
    for (const auto type : dom.state.enumerate_types(s.reg)) {
384,774✔
386
        switch (type) {
192,980✔
387
        case T_STACK: {
14,196✔
388
            const auto [lb, ub] = lb_ub_access_pair(s, reg.stack_offset);
14,196✔
389
            require_lower_bound(lb, reg_pack(R10_STACK_POINTER).stack_offset - context.runtime().subprogram_stack_size,
35,498✔
390
                                "Lower bound must be at least r10.stack_offset - subprogram_stack_size");
391
            require_upper_bound(ub, LinearExpression{context.runtime().total_stack_size()},
21,296✔
392
                                "Upper bound must be at most total_stack_size");
393
            // Stack reads must hit known-numeric bytes.
394
            if (s.access_type == AccessType::read &&
20,572✔
395
                !dom.stack->all_num_lb_ub(dom.state.values.eval_interval(lb), dom.state.values.eval_interval(ub))) {
20,456✔
396
                if (s.offset < 0) {
232✔
397
                    throw_fail("Stack content is not numeric");
×
398
                } else {
399
                    LinearExpression w = std::holds_alternative<Imm>(s.width)
232✔
400
                                             ? LinearExpression{std::get<Imm>(s.width).v}
232✔
401
                                             : reg_pack(std::get<Reg>(s.width)).svalue;
256✔
402
                    require_value(dom.state, w <= reg.stack_numeric_size - s.offset, "Stack content is not numeric");
590✔
403
                }
232✔
404
            }
405
            break;
14,148✔
406
        }
14,196✔
407
        case T_CTX: {
13,980✔
408
            const auto* desc = context.program_info().type.ctx_descriptor;
13,980✔
409
            const auto [lb, ub] = lb_ub_access_pair(s, reg.ctx_offset);
13,980✔
410
            if (s.access_type == AccessType::write && desc->end >= 0) {
13,980✔
411
                const int field_width = desc->end - desc->data;
3,390✔
412
                if (field_width <= 0) {
3,390✔
413
                    throw_fail("Cannot write to context with unexpected pointer-field layout");
×
414
                }
415
                const std::array packet_pointer_fields{
1,695✔
416
                    EbpfStructFieldDescriptor{.offset = desc->data,
1,695✔
417
                                              .span = field_width,
418
                                              .permission = EbpfStructFieldPermission::read_only,
419
                                              .max_access_width = field_width},
420
                    EbpfStructFieldDescriptor{.offset = desc->end,
1,695✔
421
                                              .span = field_width,
422
                                              .permission = EbpfStructFieldPermission::read_only,
423
                                              .max_access_width = field_width},
424
                    EbpfStructFieldDescriptor{.offset = desc->meta,
3,390✔
425
                                              .span = field_width,
426
                                              .permission = EbpfStructFieldPermission::read_only,
427
                                              .max_access_width = field_width},
428
                };
3,390✔
429
                if (write_may_touch_readonly_field(dom.state.values, lb, ub, packet_pointer_fields)) {
3,390✔
430
                    throw_fail("Cannot write to context pointer field");
20✔
431
                }
432
            }
433
            const auto ctx_size = desc->size;
13,970✔
434
            require_lower_bound(lb, LinearExpression{0}, "Lower bound must be at least 0");
20,959✔
435
            require_upper_bound(ub, LinearExpression{ctx_size},
20,950✔
436
                                "Upper bound must be at most " + std::to_string(ctx_size));
27,942✔
437
            // T_CTX: bounds suffice; non-null when in bounds.
438
            break;
13,964✔
439
        }
13,980✔
440
        case T_PACKET: {
22,908✔
441
            const auto [lb, ub] = lb_ub_access_pair(s, reg.packet_offset);
22,908✔
442
            require_lower_bound(lb, variable_registry.meta_offset(), "Lower bound must be at least meta_offset");
34,366✔
443
            // Pointer-comparison checks (width == 0) may legitimately reach
444
            // past the runtime packet_size, so they use the looser
445
            // max_packet_size ceiling. Real dereferences must be bounded by
446
            // the runtime packet_size variable.
447
            if (is_comparison_check) {
22,904✔
448
                const auto max = context.runtime().max_packet_size;
11,142✔
449
                require_upper_bound(ub, LinearExpression{max}, "Upper bound must be at most " + std::to_string(max));
16,713✔
450
            } else {
451
                require_upper_bound(ub, variable_registry.packet_size(), "Upper bound must be at most packet_size");
23,892✔
452
            }
453
            break;
22,172✔
454
        }
22,908✔
455
        case T_SHARED: {
91,358✔
456
            const auto [lb, ub] = lb_ub_access_pair(s, reg.shared_offset);
91,358✔
457
            require_lower_bound(lb, LinearExpression{0}, "Lower bound must be at least 0");
137,051✔
458
            require_upper_bound(ub, reg.shared_region_size,
137,054✔
459
                                "Upper bound must be at most " + variable_registry.name(reg.shared_region_size));
182,802✔
460
            if (!is_comparison_check && !s.or_null) {
91,268✔
461
                require_value(dom.state, reg.uvalue > 0, "Possible null access");
213,993✔
462
            }
463
            break;
91,254✔
464
        }
91,358✔
465
        case T_SOCKET: {
100✔
466
            const ebpf_platform_t* platform = context.program_info().platform;
100✔
467
            if (!platform || !platform->sock_common_layout) {
100✔
468
                throw_fail("Socket layout is unavailable");
×
469
            }
470
            const EbpfStructDescriptor& socket_layout = *platform->sock_common_layout;
100✔
471
            const auto [lb, ub] = lb_ub_access_pair(s, reg.socket_offset);
100✔
472
            require_lower_bound(lb, LinearExpression{0}, "Lower bound must be at least 0");
150✔
473
            require_upper_bound(ub, LinearExpression{socket_layout.size},
150✔
474
                                "Upper bound must be at most " + std::to_string(socket_layout.size));
200✔
475
            if (!is_comparison_check) {
100✔
476
                if (s.access_type == AccessType::write) {
28✔
477
                    throw_fail("Socket memory is read-only");
×
478
                }
479
                if (!std::holds_alternative<Imm>(s.width)) {
28✔
480
                    throw_fail("Socket access size must be constant");
×
481
                }
482
                const Interval offset = dom.state.values.eval_interval(lb);
28✔
483
                const auto exact_offset = offset.singleton();
28✔
484
                if (!exact_offset || !exact_offset->fits_cast_to<int32_t>()) {
28✔
485
                    throw_fail("Socket access offset must be precise");
×
486
                }
487
                const auto width = static_cast<int>(std::get<Imm>(s.width).v);
28✔
488
                if (!is_valid_struct_access(socket_layout, exact_offset->cast_to<int32_t>(), width, s.access_type)) {
28✔
489
                    throw_fail("Invalid socket access");
12✔
490
                }
491
                if (!s.or_null) {
22✔
492
                    require_value(dom.state, reg.uvalue > 0, "Possible null access");
58✔
493
                }
494
            }
495
            break;
94✔
496
        }
100✔
497
        case T_ALLOC_MEM: {
62✔
498
            const auto [lb, ub] = lb_ub_access_pair(s, reg.alloc_mem_offset);
62✔
499
            require_lower_bound(lb, LinearExpression{0}, "Lower bound must be at least 0");
93✔
500
            require_upper_bound(ub, reg.alloc_mem_size,
93✔
501
                                "Upper bound must be at most " + variable_registry.name(reg.alloc_mem_size));
124✔
502
            if (!is_comparison_check && !s.or_null) {
62✔
503
                require_value(dom.state, reg.uvalue > 0, "Possible null access");
120✔
504
            }
505
            break;
62✔
506
        }
62✔
507
        case T_NUM:
50,242✔
508
            if (!is_comparison_check) {
50,242✔
509
                if (s.or_null) {
498✔
510
                    require_value(dom.state, reg.svalue == 0, "Non-null number");
752✔
511
                    // A null pointer access is only valid with zero width.
512
                    if (std::holds_alternative<Imm>(s.width)) {
496✔
513
                        if (std::get<Imm>(s.width).v != 0) {
×
514
                            throw_fail("Non-zero access size with null pointer");
×
515
                        }
516
                    } else {
517
                        const auto width_svalue = reg_pack(std::get<Reg>(s.width)).svalue;
496✔
518
                        require_value(dom.state, width_svalue == 0, "Non-zero access size with null pointer");
996✔
519
                    }
520
                } else {
521
                    throw_fail("Only pointers can be dereferenced");
×
522
                }
523
            }
524
            break;
25,119✔
525
        case T_MAP: [[fallthrough]];
126✔
526
        case T_MAP_PROGRAMS:
63✔
527
            if (!is_comparison_check) {
126✔
528
                throw_fail("FDs cannot be dereferenced directly");
×
529
            }
530
            break;
63✔
531
        case T_BTF_ID:
×
532
            // TODO: implement proper access checks for these pointer types.
533
            if (!is_comparison_check) {
×
534
                throw_fail("Unsupported pointer type for memory access");
×
535
            }
536
            break;
537
        case T_FUNC:
×
538
            if (!is_comparison_check) {
×
539
                throw_fail("Function pointers cannot be dereferenced");
×
540
            }
541
            break;
542
        default: throw_fail("Invalid type");
477✔
543
        }
544
    }
192,716✔
545
}
191,794✔
546

547
void EbpfChecker::operator()(const ZeroCtxOffset& s) const {
11,184✔
548
    using namespace dsl_syntax;
5,592✔
549
    const auto reg = reg_pack(s.reg);
11,184✔
550
    // The domain is not expressive enough to handle join of null and non-null ctx,
551
    // Since non-null ctx pointers are nonzero numbers.
552
    if (s.or_null && dom.state.is_in_group(s.reg, TS_NUM) && dom.state.values.entail(reg.uvalue == 0)) {
16,824✔
553
        return;
24✔
554
    }
555
    require_value(dom.state, reg.ctx_offset == 0, "Nonzero context offset");
22,324✔
556
}
557

558
} // 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