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

Alan-Jowett / ebpf-verifier / 22108536430

16 Feb 2026 11:57PM UTC coverage: 87.142% (+0.02%) from 87.121%
22108536430

push

github

web-flow
Replace patricia tree with std::map (#1008)

Empirically, the median collection holds ~3 cells, overlap queries hit
<5% of the time, and the offset map is <1% of verifier runtime. At these
sizes, the patricia tree provided no benefit over std::map while adding
an external dependency.

- Remove radix_tree submodule dependency and offset_t bit-manipulation helpers
- Replace patricia tree with std::map<offset_t, std::set<Cell>>
- Simplify Cell to a plain struct with public (offset, size) fields
- Use defaulted == and <=> instead of manual comparison operators
- Replace Interval-based overlap check with direct integer arithmetic
- Move to_interval, symbolic_overlap, operator<< to free functions
- Inline get_scalar() as variable_registry->cell_var() at all call sites
- Document offset bound invariant in Cell::overlap
- Add assert(sz > 0) in Cell::overlap to document the invariant
- Simplify largest-cell lookup in get_overlap_cells_symbolic_offset:
  std::set is already sorted by (offset, size), so *rbegin() suffices
- Fix bug in backward scan of get_overlap_cells: remove early break
  that could skip overlapping cells at earlier offsets (a bucket with
  small cells may not overlap while an earlier bucket with larger
  cells does)
- Add assertion that offset <= EBPF_TOTAL_STACK_SIZE before the
gsl::narrow<int> conversion, documenting the precondition that
makes the narrowing safe.
- Add regression test for backward scan early-break bug

YAML test: writing to a sub-range of a cell must kill the cell so that a subsequent full-range
load returns unknown rather than the stale value.
The observation r0.svalue=0 (consistent mode) passes when the cell is correctly
havoced (0 is consistent with unknown) but fails when the stale
value r0.svalue=99 persists.

Verified: test fails when both the old remove_cell (no tombstone
cleanup) and the early break are reintroduced.
- Extract cell_var(kind, cell) helper to reduce repetition of
  variable_registry->cell_var(kind, c... (continued)

57 of 59 new or added lines in 1 file covered. (96.61%)

2 existing lines in 1 file now uncovered.

9319 of 10694 relevant lines covered (87.14%)

3049666.64 hits per line

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

86.67
/src/crab/array_domain.cpp
1
// Copyright (c) Prevail Verifier contributors.
2
// SPDX-License-Identifier: Apache-2.0
3

4
#include <algorithm>
5
#include <optional>
6
#include <set>
7
#include <unordered_map>
8
#include <utility>
9
#include <vector>
10

11
#include "boost/endian/conversion.hpp"
12
#include <gsl/narrow>
13
#include <map>
14

15
#include "arith/dsl_syntax.hpp"
16
#include "config.hpp"
17
#include "crab/array_domain.hpp"
18
#include "crab/var_registry.hpp"
19
#include "crab_utils/num_safety.hpp"
20

21
#include <ranges>
22

23
namespace prevail {
24

25
using Index = uint64_t;
26

27
using offset_t = Index;
28

29
// Conceptually, a cell is tuple of an array, offset, size, and
30
// scalar variable such that:
31
//   scalar = array[offset, offset + 1, ..., offset + size - 1]
32
// For simplicity, we don't carry the array inside the cell.
33
struct Cell final {
34
    offset_t offset{};
214,779✔
35
    unsigned size{};
212,604✔
36

37
    bool operator==(const Cell&) const = default;
103,767✔
38
    auto operator<=>(const Cell&) const = default;
322,248✔
39

40
    // Return true if [o, o + sz) definitely overlaps with the cell.
41
    // Offsets are bounded by EBPF_TOTAL_STACK_SIZE (4096), so wraparound cannot occur.
42
    [[nodiscard]]
43
    bool overlap(const offset_t o, const unsigned sz) const {
3,205,728✔
44
        assert(sz > 0 && "overlap query with zero width");
3,205,728✔
45
        return offset < o + sz && o < offset + size;
3,121,201✔
46
    }
47
};
48

49
static Interval cell_to_interval(const offset_t o, const unsigned size) {
14,688✔
50
    assert(o <= EBPF_TOTAL_STACK_SIZE && "offset out of bounds");
14,688✔
51
    const Number lb{gsl::narrow<int>(o)};
14,688✔
52
    return {lb, lb + size - 1};
22,032✔
53
}
54

55
// Return true if [symb_lb, symb_ub] may overlap with the cell,
56
// where symb_lb and symb_ub are not constant expressions.
57
static bool symbolic_overlap(const Cell& c, const Interval& range) {
96✔
58
    return !(cell_to_interval(c.offset, c.size) & range).is_bottom();
96✔
59
}
60

NEW
61
std::ostream& operator<<(std::ostream& o, const Cell& c) { return o << "cell(" << c.offset << "," << c.size << ")"; }
×
62

63
static Variable cell_var(const DataKind kind, const Cell& c) {
141,326✔
64
    return variable_registry->cell_var(kind, c.offset, c.size);
141,326✔
65
}
66

67
// Map offsets to cells.
68
// std::map/std::set are used deliberately: empirically, the median collection holds ~3 cells,
69
// overlap queries hit <5% of the time, and the entire offset map is <1% of verifier runtime.
70
// At these sizes, specialized data structures (patricia tries, flat sorted vectors) show no
71
// macro-level improvement while adding complexity or external dependencies.
72
class offset_map_t final {
4,833✔
73
  private:
74
    friend class ArrayDomain;
75

76
    using cell_set_t = std::set<Cell>;
77

78
    using map_t = std::map<offset_t, cell_set_t>;
79

80
    map_t _map;
81

82
    void remove_cell(const Cell& c);
83

84
    void insert_cell(const Cell& c);
85

86
    [[nodiscard]]
87
    std::optional<Cell> get_cell(offset_t o, unsigned size);
88

89
    Cell mk_cell(offset_t o, unsigned size);
90

91
  public:
92
    offset_map_t() = default;
9,666✔
93

94
    [[nodiscard]]
95
    bool empty() const {
96
        return _map.empty();
97
    }
98

99
    [[nodiscard]]
100
    std::size_t size() const {
101
        return _map.size();
102
    }
103

104
    void operator-=(const Cell& c) { remove_cell(c); }
20,166✔
105

106
    void operator-=(const std::vector<Cell>& cells) {
13,584✔
107
        for (const auto& c : cells) {
33,750✔
108
            this->operator-=(c);
20,166✔
109
        }
110
    }
13,584✔
111

112
    // Return in out all cells that might overlap with (o, size).
113
    std::vector<Cell> get_overlap_cells(offset_t o, unsigned size);
114

115
    [[nodiscard]]
116
    std::vector<Cell> get_overlap_cells_symbolic_offset(const Interval& range);
117

118
    friend std::ostream& operator<<(std::ostream& o, offset_map_t& m);
119

120
    /* Operations needed if used as value in a separate_domain */
121
    [[nodiscard]]
122
    bool is_top() const {
123
        return empty();
124
    }
125

126
    [[nodiscard]]
127
    // ReSharper disable once CppMemberFunctionMayBeStatic
128
    bool is_bottom() const {
129
        return false;
130
    }
131
    /*
132
       We don't distinguish between bottom and top.
133
       This is fine because separate_domain only calls bottom if operator[] is called over a bottom state.
134
       Thus, we will make sure that we don't call operator[] in that case.
135
    */
136
    static offset_map_t bottom() { return offset_map_t(); }
137
    static offset_map_t top() { return offset_map_t(); }
138
};
139

140
void offset_map_t::remove_cell(const Cell& c) {
20,166✔
141
    const offset_t key = c.offset;
20,166✔
142
    if (auto it = _map.find(key); it != _map.end()) {
20,166✔
143
        it->second.erase(c);
20,166✔
144
        if (it->second.empty()) {
20,166✔
145
            _map.erase(it);
19,880✔
146
        }
147
    }
148
}
20,166✔
149

150
[[nodiscard]]
151
std::vector<Cell> offset_map_t::get_overlap_cells_symbolic_offset(const Interval& range) {
56✔
152
    std::vector<Cell> out;
56✔
153
    for (const auto& o_cells : _map | std::views::values) {
152✔
154
        // All cells in o_cells have the same offset. They only differ in the size.
155
        // If the largest cell overlaps with [offset, offset + size)
156
        // then the rest of cells are considered to overlap.
157
        // This is an over-approximation because [offset, offset+size) can overlap
158
        // with the largest cell, but it doesn't necessarily overlap with smaller cells.
159
        // For efficiency, we assume it overlaps with all.
160
        if (!o_cells.empty()) {
96✔
161
            // Cells are sorted by (offset, size); last element has the largest size.
162
            const Cell& largest_cell = *o_cells.rbegin();
96✔
163
            if (symbolic_overlap(largest_cell, range)) {
96✔
164
                for (const auto& c : o_cells) {
192✔
165
                    out.push_back(c);
96✔
166
                }
167
            }
168
        }
169
    }
170
    return out;
56✔
171
}
×
172

173
void offset_map_t::insert_cell(const Cell& c) { _map[c.offset].insert(c); }
44,674✔
174

175
std::optional<Cell> offset_map_t::get_cell(const offset_t o, const unsigned size) {
109,464✔
176
    if (const auto it = _map.find(o); it != _map.end()) {
109,464✔
177
        if (const auto cit = it->second.find(Cell(o, size)); cit != it->second.end()) {
60,532✔
178
            return *cit;
58,656✔
179
        }
180
    }
181
    return {};
50,808✔
182
}
183

184
Cell offset_map_t::mk_cell(const offset_t o, const unsigned size) {
76,904✔
185
    // TODO: check array is the array associated to this offset map
186

187
    if (const auto maybe_c = get_cell(o, size)) {
76,904✔
188
        return *maybe_c;
32,230✔
189
    }
190
    // create a new scalar variable for representing the contents
191
    // of bytes array[o,o+1,..., o+size-1]
192
    const Cell c(o, size);
44,674✔
193
    insert_cell(c);
44,674✔
194
    return c;
44,674✔
195
}
196

197
// Return all cells that might overlap with (o, size).
198
std::vector<Cell> offset_map_t::get_overlap_cells(const offset_t o, const unsigned size) {
393,346✔
199
    std::vector<Cell> out;
393,346✔
200
    const Cell query_cell(o, size);
393,346✔
201

202
    // Search backwards: cells at offsets <= o that might extend into [o, o+size).
203
    // We cannot break early: a bucket with small cells may not overlap while an
204
    // earlier bucket with larger cells does (e.g., Cell(48,8) overlaps [54,56)
205
    // but Cell(50,2) does not). The map is tiny (~3 entries) so this is fine.
206
    for (auto it = _map.upper_bound(o); it != _map.begin();) {
3,613,939✔
207
        --it;
3,023,920✔
208
        for (const Cell& x : it->second) {
6,048,190✔
209
            if (x.overlap(o, size) && x != query_cell) {
4,589,376✔
210
                out.push_back(x);
24,470✔
211
            }
212
        }
213
    }
214

215
    // Search forwards: cells at offsets > o that start within [o, o+size).
216
    // Early break is safe here: if no cell at offset k overlaps, then k >= o + size,
217
    // and all subsequent offsets are even larger, so they cannot overlap either.
218
    // No duplicates: backward and forward scans visit disjoint key ranges.
219
    for (auto it = _map.upper_bound(o); it != _map.end(); ++it) {
602,423✔
220
        bool any_overlap = false;
181,458✔
221
        for (const Cell& x : it->second) {
362,916✔
222
            if (x.overlap(o, size)) {
187,660✔
223
                out.push_back(x);
12,404✔
224
                any_overlap = true;
6,202✔
225
            }
226
        }
227
        if (!any_overlap) {
181,458✔
228
            break;
84,527✔
229
        }
230
    }
231

232
    return out;
590,019✔
UNCOV
233
}
×
234

235
// We use a global array map
236
using array_map_t = std::unordered_map<DataKind, offset_map_t>;
237

238
static thread_local LazyAllocator<array_map_t> thread_local_array_map;
271,424✔
239

240
void clear_thread_local_state() { thread_local_array_map.clear(); }
4,208✔
241

242
static offset_map_t& lookup_array_map(const DataKind kind) { return (*thread_local_array_map)[kind]; }
538,640✔
243

244
void ArrayDomain::initialize_numbers(const int lb, const int width) {
272✔
245
    num_bytes.reset(lb, width);
272✔
246
    lookup_array_map(DataKind::svalues).mk_cell(offset_t{gsl::narrow_cast<Index>(lb)}, width);
272✔
247
}
272✔
248

249
std::ostream& operator<<(std::ostream& o, offset_map_t& m) {
×
250
    if (m._map.empty()) {
×
251
        o << "empty";
×
252
    } else {
NEW
253
        for (const auto& cells : m._map | std::views::values) {
×
254
            o << "{";
×
255
            for (auto cit = cells.begin(), cet = cells.end(); cit != cet;) {
×
256
                o << *cit;
×
257
                ++cit;
×
258
                if (cit != cet) {
×
259
                    o << ",";
×
260
                }
261
            }
262
            o << "}\n";
×
263
        }
264
    }
265
    return o;
×
266
}
267

268
// Create a new cell that is a subset of an existing cell.
269
void ArrayDomain::split_cell(NumAbsDomain& inv, const DataKind kind, const int cell_start_index,
1,004✔
270
                             const unsigned int len) const {
271
    assert(kind == DataKind::svalues || kind == DataKind::uvalues);
1,004✔
272

273
    // Get the values from the indicated stack range.
274
    const std::optional<LinearExpression> svalue = load(inv, DataKind::svalues, Interval{cell_start_index}, len);
1,004✔
275
    const std::optional<LinearExpression> uvalue = load(inv, DataKind::uvalues, Interval{cell_start_index}, len);
1,004✔
276

277
    // Create a new cell for that range.
278
    offset_map_t& offset_map = lookup_array_map(kind);
1,004✔
279
    const Cell new_cell = offset_map.mk_cell(offset_t{gsl::narrow_cast<Index>(cell_start_index)}, len);
1,004✔
280
    inv.assign(cell_var(DataKind::svalues, new_cell), svalue);
1,004✔
281
    inv.assign(cell_var(DataKind::uvalues, new_cell), uvalue);
1,506✔
282
}
1,004✔
283

284
// Prepare to havoc bytes in the middle of a cell by potentially splitting the cell if it is numeric,
285
// into the part to the left of the havoced portion, and the part to the right of the havoced portion.
286
void ArrayDomain::split_number_var(NumAbsDomain& inv, DataKind kind, const Interval& ii,
68,392✔
287
                                   const Interval& elem_size) const {
288
    assert(kind == DataKind::svalues || kind == DataKind::uvalues);
68,392✔
289
    offset_map_t& offset_map = lookup_array_map(kind);
68,392✔
290
    const std::optional<Number> n = ii.singleton();
68,392✔
291
    if (!n) {
68,392✔
292
        // We can only split a singleton offset.
293
        return;
16✔
294
    }
295
    const std::optional<Number> n_bytes = elem_size.singleton();
68,376✔
296
    if (!n_bytes) {
68,376✔
297
        // We can only split a singleton size.
298
        return;
299
    }
300
    const auto size = n_bytes->narrow<unsigned int>();
68,376✔
301
    const offset_t o(n->narrow<Index>());
68,376✔
302

303
    const std::vector<Cell> cells = offset_map.get_overlap_cells(o, size);
68,376✔
304
    for (const Cell& c : cells) {
82,968✔
305
        const auto [cell_start_index, cell_end_index] = cell_to_interval(c.offset, c.size).pair<int>();
14,592✔
306
        if (!this->num_bytes.all_num(cell_start_index, cell_end_index + 1) ||
21,129✔
307
            cell_end_index + 1UL < cell_start_index + sizeof(int64_t)) {
13,074✔
308
            // We can only split numeric cells of size 8 or less.
309
            continue;
13,644✔
310
        }
311

312
        if (!inv.eval_interval(cell_var(kind, c)).is_singleton()) {
3,663✔
313
            // We can only split cells with a singleton value.
314
            continue;
1,494✔
315
        }
316
        if (gsl::narrow_cast<Index>(cell_start_index) < o) {
948✔
317
            // Use the bytes to the left of the specified range.
318
            split_cell(inv, kind, cell_start_index, gsl::narrow<unsigned int>(o - cell_start_index));
286✔
319
        }
320
        if (o + size < cell_end_index + 1UL) {
948✔
321
            // Use the bytes to the right of the specified range.
322
            split_cell(inv, kind, gsl::narrow<int>(o + size),
718✔
323
                       gsl::narrow<unsigned int>(cell_end_index - (o + size - 1)));
718✔
324
        }
325
    }
326
}
68,376✔
327

328
// we can only treat this as non-member because we use global state
329
static std::optional<std::pair<offset_t, unsigned>> kill_and_find_var(NumAbsDomain& inv, DataKind kind,
321,174✔
330
                                                                      const Interval& ii, const Interval& elem_size) {
331
    std::optional<std::pair<offset_t, unsigned>> res;
321,174✔
332

333
    offset_map_t& offset_map = lookup_array_map(kind);
321,174✔
334
    std::vector<Cell> cells;
321,174✔
335
    if (const std::optional<Number> n = ii.singleton()) {
321,174✔
336
        if (const auto n_bytes = elem_size.singleton()) {
321,118✔
337
            auto size = n_bytes->narrow<unsigned int>();
321,118✔
338
            // -- Constant index: kill overlapping cells
339
            offset_t o(n->narrow<Index>());
321,118✔
340
            cells = offset_map.get_overlap_cells(o, size);
481,677✔
341
            res = std::make_pair(o, size);
321,118✔
342
        }
343
    }
344
    if (!res) {
321,174✔
345
        // -- Non-constant index: kill overlapping cells
346
        cells = offset_map.get_overlap_cells_symbolic_offset(ii | (ii + elem_size));
84✔
347
    }
348
    if (!cells.empty()) {
321,174✔
349
        // Forget the scalars from the numerical domain
350
        for (const auto& c : cells) {
33,750✔
351
            inv.havoc(cell_var(kind, c));
20,166✔
352

353
            // Forget signed and unsigned values together.
354
            if (kind == DataKind::svalues) {
20,166✔
355
                inv.havoc(cell_var(DataKind::uvalues, c));
7,542✔
356
            } else if (kind == DataKind::uvalues) {
12,624✔
357
                inv.havoc(cell_var(DataKind::svalues, c));
7,114✔
358
            }
359
        }
360
        // Remove the cells. If needed again they will be re-created.
361
        offset_map -= cells;
13,584✔
362
    }
363
    return res;
481,761✔
364
}
321,174✔
365
static std::tuple<int, int> as_numbytes_range(const Interval& index, const Interval& width) {
4,132✔
366
    return (index | (index + width)).bound(0, EBPF_TOTAL_STACK_SIZE);
4,132✔
367
}
368

369
bool ArrayDomain::all_num_lb_ub(const Interval& lb, const Interval& ub) const {
3,634✔
370
    const auto [min_lb, max_ub] = (lb | ub).bound(0, EBPF_TOTAL_STACK_SIZE);
3,634✔
371
    if (min_lb > max_ub) {
3,634✔
372
        return false;
373
    }
374
    return this->num_bytes.all_num(min_lb, max_ub);
3,634✔
375
}
376

377
bool ArrayDomain::all_num_width(const Interval& index, const Interval& width) const {
4,128✔
378
    const auto [min_lb, max_ub] = as_numbytes_range(index, width);
4,128✔
379
    assert(min_lb <= max_ub);
4,128✔
380
    return this->num_bytes.all_num(min_lb, max_ub);
6,192✔
381
}
382

383
// Get the number of bytes, starting at offset, that are known to be numbers.
384
int ArrayDomain::min_all_num_size(const NumAbsDomain& inv, const Variable offset) const {
14,378✔
385
    const auto min_lb = inv.eval_interval(offset).lb().number();
21,567✔
386
    const auto max_ub = inv.eval_interval(offset).ub().number();
21,567✔
387
    if (!min_lb || !max_ub || !min_lb->fits<int32_t>() || !max_ub->fits<int32_t>()) {
14,378✔
388
        return 0;
1,935✔
389
    }
390
    const auto lb = min_lb->narrow<int>();
10,508✔
391
    const auto ub = max_ub->narrow<int>();
10,508✔
392
    return std::max(0, this->num_bytes.all_num_width(lb) - (ub - lb));
14,822✔
393
}
394

395
// Get one byte of a value.
396
std::optional<uint8_t> get_value_byte(const NumAbsDomain& inv, const offset_t o, const int width) {
25,680✔
397
    const Variable v = variable_registry->cell_var(DataKind::svalues, (o / width) * width, width);
25,680✔
398
    const std::optional<Number> t = inv.eval_interval(v).singleton();
25,680✔
399
    if (!t) {
25,680✔
400
        return {};
13,926✔
401
    }
402
    Index n = t->cast_to<Index>();
11,754✔
403

404
    // Convert value to bytes of the appropriate endian-ness.
405
    switch (width) {
11,754✔
406
    case sizeof(uint8_t): break;
87✔
407
    case sizeof(uint16_t):
132✔
408
        if (thread_local_options.big_endian) {
132✔
409
            n = boost::endian::native_to_big<uint16_t>(n);
×
410
        } else {
411
            n = boost::endian::native_to_little<uint16_t>(n);
132✔
412
        }
413
        break;
66✔
414
    case sizeof(uint32_t):
596✔
415
        if (thread_local_options.big_endian) {
596✔
416
            n = boost::endian::native_to_big<uint32_t>(n);
×
417
        } else {
418
            n = boost::endian::native_to_little<uint32_t>(n);
596✔
419
        }
420
        break;
298✔
421
    case sizeof(Index):
10,852✔
422
        if (thread_local_options.big_endian) {
10,852✔
423
            n = boost::endian::native_to_big<Index>(n);
48✔
424
        } else {
425
            n = boost::endian::native_to_little<Index>(n);
5,402✔
426
        }
427
        break;
5,426✔
428
    default: CRAB_ERROR("Unexpected width ", width);
×
429
    }
430
    const auto bytes = reinterpret_cast<uint8_t*>(&n);
11,754✔
431
    return bytes[o % width];
11,754✔
432
}
433

434
std::optional<LinearExpression> ArrayDomain::load(const NumAbsDomain& inv, const DataKind kind, const Interval& i,
29,952✔
435
                                                  const int width) const {
436
    if (const std::optional<Number> n = i.singleton()) {
29,952✔
437
        offset_map_t& offset_map = lookup_array_map(kind);
29,936✔
438
        const int64_t k = n->narrow<int64_t>();
29,936✔
439
        const offset_t o(k);
29,936✔
440
        const unsigned size = to_unsigned(width);
29,936✔
441
        if (const auto cell = lookup_array_map(kind).get_cell(o, size)) {
29,936✔
442
            return cell_var(kind, *cell);
35,709✔
443
        }
444
        if (kind == DataKind::svalues || kind == DataKind::uvalues) {
6,130✔
445
            // Copy bytes into result_buffer, taking into account that the
446
            // bytes might be in different stack variables and might be unaligned.
447
            uint8_t result_buffer[8];
448
            bool found = true;
11,999✔
449
            for (unsigned int index = 0; index < size; index++) {
17,876✔
450
                const offset_t byte_offset{o + index};
14,890✔
451
                std::optional<uint8_t> b = get_value_byte(inv, byte_offset, 8);
14,890✔
452
                if (!b) {
14,890✔
453
                    b = get_value_byte(inv, byte_offset, 4);
4,038✔
454
                    if (!b) {
4,038✔
455
                        b = get_value_byte(inv, byte_offset, 2);
3,442✔
456
                        if (!b) {
3,442✔
457
                            b = get_value_byte(inv, byte_offset, 1);
3,310✔
458
                        }
459
                    }
460
                }
461
                if (b) {
14,890✔
462
                    result_buffer[index] = *b;
11,754✔
463
                } else {
464
                    found = false;
3,136✔
465
                    break;
3,136✔
466
                }
467
            }
468
            if (found) {
4,629✔
469
                // We have an aligned result in result_buffer so we can now
470
                // convert to an integer.
471
                if (size == 1) {
2,986✔
472
                    return *result_buffer;
2,465✔
473
                }
474
                if (size == 2) {
2,620✔
475
                    uint16_t b = *reinterpret_cast<uint16_t*>(result_buffer);
448✔
476
                    if (thread_local_options.big_endian) {
448✔
477
                        b = boost::endian::native_to_big<uint16_t>(b);
12✔
478
                    } else {
479
                        b = boost::endian::native_to_little<uint16_t>(b);
442✔
480
                    }
481
                    return b;
448✔
482
                }
483
                if (size == 4) {
2,172✔
484
                    uint32_t b = *reinterpret_cast<uint32_t*>(result_buffer);
1,382✔
485
                    if (thread_local_options.big_endian) {
1,382✔
486
                        b = boost::endian::native_to_big<uint32_t>(b);
12✔
487
                    } else {
488
                        b = boost::endian::native_to_little<uint32_t>(b);
685✔
489
                    }
490
                    return b;
1,382✔
491
                }
492
                if (size == 8) {
790✔
493
                    Index b = *reinterpret_cast<Index*>(result_buffer);
86✔
494
                    if (thread_local_options.big_endian) {
86✔
495
                        b = boost::endian::native_to_big<Index>(b);
4✔
496
                    } else {
497
                        b = boost::endian::native_to_little<Index>(b);
41✔
498
                    }
499
                    return kind == DataKind::uvalues ? Number(b) : Number(to_signed(b));
86✔
500
                }
501
            }
502
        }
503

504
        const std::vector<Cell> cells = offset_map.get_overlap_cells(o, size);
3,848✔
505
        if (cells.empty()) {
3,848✔
506
            const Cell c = offset_map.mk_cell(o, size);
2,046✔
507
            // Here it's ok to do assignment (instead of expand) because c is not a summarized variable.
508
            // Otherwise, it would be unsound.
509
            return cell_var(kind, c);
2,046✔
510
        }
511
        CRAB_WARN("Ignored read from cell ", kind, "[", o, "...", o + size - 1, "]", " because it overlaps with ",
1,802✔
512
                  cells.size(), " cells");
513
        /*
514
            TODO: we can apply here "Value Recomposition" a la Mine'06 (https://arxiv.org/pdf/cs/0703074.pdf)
515
                to construct values of some type from a sequence of bytes.
516
                It can be endian-independent but it would more precise if we choose between little- and big-endian.
517
        */
518
    } else {
3,848✔
519
        // TODO: we can be more precise here
520
        CRAB_WARN("array expansion: ignored array load because of non-constant array index ", i);
16✔
521
    }
522
    return {};
1,818✔
523
}
524

525
std::optional<LinearExpression> ArrayDomain::load_type(const Interval& i, int width) const {
11,724✔
526
    if (std::optional<Number> n = i.singleton()) {
11,724✔
527
        offset_map_t& offset_map = lookup_array_map(DataKind::types);
11,724✔
528
        int64_t k = n->narrow<int64_t>();
11,724✔
529
        auto [only_num, only_non_num] = num_bytes.uniformity(k, width);
11,724✔
530
        if (only_num) {
11,724✔
531
            return T_NUM;
9,100✔
532
        }
533
        if (!only_non_num || width != 8) {
2,624✔
534
            return {};
×
535
        }
536
        offset_t o(k);
2,624✔
537
        unsigned size = to_unsigned(width);
2,624✔
538
        if (auto cell = lookup_array_map(DataKind::types).get_cell(o, size)) {
2,624✔
539
            return cell_var(DataKind::types, *cell);
3,930✔
540
        }
541
        std::vector<Cell> cells = offset_map.get_overlap_cells(o, size);
4✔
542
        if (cells.empty()) {
4✔
543
            Cell c = offset_map.mk_cell(o, size);
4✔
544
            // Here it's ok to do assignment (instead of expand) because c is not a summarized variable.
545
            // Otherwise, it would be unsound.
546
            return cell_var(DataKind::types, c);
4✔
547
        }
548
        CRAB_WARN("Ignored read from cell ", DataKind::types, "[", o, "...", o + size - 1, "]",
×
549
                  " because it overlaps with ", cells.size(), " cells");
550
        /*
551
            TODO: we can apply here "Value Recomposition" a la Mine'06 (https://arxiv.org/pdf/cs/0703074.pdf)
552
                to construct values of some type from a sequence of bytes.
553
                It can be endian-independent but it would more precise if we choose between little- and big-endian.
554
        */
555
    } else {
4✔
556
        // Check whether the kind is uniform across the entire interval.
557
        auto lb = i.lb().number();
×
558
        auto ub = i.ub().number();
×
559
        if (lb.has_value() && ub.has_value()) {
×
560
            Number fullwidth = ub.value() - lb.value() + width;
×
561
            if (lb->fits<uint32_t>() && fullwidth.fits<uint32_t>()) {
×
562
                auto [only_num, only_non_num] =
×
563
                    num_bytes.uniformity(lb->narrow<uint32_t>(), fullwidth.narrow<uint32_t>());
×
564
                if (only_num) {
×
565
                    return T_NUM;
×
566
                }
567
            }
568
        }
569
    }
570
    return {};
×
571
}
572

573
// We are about to write to a given range of bytes on the stack.
574
// Any cells covering that range need to be removed, and any cells that only
575
// partially cover that range can be split such that any non-covered portions become new cells.
576
static std::optional<std::pair<offset_t, unsigned>> split_and_find_var(const ArrayDomain& array_domain,
321,174✔
577
                                                                       NumAbsDomain& inv, const DataKind kind,
578
                                                                       const Interval& idx, const Interval& elem_size) {
579
    if (kind == DataKind::svalues || kind == DataKind::uvalues) {
296,846✔
580
        array_domain.split_number_var(inv, kind, idx, elem_size);
68,392✔
581
    }
582
    return kill_and_find_var(inv, kind, idx, elem_size);
296,846✔
583
}
584

585
std::optional<Variable> ArrayDomain::store(NumAbsDomain& inv, const DataKind kind, const Interval& idx,
49,274✔
586
                                           const Interval& elem_size) {
587
    if (auto maybe_cell = split_and_find_var(*this, inv, kind, idx, elem_size)) {
49,274✔
588
        // perform strong update
589
        auto [offset, size] = *maybe_cell;
49,266✔
590
        const Cell c = lookup_array_map(kind).mk_cell(offset, size);
49,266✔
591
        Variable v = cell_var(kind, c);
49,266✔
592
        return v;
49,266✔
593
    }
594
    return {};
8✔
595
}
596

597
std::optional<Variable> ArrayDomain::store_type(TypeDomain& inv, const Interval& idx, const Interval& width,
24,316✔
598
                                                const bool is_num) {
599
    constexpr auto kind = DataKind::types;
24,316✔
600
    if (auto maybe_cell = split_and_find_var(*this, inv.inv, kind, idx, width)) {
24,316✔
601
        // perform strong update
602
        auto [offset, size] = *maybe_cell;
24,312✔
603
        if (is_num) {
24,312✔
604
            num_bytes.reset(offset, size);
23,754✔
605
        } else {
606
            num_bytes.havoc(offset, size);
558✔
607
        }
608
        const Cell c = lookup_array_map(kind).mk_cell(offset, size);
24,312✔
609
        Variable v = cell_var(kind, c);
24,312✔
610
        return v;
24,312✔
611
    } else {
612
        using namespace dsl_syntax;
2✔
613
        // havoc the entire range
614
        const auto [lb, ub] = as_numbytes_range(idx, width);
4✔
615
        num_bytes.havoc(lb, ub);
4✔
616
    }
617
    return {};
4✔
618
}
619

620
void ArrayDomain::havoc(NumAbsDomain& inv, const DataKind kind, const Interval& idx, const Interval& elem_size) {
223,244✔
621
    split_and_find_var(*this, inv, kind, idx, elem_size);
223,244✔
622
}
223,244✔
623

624
void ArrayDomain::havoc_type(TypeDomain& inv, const Interval& idx, const Interval& elem_size) {
24,340✔
625
    constexpr auto kind = DataKind::types;
24,340✔
626
    if (auto maybe_cell = split_and_find_var(*this, inv.inv, kind, idx, elem_size)) {
24,340✔
627
        auto [offset, size] = *maybe_cell;
24,336✔
628
        num_bytes.havoc(offset, size);
24,336✔
629
    }
630
}
24,340✔
631

632
void ArrayDomain::store_numbers(const Interval& _idx, const Interval& _width) {
1,088✔
633
    const std::optional<Number> idx_n = _idx.singleton();
1,088✔
634
    if (!idx_n) {
1,088✔
635
        CRAB_WARN("array expansion store range ignored because ", "lower bound is not constant");
×
636
        return;
×
637
    }
638

639
    const std::optional<Number> width = _width.singleton();
1,088✔
640
    if (!width) {
1,088✔
641
        CRAB_WARN("array expansion store range ignored because ", "upper bound is not constant");
×
642
        return;
×
643
    }
644

645
    if (*idx_n + *width > EBPF_TOTAL_STACK_SIZE) {
1,088✔
646
        CRAB_WARN("array expansion store range ignored because ",
×
647
                  "the number of elements is larger than default limit of ", EBPF_TOTAL_STACK_SIZE);
648
        return;
×
649
    }
650
    num_bytes.reset(idx_n->narrow<int>(), width->narrow<int>());
1,088✔
651
}
652

653
void ArrayDomain::set_to_top() { num_bytes.set_to_top(); }
×
654

655
void ArrayDomain::set_to_bottom() { num_bytes.set_to_bottom(); }
×
656

UNCOV
657
bool ArrayDomain::is_bottom() const { return num_bytes.is_bottom(); }
×
658

659
bool ArrayDomain::is_top() const { return num_bytes.is_top(); }
×
660

661
StringInvariant ArrayDomain::to_set() const { return num_bytes.to_set(); }
1,422✔
662

663
bool ArrayDomain::operator<=(const ArrayDomain& other) const { return num_bytes <= other.num_bytes; }
548✔
664

665
bool ArrayDomain::operator==(const ArrayDomain& other) const { return num_bytes == other.num_bytes; }
×
666

667
void ArrayDomain::operator|=(const ArrayDomain& other) { num_bytes |= other.num_bytes; }
128✔
668

669
void ArrayDomain::operator|=(ArrayDomain&& other) { num_bytes |= std::move(other.num_bytes); }
17,074✔
670

671
ArrayDomain ArrayDomain::operator|(const ArrayDomain& other) const { return ArrayDomain(num_bytes | other.num_bytes); }
×
672

673
ArrayDomain ArrayDomain::operator&(const ArrayDomain& other) const { return ArrayDomain(num_bytes & other.num_bytes); }
54✔
674

675
ArrayDomain ArrayDomain::widen(const ArrayDomain& other) const { return ArrayDomain(num_bytes | other.num_bytes); }
102✔
676

677
ArrayDomain ArrayDomain::narrow(const ArrayDomain& other) const { return ArrayDomain(num_bytes & other.num_bytes); }
×
678

679
std::ostream& operator<<(std::ostream& o, const ArrayDomain& dom) { return o << dom.num_bytes; }
×
680
} // 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