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

Alan-Jowett / ebpf-verifier / 21765175576

06 Feb 2026 11:02AM UTC coverage: 86.699% (-0.09%) from 86.792%
21765175576

push

github

web-flow
SplitDBM as one-sided numerical domain (#985)


Signed-off-by: Elazar Gershuni <elazarg@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

1316 of 1401 new or added lines in 15 files covered. (93.93%)

136 existing lines in 4 files now uncovered.

9334 of 10766 relevant lines covered (86.7%)

3133567.38 hits per line

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

86.08
/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 <stdexcept>
8
#include <unordered_map>
9
#include <utility>
10
#include <vector>
11

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

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

22
namespace prevail {
23

24
using Index = uint64_t;
25

26
class offset_t final {
27
    Index _index{};
28
    int _prefix_length;
29

30
  public:
31
    static constexpr int bitsize = 8 * sizeof(Index);
32
    offset_t() : _prefix_length(bitsize) {}
2,999,236✔
33
    explicit offset_t(const Index index) : _index(index), _prefix_length(bitsize) {}
2,939,316✔
34
    offset_t(const Index index, const int prefix_length) : _index(index), _prefix_length(prefix_length) {}
3,795,594✔
35
    explicit operator int() const { return gsl::narrow<int>(_index); }
2,579,580✔
36
    operator Index() const { return _index; }
732,855,395✔
37
    [[nodiscard]]
38
    int prefix_length() const {
15,463,513✔
39
        return _prefix_length;
15,463,513✔
40
    }
41

42
    Index operator[](const int n) const { return (_index >> (bitsize - 1 - n)) & 1; }
16,455,813✔
43
};
44

45
// NOTE: required by radix_tree
46
// Get the length of a key, which is the size usable with the [] operator.
47
[[maybe_unused]]
48
int radix_length(const offset_t& offset) {
15,463,513✔
49
    return offset.prefix_length();
15,463,513✔
50
}
51

52
// NOTE: required by radix_tree
53
// Get a range of bits out of the middle of a key, starting at [begin] for a given length.
54
[[maybe_unused]]
55
offset_t radix_substr(const offset_t& key, const int begin, const int length) {
7,521,218✔
56
    // radix_tree should only call this with valid bit-slice arguments.
57
    // If this is violated, treat it as a verifier bug and fail fast.
58
    if (begin < 0 || length < 0) {
7,521,218✔
UNCOV
59
        throw std::runtime_error("radix_substr: negative begin/length");
×
60
    }
61
    if (begin >= offset_t::bitsize || length > offset_t::bitsize) {
7,521,218✔
UNCOV
62
        throw std::runtime_error("radix_substr: begin or length exceeds bitsize");
×
63
    }
64
    if (static_cast<long long>(begin) + static_cast<long long>(length) > offset_t::bitsize) {
7,521,218✔
UNCOV
65
        throw std::runtime_error("radix_substr: begin + length exceeds bitsize");
×
66
    }
67

68
    if (length == 0) {
7,521,218✔
69
        return offset_t{0, 0};
114,400✔
70
    }
71

72
    // begin + length <= bitsize implies begin == 0 when length == bitsize.
73
    if (length == offset_t::bitsize) {
7,406,818✔
74
        return offset_t{gsl::narrow_cast<Index>(key), length};
69,970✔
75
    }
76

77
    // 1 <= length <= 63 here, so shifts are well-defined.
78
    Index mask = (Index{1} << length) - 1;
7,336,848✔
79
    mask <<= (offset_t::bitsize - length - begin);
7,336,848✔
80

81
    const Index value = (gsl::narrow_cast<Index>(key) & mask) << begin;
7,336,848✔
82
    return offset_t{value, length};
7,336,848✔
83
}
84

85
// NOTE: required by radix_tree
86
// Concatenate two bit patterns.
87
[[maybe_unused]]
UNCOV
88
offset_t radix_join(const offset_t& entry1, const offset_t& entry2) {
×
UNCOV
89
    const Index value = entry1 | (entry2 >> entry1.prefix_length());
×
UNCOV
90
    const int prefix_length = entry1.prefix_length() + entry2.prefix_length();
×
UNCOV
91
    return offset_t{value, prefix_length};
×
92
}
93

94
/***
95
   Conceptually, a cell is tuple of an array, offset, size, and
96
   scalar variable such that:
97

98
_scalar = array[_offset, _offset + 1, ..., _offset + _size - 1]
99

100
    For simplicity, we don't carry the array inside the cell class.
101
    Only, offset_map objects can create cells. They will consider the
102
            array when generating the scalar variable.
103
*/
104
class Cell final {
105
  private:
106
    friend class offset_map_t;
107

108
    offset_t _offset{};
109
    unsigned _size{};
110

111
    // Only offset_map_t can create cells
112
    Cell() = default;
448✔
113

114
    Cell(const offset_t offset, const unsigned size) : _offset(offset), _size(size) {}
857,972✔
115

116
    static Interval to_interval(const offset_t o, const unsigned size) {
2,579,580✔
117
        const Number lb{gsl::narrow<int>(o)};
2,579,580✔
118
        return {lb, lb + size - 1};
7,738,740✔
119
    }
2,579,580✔
120

121
  public:
122
    [[nodiscard]]
123
    Interval to_interval() const {
1,866,022✔
124
        return to_interval(_offset, _size);
1,866,022✔
125
    }
126

127
    [[nodiscard]]
128
    bool is_null() const {
544✔
129
        return _offset == 0 && _size == 0;
496✔
130
    }
131

132
    [[nodiscard]]
133
    offset_t get_offset() const {
685,752✔
134
        return _offset;
685,752✔
135
    }
136

137
    [[nodiscard]]
138
    Variable get_scalar(const DataKind kind) const {
140,422✔
139
        return variable_registry->cell_var(kind, gsl::narrow<Index>(_offset), _size);
210,633✔
140
    }
141

142
    // ignore the scalar variable
143
    bool operator==(const Cell& o) const { return to_interval() == o.to_interval(); }
1,138,256✔
144

145
    // ignore the scalar variable
146
    bool operator<(const Cell& o) const {
1,060,156✔
147
        if (_offset == o._offset) {
1,060,156✔
148
            return _size < o._size;
1,060,156✔
149
        }
UNCOV
150
        return _offset < o._offset;
×
151
    }
152

153
    // Return true if [o, o + size) definitely overlaps with the cell,
154
    // where o is a constant expression.
155
    [[nodiscard]]
156
    bool overlap(const offset_t& o, const unsigned size) const {
713,558✔
157
        const Interval x = to_interval();
713,558✔
158
        const Interval y = to_interval(o, size);
713,558✔
159
        const bool res = (!(x & y).is_bottom());
1,070,337✔
160
        CRAB_LOG("array-expansion-overlap",
713,558✔
161
                 std::cout << "**Checking if " << x << " overlaps with " << y << "=" << res << "\n";);
162
        return res;
713,558✔
163
    }
1,070,337✔
164

165
    // Return true if [symb_lb, symb_ub] may overlap with the cell,
166
    // where symb_lb and symb_ub are not constant expressions.
167
    [[nodiscard]]
168
    bool symbolic_overlap(const Interval& range) const;
169

UNCOV
170
    friend std::ostream& operator<<(std::ostream& o, const Cell& c) { return o << "cell(" << c.to_interval() << ")"; }
×
171
};
172

173
// Return true if [symb_lb, symb_ub] may overlap with the cell,
174
// where symb_lb and symb_ub are not constant expressions.
175
bool Cell::symbolic_overlap(const Interval& range) const { return !(to_interval() & range).is_bottom(); }
240✔
176

177
// Map offsets to cells
178
class offset_map_t final {
4,789✔
179
  private:
180
    friend class ArrayDomain;
181

182
    using cell_set_t = std::set<Cell>;
183

184
    /*
185
      The keys in the patricia tree are processing in big-endian order. This means that the keys are sorted.
186
      Sortedness is very important to efficiently perform operations such as checking for overlap cells.
187
      Since keys are treated as bit patterns, negative offsets can be used, but they are treated as large unsigned
188
      numbers.
189
    */
190
    using PatriciaTree = radix_tree<offset_t, cell_set_t>;
191

192
    PatriciaTree _map;
193

194
    // for algorithm::lower_bound and algorithm::upper_bound
195
    struct CompareBinding {
196
        bool operator()(const PatriciaTree::value_type& kv, const offset_t& o) const { return kv.first < o; }
1,837,862✔
197
        bool operator()(const offset_t& o, const PatriciaTree::value_type& kv) const { return o < kv.first; }
1,821,304✔
198
        bool operator()(const PatriciaTree::value_type& kv1, const PatriciaTree::value_type& kv2) const {
199
            return kv1.first < kv2.first;
200
        }
201
    };
202

203
    void remove_cell(const Cell& c);
204

205
    void insert_cell(const Cell& c);
206

207
    [[nodiscard]]
208
    std::optional<Cell> get_cell(offset_t o, unsigned size);
209

210
    Cell mk_cell(offset_t o, unsigned size);
211

212
  public:
213
    offset_map_t() = default;
9,578✔
214

215
    [[nodiscard]]
216
    bool empty() const {
217
        return _map.empty();
218
    }
219

220
    [[nodiscard]]
221
    std::size_t size() const {
222
        return _map.size();
223
    }
224

225
    void operator-=(const Cell& c) { remove_cell(c); }
19,550✔
226

227
    void operator-=(const std::vector<Cell>& cells) {
13,534✔
228
        for (const auto& c : cells) {
33,084✔
229
            this->operator-=(c);
19,550✔
230
        }
231
    }
13,534✔
232

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

236
    [[nodiscard]]
237
    std::vector<Cell> get_overlap_cells_symbolic_offset(const Interval& range);
238

239
    friend std::ostream& operator<<(std::ostream& o, offset_map_t& m);
240

241
    /* Operations needed if used as value in a separate_domain */
242
    [[nodiscard]]
243
    bool is_top() const {
244
        return empty();
245
    }
246

247
    [[nodiscard]]
248
    // ReSharper disable once CppMemberFunctionMayBeStatic
249
    bool is_bottom() const {
250
        return false;
251
    }
252
    /*
253
       We don't distinguish between bottom and top.
254
       This is fine because separate_domain only calls bottom if operator[] is called over a bottom state.
255
       Thus, we will make sure that we don't call operator[] in that case.
256
    */
257
    static offset_map_t bottom() { return offset_map_t(); }
258
    static offset_map_t top() { return offset_map_t(); }
259
};
260

261
void offset_map_t::remove_cell(const Cell& c) {
330,456✔
262
    const offset_t key = c.get_offset();
330,456✔
263
    _map[key].erase(c);
330,456✔
264
}
330,456✔
265

266
[[nodiscard]]
267
std::vector<Cell> offset_map_t::get_overlap_cells_symbolic_offset(const Interval& range) {
56✔
268
    std::vector<Cell> out;
56✔
269
    for (const auto& [_offset, o_cells] : _map) {
756✔
270
        // All cells in o_cells have the same offset. They only differ in the size.
271
        // If the largest cell overlaps with [offset, offset + size)
272
        // then the rest of cells are considered to overlap.
273
        // This is an over-approximation because [offset, offset+size) can overlap
274
        // with the largest cell, but it doesn't necessarily overlap with smaller cells.
275
        // For efficiency, we assume it overlaps with all.
276
        Cell largest_cell;
448✔
277
        for (const Cell& c : o_cells) {
544✔
278
            if (largest_cell.is_null()) {
96✔
279
                largest_cell = c;
96✔
280
            } else {
281
                assert(c.get_offset() == largest_cell.get_offset());
×
UNCOV
282
                if (largest_cell < c) {
×
UNCOV
283
                    largest_cell = c;
×
284
                }
285
            }
286
        }
287
        if (!largest_cell.is_null()) {
672✔
288
            if (largest_cell.symbolic_overlap(range)) {
96✔
289
                for (const auto& c : o_cells) {
192✔
290
                    out.push_back(c);
96✔
291
                }
292
            }
293
        }
294
    }
295
    return out;
56✔
UNCOV
296
}
×
297

298
void offset_map_t::insert_cell(const Cell& c) { _map[c.get_offset()].insert(c); }
355,296✔
299

300
std::optional<Cell> offset_map_t::get_cell(const offset_t o, const unsigned size) {
502,676✔
301
    cell_set_t& cells = _map[o];
502,676✔
302
    const auto it = cells.find(Cell(o, size));
502,676✔
303
    if (it != cells.end()) {
502,676✔
304
        return *it;
141,334✔
305
    }
306
    return {};
361,342✔
307
}
308

309
Cell offset_map_t::mk_cell(const offset_t o, const unsigned size) {
77,050✔
310
    // TODO: check array is the array associated to this offset map
311

312
    if (const auto maybe_c = get_cell(o, size)) {
77,050✔
313
        return *maybe_c;
32,660✔
314
    }
315
    // create a new scalar variable for representing the contents
316
    // of bytes array[o,o+1,..., o+size-1]
317
    const Cell c(o, size);
44,390✔
318
    insert_cell(c);
44,390✔
319
    return c;
44,390✔
320
}
321

322
// Return all cells that might overlap with (o, size).
323
std::vector<Cell> offset_map_t::get_overlap_cells(const offset_t o, const unsigned size) {
393,086✔
324
    std::vector<Cell> out;
393,086✔
325
    constexpr CompareBinding comp;
196,543✔
326

327
    bool added = false;
393,086✔
328
    auto maybe_c = get_cell(o, size);
393,086✔
329
    if (!maybe_c) {
393,086✔
330
        maybe_c = Cell(o, size);
310,906✔
331
        insert_cell(*maybe_c);
310,906✔
332
        added = true;
155,453✔
333
    }
334

335
    auto lb_it = std::lower_bound(_map.begin(), _map.end(), o, comp);
393,086✔
336
    if (lb_it != _map.end()) {
393,086✔
337
        // Store _map[begin,...,lb_it] into a vector so that we can
338
        // go backwards from lb_it.
339
        //
340
        // TODO: give support for reverse iterator in patricia_tree.
341
        std::vector<cell_set_t> upto_lb;
393,086✔
342
        upto_lb.reserve(std::distance(_map.begin(), lb_it));
786,172✔
343
        for (auto it = _map.begin(), et = lb_it; it != et; ++it) {
7,335,332✔
344
            upto_lb.push_back(it->second);
6,942,246✔
345
        }
346
        upto_lb.push_back(lb_it->second);
393,086✔
347

348
        for (int i = gsl::narrow<int>(upto_lb.size() - 1); i >= 0; --i) {
789,880✔
349
            ///////
350
            // All the cells in upto_lb[i] have the same offset. They
351
            // just differ in the size.
352
            //
353
            // If none of the cells in upto_lb[i] overlap with (o, size)
354
            // we can stop.
355
            ////////
356
            bool continue_outer_loop = false;
721,286✔
357
            for (const Cell& x : upto_lb[i]) {
1,276,014✔
358
                if (x.overlap(o, size)) {
554,728✔
359
                    if (x != *maybe_c) {
417,308✔
360
                        // FIXME: we might have some duplicates. this is a very drastic solution.
361
                        if (std::ranges::find(out, x) == out.end()) {
24,222✔
362
                            out.push_back(x);
24,222✔
363
                        }
364
                    }
365
                    continue_outer_loop = true;
208,654✔
366
                }
367
            }
368
            if (!continue_outer_loop) {
721,286✔
369
                break;
162,246✔
370
            }
371
        }
372
    }
393,086✔
373

374
    // search for overlapping cells > o
375
    auto ub_it = std::upper_bound(_map.begin(), _map.end(), o, comp);
393,086✔
376
    for (; ub_it != _map.end(); ++ub_it) {
404,414✔
377
        bool continue_outer_loop = false;
355,090✔
378
        for (const Cell& x : ub_it->second) {
502,592✔
379
            if (x.overlap(o, size)) {
147,502✔
380
                // FIXME: we might have some duplicates. this is a very drastic solution.
381
                if (std::ranges::find(out, x) == out.end()) {
11,328✔
382
                    out.push_back(x);
11,328✔
383
                }
384
                continue_outer_loop = true;
5,664✔
385
            }
386
        }
387
        if (!continue_outer_loop) {
355,090✔
388
            break;
171,881✔
389
        }
390
    }
391

392
    // do not forget the rest of overlapping cells == o
393
    for (auto it = ++lb_it, et = ub_it; it != et; ++it) {
404,414✔
394
        bool continue_outer_loop = false;
11,328✔
395
        for (const Cell& x : it->second) {
22,656✔
396
            if (x == *maybe_c) { // we don't put it in out
11,328✔
UNCOV
397
                continue;
×
398
            }
399
            if (x.overlap(o, size)) {
11,328✔
400
                if (std::ranges::find(out, x) == out.end()) {
11,328✔
UNCOV
401
                    out.push_back(x);
×
402
                }
403
                continue_outer_loop = true;
5,664✔
404
            }
405
        }
406
        if (!continue_outer_loop) {
11,328✔
407
            break;
408
        }
409
    }
410

411
    if (added) {
393,086✔
412
        remove_cell(*maybe_c);
310,906✔
413
    }
414
    return out;
589,629✔
UNCOV
415
}
×
416

417
// We use a global array map
418
using array_map_t = std::unordered_map<DataKind, offset_map_t>;
419

420
static thread_local LazyAllocator<array_map_t> thread_local_array_map;
271,246✔
421

422
void clear_thread_local_state() { thread_local_array_map.clear(); }
4,156✔
423

424
static offset_map_t& lookup_array_map(const DataKind kind) { return (*thread_local_array_map)[kind]; }
538,336✔
425

426
void ArrayDomain::initialize_numbers(const int lb, const int width) {
272✔
427
    num_bytes.reset(lb, width);
272✔
428
    lookup_array_map(DataKind::svalues).mk_cell(offset_t{gsl::narrow_cast<Index>(lb)}, width);
272✔
429
}
272✔
430

UNCOV
431
std::ostream& operator<<(std::ostream& o, offset_map_t& m) {
×
432
    if (m._map.empty()) {
×
UNCOV
433
        o << "empty";
×
434
    } else {
UNCOV
435
        for (const auto& [_offset, cells] : m._map) {
×
UNCOV
436
            o << "{";
×
UNCOV
437
            for (auto cit = cells.begin(), cet = cells.end(); cit != cet;) {
×
UNCOV
438
                o << *cit;
×
UNCOV
439
                ++cit;
×
UNCOV
440
                if (cit != cet) {
×
UNCOV
441
                    o << ",";
×
442
                }
443
            }
UNCOV
444
            o << "}\n";
×
445
        }
446
    }
UNCOV
447
    return o;
×
448
}
449

450
// Create a new cell that is a subset of an existing cell.
451
void ArrayDomain::split_cell(NumAbsDomain& inv, const DataKind kind, const int cell_start_index,
1,002✔
452
                             const unsigned int len) const {
453
    assert(kind == DataKind::svalues || kind == DataKind::uvalues);
1,002✔
454

455
    // Get the values from the indicated stack range.
456
    const std::optional<LinearExpression> svalue = load(inv, DataKind::svalues, Interval{cell_start_index}, len);
1,002✔
457
    const std::optional<LinearExpression> uvalue = load(inv, DataKind::uvalues, Interval{cell_start_index}, len);
1,002✔
458

459
    // Create a new cell for that range.
460
    offset_map_t& offset_map = lookup_array_map(kind);
1,002✔
461
    const Cell new_cell = offset_map.mk_cell(offset_t{gsl::narrow_cast<Index>(cell_start_index)}, len);
1,002✔
462
    inv.assign(new_cell.get_scalar(DataKind::svalues), svalue);
1,002✔
463
    inv.assign(new_cell.get_scalar(DataKind::uvalues), uvalue);
1,503✔
464
}
1,002✔
465

466
// Prepare to havoc bytes in the middle of a cell by potentially splitting the cell if it is numeric,
467
// into the part to the left of the havoced portion, and the part to the right of the havoced portion.
468
void ArrayDomain::split_number_var(NumAbsDomain& inv, DataKind kind, const Interval& ii,
68,356✔
469
                                   const Interval& elem_size) const {
470
    assert(kind == DataKind::svalues || kind == DataKind::uvalues);
68,356✔
471
    offset_map_t& offset_map = lookup_array_map(kind);
68,356✔
472
    const std::optional<Number> n = ii.singleton();
68,356✔
473
    if (!n) {
68,356✔
474
        // We can only split a singleton offset.
475
        return;
8✔
476
    }
477
    const std::optional<Number> n_bytes = elem_size.singleton();
68,340✔
478
    if (!n_bytes) {
68,340✔
479
        // We can only split a singleton size.
UNCOV
480
        return;
×
481
    }
482
    const auto size = n_bytes->narrow<unsigned int>();
68,340✔
483
    const offset_t o(n->narrow<Index>());
68,340✔
484

485
    const std::vector<Cell> cells = offset_map.get_overlap_cells(o, size);
68,340✔
486
    for (const Cell& c : cells) {
82,452✔
487
        const auto [cell_start_index, cell_end_index] = c.to_interval().pair<int>();
21,168✔
488
        if (!this->num_bytes.all_num(cell_start_index, cell_end_index + 1) ||
20,419✔
489
            cell_end_index + 1UL < cell_start_index + sizeof(int64_t)) {
12,614✔
490
            // We can only split numeric cells of size 8 or less.
491
            continue;
13,186✔
492
        }
493

494
        if (!inv.eval_interval(c.get_scalar(kind)).is_singleton()) {
4,844✔
495
            // We can only split cells with a singleton value.
496
            continue;
1,496✔
497
        }
498
        if (gsl::narrow_cast<Index>(cell_start_index) < o) {
926✔
499
            // Use the bytes to the left of the specified range.
500
            split_cell(inv, kind, cell_start_index, gsl::narrow<unsigned int>(o - cell_start_index));
268✔
501
        }
502
        if (o + size < cell_end_index + 1UL) {
926✔
503
            // Use the bytes to the right of the specified range.
504
            split_cell(inv, kind, gsl::narrow<int>(o + size),
734✔
505
                       gsl::narrow<unsigned int>(cell_end_index - (o + size - 1)));
734✔
506
        }
507
    }
508
}
68,356✔
509

510
// we can only treat this as non-member because we use global state
511
static std::optional<std::pair<offset_t, unsigned>> kill_and_find_var(NumAbsDomain& inv, DataKind kind,
320,998✔
512
                                                                      const Interval& ii, const Interval& elem_size) {
513
    std::optional<std::pair<offset_t, unsigned>> res;
320,998✔
514

515
    offset_map_t& offset_map = lookup_array_map(kind);
320,998✔
516
    std::vector<Cell> cells;
320,998✔
517
    if (const std::optional<Number> n = ii.singleton()) {
320,998✔
518
        if (const auto n_bytes = elem_size.singleton()) {
320,942✔
519
            auto size = n_bytes->narrow<unsigned int>();
320,942✔
520
            // -- Constant index: kill overlapping cells
521
            offset_t o(n->narrow<Index>());
320,942✔
522
            cells = offset_map.get_overlap_cells(o, size);
481,413✔
523
            res = std::make_pair(o, size);
320,942✔
524
        }
320,942✔
525
    }
160,499✔
526
    if (!res) {
320,998✔
527
        // -- Non-constant index: kill overlapping cells
528
        cells = offset_map.get_overlap_cells_symbolic_offset(ii | (ii + elem_size));
140✔
529
    }
530
    if (!cells.empty()) {
320,998✔
531
        // Forget the scalars from the numerical domain
532
        for (const auto& c : cells) {
33,084✔
533
            inv.havoc(c.get_scalar(kind));
19,550✔
534

535
            // Forget signed and unsigned values together.
536
            if (kind == DataKind::svalues) {
19,550✔
537
                inv.havoc(c.get_scalar(DataKind::uvalues));
7,286✔
538
            } else if (kind == DataKind::uvalues) {
12,264✔
539
                inv.havoc(c.get_scalar(DataKind::svalues));
6,890✔
540
            }
541
        }
542
        // Remove the cells. If needed again they will be re-created.
543
        offset_map -= cells;
13,534✔
544
    }
545
    return res;
481,497✔
546
}
320,998✔
547
static std::tuple<int, int> as_numbytes_range(const Interval& index, const Interval& width) {
4,132✔
548
    return (index | (index + width)).bound(0, EBPF_TOTAL_STACK_SIZE);
8,264✔
549
}
550

551
bool ArrayDomain::all_num_lb_ub(const Interval& lb, const Interval& ub) const {
3,634✔
552
    const auto [min_lb, max_ub] = (lb | ub).bound(0, EBPF_TOTAL_STACK_SIZE);
5,451✔
553
    if (min_lb > max_ub) {
3,634✔
554
        return false;
555
    }
556
    return this->num_bytes.all_num(min_lb, max_ub);
3,634✔
557
}
558

559
bool ArrayDomain::all_num_width(const Interval& index, const Interval& width) const {
4,128✔
560
    const auto [min_lb, max_ub] = as_numbytes_range(index, width);
4,128✔
561
    assert(min_lb <= max_ub);
4,128✔
562
    return this->num_bytes.all_num(min_lb, max_ub);
6,192✔
563
}
564

565
// Get the number of bytes, starting at offset, that are known to be numbers.
566
int ArrayDomain::min_all_num_size(const NumAbsDomain& inv, const Variable offset) const {
14,372✔
567
    const auto min_lb = inv.eval_interval(offset).lb().number();
35,930✔
568
    const auto max_ub = inv.eval_interval(offset).ub().number();
35,930✔
569
    if (!min_lb || !max_ub || !min_lb->fits<int32_t>() || !max_ub->fits<int32_t>()) {
14,372✔
570
        return 0;
1,935✔
571
    }
572
    const auto lb = min_lb->narrow<int>();
10,502✔
573
    const auto ub = max_ub->narrow<int>();
10,502✔
574
    return std::max(0, this->num_bytes.all_num_width(lb) - (ub - lb));
14,816✔
575
}
14,372✔
576

577
// Get one byte of a value.
578
std::optional<uint8_t> get_value_byte(const NumAbsDomain& inv, const offset_t o, const int width) {
25,284✔
579
    const Variable v = variable_registry->cell_var(DataKind::svalues, (o / width) * width, width);
37,926✔
580
    const std::optional<Number> t = inv.eval_interval(v).singleton();
37,926✔
581
    if (!t) {
25,284✔
582
        return {};
13,878✔
583
    }
584
    Index n = t->cast_to<Index>();
11,406✔
585

586
    // Convert value to bytes of the appropriate endian-ness.
587
    switch (width) {
11,406✔
588
    case sizeof(uint8_t): break;
87✔
589
    case sizeof(uint16_t):
132✔
590
        if (thread_local_options.big_endian) {
132✔
UNCOV
591
            n = boost::endian::native_to_big<uint16_t>(n);
×
592
        } else {
593
            n = boost::endian::native_to_little<uint16_t>(n);
132✔
594
        }
595
        break;
66✔
596
    case sizeof(uint32_t):
596✔
597
        if (thread_local_options.big_endian) {
596✔
UNCOV
598
            n = boost::endian::native_to_big<uint32_t>(n);
×
599
        } else {
600
            n = boost::endian::native_to_little<uint32_t>(n);
596✔
601
        }
602
        break;
298✔
603
    case sizeof(Index):
10,504✔
604
        if (thread_local_options.big_endian) {
10,504✔
605
            n = boost::endian::native_to_big<Index>(n);
48✔
606
        } else {
607
            n = boost::endian::native_to_little<Index>(n);
5,228✔
608
        }
609
        break;
5,252✔
UNCOV
610
    default: CRAB_ERROR("Unexpected width ", width);
×
611
    }
612
    const auto bytes = reinterpret_cast<uint8_t*>(&n);
11,406✔
613
    return bytes[o % width];
11,406✔
614
}
25,284✔
615

616
std::optional<LinearExpression> ArrayDomain::load(const NumAbsDomain& inv, const DataKind kind, const Interval& i,
29,932✔
617
                                                  const int width) const {
618
    if (const std::optional<Number> n = i.singleton()) {
29,932✔
619
        offset_map_t& offset_map = lookup_array_map(kind);
29,916✔
620
        const int64_t k = n->narrow<int64_t>();
29,916✔
621
        const offset_t o(k);
29,916✔
622
        const unsigned size = to_unsigned(width);
29,916✔
623
        if (const auto cell = lookup_array_map(kind).get_cell(o, size)) {
29,916✔
624
            return cell->get_scalar(kind);
35,811✔
625
        }
626
        if (kind == DataKind::svalues || kind == DataKind::uvalues) {
6,042✔
627
            // Copy bytes into result_buffer, taking into account that the
628
            // bytes might be in different stack variables and might be unaligned.
629
            uint8_t result_buffer[8];
630
            bool found = true;
11,737✔
631
            for (unsigned int index = 0; index < size; index++) {
17,440✔
632
                const offset_t byte_offset{o + index};
14,530✔
633
                std::optional<uint8_t> b = get_value_byte(inv, byte_offset, 8);
14,530✔
634
                if (!b) {
14,530✔
635
                    b = get_value_byte(inv, byte_offset, 4);
4,026✔
636
                    if (!b) {
4,026✔
637
                        b = get_value_byte(inv, byte_offset, 2);
3,430✔
638
                        if (!b) {
3,430✔
639
                            b = get_value_byte(inv, byte_offset, 1);
3,298✔
640
                        }
641
                    }
642
                }
643
                if (b) {
14,530✔
644
                    result_buffer[index] = *b;
11,406✔
645
                } else {
646
                    found = false;
3,124✔
647
                    break;
3,124✔
648
                }
649
            }
650
            if (found) {
4,579✔
651
                // We have an aligned result in result_buffer so we can now
652
                // convert to an integer.
653
                if (size == 1) {
2,910✔
654
                    return *result_buffer;
2,425✔
655
                }
656
                if (size == 2) {
2,544✔
657
                    uint16_t b = *reinterpret_cast<uint16_t*>(result_buffer);
444✔
658
                    if (thread_local_options.big_endian) {
444✔
659
                        b = boost::endian::native_to_big<uint16_t>(b);
12✔
660
                    } else {
661
                        b = boost::endian::native_to_little<uint16_t>(b);
438✔
662
                    }
663
                    return b;
444✔
664
                }
665
                if (size == 4) {
2,100✔
666
                    uint32_t b = *reinterpret_cast<uint32_t*>(result_buffer);
1,346✔
667
                    if (thread_local_options.big_endian) {
1,346✔
668
                        b = boost::endian::native_to_big<uint32_t>(b);
12✔
669
                    } else {
670
                        b = boost::endian::native_to_little<uint32_t>(b);
667✔
671
                    }
672
                    return b;
1,346✔
673
                }
674
                if (size == 8) {
754✔
675
                    Index b = *reinterpret_cast<Index*>(result_buffer);
86✔
676
                    if (thread_local_options.big_endian) {
86✔
677
                        b = boost::endian::native_to_big<Index>(b);
4✔
678
                    } else {
679
                        b = boost::endian::native_to_little<Index>(b);
41✔
680
                    }
681
                    return kind == DataKind::uvalues ? Number(b) : Number(to_signed(b));
134✔
682
                }
683
            }
684
        }
685

686
        const std::vector<Cell> cells = offset_map.get_overlap_cells(o, size);
3,800✔
687
        if (cells.empty()) {
3,800✔
688
            const Cell c = offset_map.mk_cell(o, size);
2,236✔
689
            // Here it's ok to do assignment (instead of expand) because c is not a summarized variable.
690
            // Otherwise, it would be unsound.
691
            return c.get_scalar(kind);
2,236✔
692
        }
693
        CRAB_WARN("Ignored read from cell ", kind, "[", o, "...", o + size - 1, "]", " because it overlaps with ",
1,564✔
694
                  cells.size(), " cells");
695
        /*
696
            TODO: we can apply here "Value Recomposition" a la Mine'06 (https://arxiv.org/pdf/cs/0703074.pdf)
697
                to construct values of some type from a sequence of bytes.
698
                It can be endian-independent but it would more precise if we choose between little- and big-endian.
699
        */
700
    } else {
3,800✔
701
        // TODO: we can be more precise here
702
        CRAB_WARN("array expansion: ignored array load because of non-constant array index ", i);
798✔
703
    }
29,142✔
704
    return {};
1,580✔
705
}
706

707
std::optional<LinearExpression> ArrayDomain::load_type(const Interval& i, int width) const {
11,716✔
708
    if (std::optional<Number> n = i.singleton()) {
11,716✔
709
        offset_map_t& offset_map = lookup_array_map(DataKind::types);
11,716✔
710
        int64_t k = n->narrow<int64_t>();
11,716✔
711
        auto [only_num, only_non_num] = num_bytes.uniformity(k, width);
11,716✔
712
        if (only_num) {
11,716✔
713
            return T_NUM;
9,092✔
714
        }
715
        if (!only_non_num || width != 8) {
2,624✔
UNCOV
716
            return {};
×
717
        }
718
        offset_t o(k);
2,624✔
719
        unsigned size = to_unsigned(width);
2,624✔
720
        if (auto cell = lookup_array_map(DataKind::types).get_cell(o, size)) {
2,624✔
721
            return cell->get_scalar(DataKind::types);
3,930✔
722
        }
723
        std::vector<Cell> cells = offset_map.get_overlap_cells(o, size);
4✔
724
        if (cells.empty()) {
4✔
725
            Cell c = offset_map.mk_cell(o, size);
4✔
726
            // Here it's ok to do assignment (instead of expand) because c is not a summarized variable.
727
            // Otherwise, it would be unsound.
728
            return c.get_scalar(DataKind::types);
4✔
729
        }
730
        CRAB_WARN("Ignored read from cell ", DataKind::types, "[", o, "...", o + size - 1, "]",
×
731
                  " because it overlaps with ", cells.size(), " cells");
732
        /*
733
            TODO: we can apply here "Value Recomposition" a la Mine'06 (https://arxiv.org/pdf/cs/0703074.pdf)
734
                to construct values of some type from a sequence of bytes.
735
                It can be endian-independent but it would more precise if we choose between little- and big-endian.
736
        */
737
    } else {
4✔
738
        // Check whether the kind is uniform across the entire interval.
UNCOV
739
        auto lb = i.lb().number();
×
UNCOV
740
        auto ub = i.ub().number();
×
UNCOV
741
        if (lb.has_value() && ub.has_value()) {
×
UNCOV
742
            Number fullwidth = ub.value() - lb.value() + width;
×
UNCOV
743
            if (lb->fits<uint32_t>() && fullwidth.fits<uint32_t>()) {
×
UNCOV
744
                auto [only_num, only_non_num] =
×
UNCOV
745
                    num_bytes.uniformity(lb->narrow<uint32_t>(), fullwidth.narrow<uint32_t>());
×
UNCOV
746
                if (only_num) {
×
UNCOV
747
                    return T_NUM;
×
748
                }
749
            }
UNCOV
750
        }
×
751
    }
11,716✔
UNCOV
752
    return {};
×
753
}
754

755
// We are about to write to a given range of bytes on the stack.
756
// Any cells covering that range need to be removed, and any cells that only
757
// partially cover that range can be split such that any non-covered portions become new cells.
758
static std::optional<std::pair<offset_t, unsigned>> split_and_find_var(const ArrayDomain& array_domain,
320,998✔
759
                                                                       NumAbsDomain& inv, const DataKind kind,
760
                                                                       const Interval& idx, const Interval& elem_size) {
761
    if (kind == DataKind::svalues || kind == DataKind::uvalues) {
296,684✔
762
        array_domain.split_number_var(inv, kind, idx, elem_size);
68,356✔
763
    }
764
    return kill_and_find_var(inv, kind, idx, elem_size);
296,684✔
765
}
766

767
std::optional<Variable> ArrayDomain::store(NumAbsDomain& inv, const DataKind kind, const Interval& idx,
49,246✔
768
                                           const Interval& elem_size) {
769
    if (auto maybe_cell = split_and_find_var(*this, inv, kind, idx, elem_size)) {
49,246✔
770
        // perform strong update
771
        auto [offset, size] = *maybe_cell;
49,238✔
772
        Variable v = lookup_array_map(kind).mk_cell(offset, size).get_scalar(kind);
49,238✔
773
        return v;
49,238✔
774
    }
775
    return {};
8✔
776
}
777

778
std::optional<Variable> ArrayDomain::store_type(TypeDomain& inv, const Interval& idx, const Interval& width,
24,302✔
779
                                                const bool is_num) {
780
    constexpr auto kind = DataKind::types;
24,302✔
781
    if (auto maybe_cell = split_and_find_var(*this, inv.inv, kind, idx, width)) {
24,302✔
782
        // perform strong update
783
        auto [offset, size] = *maybe_cell;
24,298✔
784
        if (is_num) {
24,298✔
785
            num_bytes.reset(offset, size);
23,740✔
786
        } else {
787
            num_bytes.havoc(offset, size);
558✔
788
        }
789
        Variable v = lookup_array_map(kind).mk_cell(offset, size).get_scalar(kind);
24,298✔
790
        return v;
24,298✔
791
    } else {
792
        using namespace dsl_syntax;
2✔
793
        // havoc the entire range
794
        const auto [lb, ub] = as_numbytes_range(idx, width);
4✔
795
        num_bytes.havoc(lb, ub);
4✔
796
    }
797
    return {};
4✔
798
}
799

800
void ArrayDomain::havoc(NumAbsDomain& inv, const DataKind kind, const Interval& idx, const Interval& elem_size) {
223,124✔
801
    split_and_find_var(*this, inv, kind, idx, elem_size);
223,124✔
802
}
223,124✔
803

804
void ArrayDomain::havoc_type(TypeDomain& inv, const Interval& idx, const Interval& elem_size) {
24,326✔
805
    constexpr auto kind = DataKind::types;
24,326✔
806
    auto maybe_cell = split_and_find_var(*this, inv.inv, kind, idx, elem_size);
24,326✔
807
    if (maybe_cell) {
24,326✔
808
        auto [offset, size] = *maybe_cell;
24,322✔
809
        num_bytes.havoc(offset, size);
24,322✔
810
    }
811
}
24,326✔
812

813
void ArrayDomain::store_numbers(const Interval& _idx, const Interval& _width) {
1,088✔
814

815
    if (is_bottom()) {
1,088✔
UNCOV
816
        return;
×
817
    }
818

819
    const std::optional<Number> idx_n = _idx.singleton();
1,088✔
820
    if (!idx_n) {
1,088✔
UNCOV
821
        CRAB_WARN("array expansion store range ignored because ", "lower bound is not constant");
×
UNCOV
822
        return;
×
823
    }
824

825
    const std::optional<Number> width = _width.singleton();
1,088✔
826
    if (!width) {
1,088✔
UNCOV
827
        CRAB_WARN("array expansion store range ignored because ", "upper bound is not constant");
×
UNCOV
828
        return;
×
829
    }
830

831
    if (*idx_n + *width > EBPF_TOTAL_STACK_SIZE) {
1,088✔
UNCOV
832
        CRAB_WARN("array expansion store range ignored because ",
×
833
                  "the number of elements is larger than default limit of ", EBPF_TOTAL_STACK_SIZE);
UNCOV
834
        return;
×
835
    }
836
    num_bytes.reset(idx_n->narrow<int>(), width->narrow<int>());
1,088✔
837
}
1,088✔
838

UNCOV
839
void ArrayDomain::set_to_top() { num_bytes.set_to_top(); }
×
840

UNCOV
841
void ArrayDomain::set_to_bottom() { num_bytes.set_to_bottom(); }
×
842

843
bool ArrayDomain::is_bottom() const { return num_bytes.is_bottom(); }
9,664✔
844

UNCOV
845
bool ArrayDomain::is_top() const { return num_bytes.is_top(); }
×
846

847
StringInvariant ArrayDomain::to_set() const { return num_bytes.to_set(); }
1,400✔
848

849
bool ArrayDomain::operator<=(const ArrayDomain& other) const { return num_bytes <= other.num_bytes; }
506✔
850

UNCOV
851
bool ArrayDomain::operator==(const ArrayDomain& other) const { return num_bytes == other.num_bytes; }
×
852

853
void ArrayDomain::operator|=(const ArrayDomain& other) {
120✔
854
    if (is_bottom()) {
120✔
855
        *this = other;
856
        return;
857
    }
858
    num_bytes |= other.num_bytes;
120✔
859
}
860

861
void ArrayDomain::operator|=(ArrayDomain&& other) {
17,032✔
862
    if (is_bottom()) {
17,032✔
863
        *this = std::move(other);
864
        return;
865
    }
866
    num_bytes |= std::move(other.num_bytes);
17,032✔
867
}
868

UNCOV
869
ArrayDomain ArrayDomain::operator|(const ArrayDomain& other) const { return ArrayDomain(num_bytes | other.num_bytes); }
×
870

871
ArrayDomain ArrayDomain::operator&(const ArrayDomain& other) const { return ArrayDomain(num_bytes & other.num_bytes); }
44✔
872

873
ArrayDomain ArrayDomain::widen(const ArrayDomain& other) const { return ArrayDomain(num_bytes | other.num_bytes); }
84✔
874

UNCOV
875
ArrayDomain ArrayDomain::narrow(const ArrayDomain& other) const { return ArrayDomain(num_bytes & other.num_bytes); }
×
876

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