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

Alan-Jowett / ebpf-verifier / 22160684311

18 Feb 2026 09:20PM UTC coverage: 88.256% (+1.1%) from 87.142%
22160684311

push

github

elazarg
lint

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

78 of 82 new or added lines in 6 files covered. (95.12%)

402 existing lines in 18 files now uncovered.

10731 of 12159 relevant lines covered (88.26%)

837642.51 hits per line

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

87.23
/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,829✔
35
    unsigned size{};
212,654✔
36

37
    bool operator==(const Cell&) const = default;
103,811✔
38
    auto operator<=>(const Cell&) const = default;
322,304✔
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,206,334✔
44
        assert(sz > 0 && "overlap query with zero width");
3,206,334✔
45
        return offset < o + sz && o < offset + size;
3,121,494✔
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

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,554✔
64
    return variable_registry->cell_var(kind, c.offset, c.size);
141,554✔
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,899✔
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,798✔
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,150✔
105

106
    void operator-=(const std::vector<Cell>& cells) {
13,568✔
107
        for (const auto& c : cells) {
33,718✔
108
            this->operator-=(c);
20,150✔
109
        }
110
    }
13,568✔
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,150✔
141
    const offset_t key = c.offset;
20,150✔
142
    if (auto it = _map.find(key); it != _map.end()) {
20,150✔
143
        it->second.erase(c);
20,150✔
144
        if (it->second.empty()) {
20,150✔
145
            _map.erase(it);
19,864✔
146
        }
147
    }
148
}
20,150✔
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,888✔
174

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

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

187
    if (const auto maybe_c = get_cell(o, size)) {
77,148✔
188
        return *maybe_c;
32,260✔
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,888✔
193
    insert_cell(c);
44,888✔
194
    return c;
44,888✔
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) {
394,800✔
199
    std::vector<Cell> out;
394,800✔
200
    const Cell query_cell(o, size);
394,800✔
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,616,100✔
207
        --it;
3,023,900✔
208
        for (const Cell& x : it->second) {
6,048,150✔
209
            if (x.overlap(o, size) && x != query_cell) {
4,589,368✔
210
                out.push_back(x);
24,454✔
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) {
604,604✔
220
        bool any_overlap = false;
182,084✔
221
        for (const Cell& x : it->second) {
364,168✔
222
            if (x.overlap(o, size)) {
188,286✔
223
                out.push_back(x);
12,404✔
224
                any_overlap = true;
6,202✔
225
            }
226
        }
227
        if (!any_overlap) {
182,084✔
228
            break;
84,840✔
229
        }
230
    }
231

232
    return out;
592,200✔
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;
272,292✔
239

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

242
static offset_map_t& lookup_array_map(const DataKind kind) { return (*thread_local_array_map)[kind]; }
540,342✔
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 {
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,696✔
287
                                   const Interval& elem_size) const {
288
    assert(kind == DataKind::svalues || kind == DataKind::uvalues);
68,696✔
289
    offset_map_t& offset_map = lookup_array_map(kind);
68,696✔
290
    const std::optional<Number> n = ii.singleton();
68,696✔
291
    if (!n) {
68,696✔
292
        // We can only split a singleton offset.
293
        return;
16✔
294
    }
295
    const std::optional<Number> n_bytes = elem_size.singleton();
68,680✔
296
    if (!n_bytes) {
68,680✔
297
        // We can only split a singleton size.
298
        return;
299
    }
300
    const auto size = n_bytes->narrow<unsigned int>();
68,680✔
301
    const offset_t o(n->narrow<Index>());
68,680✔
302

303
    const std::vector<Cell> cells = offset_map.get_overlap_cells(o, size);
68,680✔
304
    for (const Cell& c : cells) {
83,272✔
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,680✔
327

328
// we can only treat this as non-member because we use global state
329
// Find overlapping cells for the given index range and kill (havoc + remove) them.
330
// Returns the exact offset and size if the index and element size are both constant.
331
template <typename HavocFn>
332
static std::optional<std::pair<offset_t, unsigned>> kill_and_find_var(const HavocFn& havoc_var, DataKind kind,
322,332✔
333
                                                                      const Interval& ii, const Interval& elem_size) {
334
    std::optional<std::pair<offset_t, unsigned>> res;
322,332✔
335

336
    offset_map_t& offset_map = lookup_array_map(kind);
322,332✔
337
    std::vector<Cell> cells;
322,332✔
338
    if (const std::optional<Number> n = ii.singleton()) {
322,332✔
339
        if (const auto n_bytes = elem_size.singleton()) {
322,276✔
340
            auto size = n_bytes->narrow<unsigned int>();
322,276✔
341
            // -- Constant index: kill overlapping cells
342
            offset_t o(n->narrow<Index>());
322,276✔
343
            cells = offset_map.get_overlap_cells(o, size);
483,414✔
344
            res = std::make_pair(o, size);
322,276✔
345
        }
346
    }
347
    if (!res) {
322,332✔
348
        // -- Non-constant index: kill overlapping cells
349
        cells = offset_map.get_overlap_cells_symbolic_offset(ii | (ii + elem_size));
84✔
350
    }
351
    if (!cells.empty()) {
322,332✔
352
        // Forget the scalars from the relevant domain
353
        for (const auto& c : cells) {
33,718✔
354
            havoc_var(cell_var(kind, c));
20,150✔
355

356
            // Forget signed and unsigned values together.
357
            if (kind == DataKind::svalues) {
20,150✔
358
                havoc_var(cell_var(DataKind::uvalues, c));
11,313✔
359
            } else if (kind == DataKind::uvalues) {
12,608✔
360
                havoc_var(cell_var(DataKind::svalues, c));
10,671✔
361
            }
362
        }
363
        // Remove the cells. If needed again they will be re-created.
364
        offset_map -= cells;
13,568✔
365
    }
366
    return res;
483,498✔
367
}
322,332✔
368
static std::tuple<int, int> as_numbytes_range(const Interval& index, const Interval& width) {
4,154✔
369
    return (index | (index + width)).bound(0, EBPF_TOTAL_STACK_SIZE);
4,154✔
370
}
371

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

380
bool ArrayDomain::all_num_width(const Interval& index, const Interval& width) const {
4,150✔
381
    const auto [min_lb, max_ub] = as_numbytes_range(index, width);
4,150✔
382
    assert(min_lb <= max_ub);
4,150✔
383
    return this->num_bytes.all_num(min_lb, max_ub);
6,225✔
384
}
385

386
// Get the number of bytes, starting at offset, that are known to be numbers.
387
int ArrayDomain::min_all_num_size(const NumAbsDomain& inv, const Variable offset) const {
10,550✔
388
    const auto min_lb = inv.eval_interval(offset).lb().number();
15,825✔
389
    const auto max_ub = inv.eval_interval(offset).ub().number();
15,825✔
390
    if (!min_lb || !max_ub || !min_lb->fits<int32_t>() || !max_ub->fits<int32_t>()) {
10,550✔
391
        return 0;
392
    }
393
    const auto lb = min_lb->narrow<int>();
10,550✔
394
    const auto ub = max_ub->narrow<int>();
10,550✔
395
    return std::max(0, this->num_bytes.all_num_width(lb) - (ub - lb));
14,873✔
396
}
397

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

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

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

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

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

576
// We are about to write to a given range of bytes on the stack.
577
// Any cells covering that range need to be removed, and any cells that only
578
// partially cover that range can be split such that any non-covered portions become new cells.
579
static std::optional<std::pair<offset_t, unsigned>> split_and_find_var(const ArrayDomain& array_domain,
273,504✔
580
                                                                       NumAbsDomain& inv, const DataKind kind,
581
                                                                       const Interval& idx, const Interval& elem_size) {
582
    if (kind == DataKind::svalues || kind == DataKind::uvalues) {
273,504✔
583
        array_domain.split_number_var(inv, kind, idx, elem_size);
68,696✔
584
    }
585
    return kill_and_find_var([&inv](Variable v) { inv.havoc(v); }, kind, idx, elem_size);
302,900✔
586
}
587

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

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

623
void ArrayDomain::havoc(NumAbsDomain& inv, const DataKind kind, const Interval& idx, const Interval& elem_size) {
224,064✔
624
    split_and_find_var(*this, inv, kind, idx, elem_size);
224,064✔
625
}
224,064✔
626

627
void ArrayDomain::havoc_type(TypeDomain& inv, const Interval& idx, const Interval& elem_size) {
24,426✔
628
    constexpr auto kind = DataKind::types;
24,426✔
629
    if (auto maybe_cell = kill_and_find_var([&inv](Variable v) { inv.havoc_type(v); }, kind, idx, elem_size)) {
29,836✔
630
        auto [offset, size] = *maybe_cell;
24,422✔
631
        num_bytes.havoc(offset, size);
24,422✔
632
    }
633
}
24,426✔
634

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

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

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

656
void ArrayDomain::set_to_top() { num_bytes.set_to_top(); }
8✔
657

UNCOV
658
void ArrayDomain::set_to_bottom() { num_bytes.set_to_bottom(); }
×
659

UNCOV
660
bool ArrayDomain::is_bottom() const { return num_bytes.is_bottom(); }
×
661

UNCOV
662
bool ArrayDomain::is_top() const { return num_bytes.is_top(); }
×
663

664
StringInvariant ArrayDomain::to_set() const { return num_bytes.to_set(); }
1,432✔
665

666
bool ArrayDomain::operator<=(const ArrayDomain& other) const { return num_bytes <= other.num_bytes; }
560✔
667

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

670
void ArrayDomain::operator|=(const ArrayDomain& other) { num_bytes |= other.num_bytes; }
130✔
671

672
void ArrayDomain::operator|=(ArrayDomain&& other) { num_bytes |= std::move(other.num_bytes); }
17,122✔
673

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

676
ArrayDomain ArrayDomain::operator&(const ArrayDomain& other) const { return ArrayDomain(num_bytes & other.num_bytes); }
56✔
677

678
ArrayDomain ArrayDomain::widen(const ArrayDomain& other) const { return ArrayDomain(num_bytes | other.num_bytes); }
108✔
679

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

682
std::ostream& operator<<(std::ostream& o, const ArrayDomain& dom) { return o << dom.num_bytes; }
6✔
683
} // 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