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

Alan-Jowett / ebpf-verifier / 29279923487

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

push

github

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

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

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

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

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

9313 of 10748 relevant lines covered (86.65%)

6391896.2 hits per line

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

85.34
/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{};
11,299,995✔
35
    unsigned size{};
11,288,994✔
36

37
    bool operator==(const Cell&) const = default;
335,497✔
38
    auto operator<=>(const Cell&) const = default;
21,995,891✔
39

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

49
static Interval cell_to_interval(const offset_t o, const unsigned size) {
68,460✔
50
    const Number lb{gsl::narrow<int>(o)};
68,460✔
51
    return {lb, lb + size - 1};
102,690✔
52
}
53

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

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

62
static Variable cell_var(const DataKind kind, const Cell& c) {
551,362✔
63
    return variable_registry.cell_var(kind, c.offset, c.size);
551,362✔
64
}
65

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

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

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

79
    map_t _map;
80

81
    void remove_cell(const Cell& c);
82

83
    void insert_cell(const Cell& c);
84

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

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

90
  public:
91
    offset_map_t() = default;
30,928✔
92

93
    [[nodiscard]]
94
    std::size_t size() const {
95
        return _map.size();
96
    }
97

98
    void operator-=(const Cell& c) { remove_cell(c); }
94,124✔
99

100
    void operator-=(const std::vector<Cell>& cells) {
54,580✔
101
        for (const auto& c : cells) {
148,704✔
102
            this->operator-=(c);
94,124✔
103
        }
104
    }
54,580✔
105

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

109
    [[nodiscard]]
110
    std::vector<Cell> get_overlap_cells_symbolic_offset(const Interval& range);
111

112
    friend std::ostream& operator<<(std::ostream& o, offset_map_t& m);
113
};
114

115
void offset_map_t::remove_cell(const Cell& c) {
94,124✔
116
    const offset_t key = c.offset;
94,124✔
117
    if (const auto it = _map.find(key); it != _map.end()) {
94,124✔
118
        it->second.erase(c);
94,124✔
119
        if (it->second.empty()) {
94,124✔
120
            _map.erase(it);
68,430✔
121
        }
122
    }
123
}
94,124✔
124

125
[[nodiscard]]
126
std::vector<Cell> offset_map_t::get_overlap_cells_symbolic_offset(const Interval& range) {
186✔
127
    std::vector<Cell> out;
186✔
128
    for (const auto& o_cells : _map | std::views::values) {
294✔
129
        // All cells in o_cells have the same offset. They only differ in the size.
130
        // If the largest cell overlaps with [offset, offset + size)
131
        // then the rest of cells are considered to overlap.
132
        // This is an over-approximation because [offset, offset+size) can overlap
133
        // with the largest cell, but it doesn't necessarily overlap with smaller cells.
134
        // For efficiency, we assume it overlaps with all.
135
        if (!o_cells.empty()) {
108✔
136
            // Cells are sorted by (offset, size); last element has the largest size.
137
            const Cell& largest_cell = *o_cells.rbegin();
108✔
138
            if (symbolic_overlap(largest_cell, range)) {
108✔
139
                for (const auto& c : o_cells) {
126✔
140
                    out.push_back(c);
66✔
141
                }
142
            }
143
        }
144
    }
145
    return out;
186✔
146
}
×
147

148
void offset_map_t::insert_cell(const Cell& c) { _map[c.offset].insert(c); }
5,133,140✔
149

150
std::optional<Cell> offset_map_t::get_cell(const offset_t o, const unsigned size) {
398,954✔
151
    if (const auto it = _map.find(o); it != _map.end()) {
398,954✔
152
        if (const auto cit = it->second.find(Cell(o, size)); cit != it->second.end()) {
253,420✔
153
            return *cit;
246,762✔
154
        }
155
    }
156
    return {};
152,192✔
157
}
158

159
Cell offset_map_t::mk_cell(const offset_t o, const unsigned size) {
216,828✔
160
    // TODO: check array is the array associated to this offset map
161

162
    if (const auto maybe_c = get_cell(o, size)) {
216,828✔
163
        return *maybe_c;
88,526✔
164
    }
165
    // create a new scalar variable for representing the contents
166
    // of bytes array[o,o+1,..., o+size-1]
167
    const Cell c(o, size);
128,302✔
168
    insert_cell(c);
128,302✔
169
    return c;
128,302✔
170
}
171

172
// Return all cells that might overlap with (o, size).
173
std::vector<Cell> offset_map_t::get_overlap_cells(const offset_t o, const unsigned size) {
1,450,000✔
174
    std::vector<Cell> out;
1,450,000✔
175
    const Cell query_cell(o, size);
1,450,000✔
176

177
    // Search backwards: cells at offsets <= o that might extend into [o, o+size).
178
    // We cannot break early: a bucket with small cells may not overlap while an
179
    // earlier bucket with larger cells does (e.g., Cell(48,8) overlaps [54,56)
180
    // but Cell(50,2) does not). The map is tiny (~3 entries) so this is fine.
181
    for (auto it = _map.upper_bound(o); it != _map.begin();) {
10,737,200✔
182
        --it;
8,562,200✔
183
        for (const Cell& x : it->second) {
17,721,022✔
184
            if (x.overlap(o, size) && x != query_cell) {
13,911,482✔
185
                out.push_back(x);
112,294✔
186
            }
187
        }
188
    }
189

190
    // Search forwards: cells at offsets > o that start within [o, o+size).
191
    // Early break is safe here: if no cell at offset k overlaps, then k >= o + size,
192
    // and all subsequent offsets are even larger, so they cannot overlap either.
193
    // No duplicates: backward and forward scans visit disjoint key ranges.
194
    for (auto it = _map.upper_bound(o); it != _map.end(); ++it) {
2,230,592✔
195
        bool any_overlap = false;
535,284✔
196
        for (const Cell& x : it->second) {
1,099,852✔
197
            if (x.overlap(o, size)) {
593,686✔
198
                out.push_back(x);
58,236✔
199
                any_overlap = true;
29,118✔
200
            }
201
        }
202
        if (!any_overlap) {
535,284✔
203
            break;
239,846✔
204
        }
205
    }
206

207
    return out;
2,175,000✔
208
}
×
209

210
// Per-domain registry of the stack cells `ArrayDomain` is currently tracking,
211
// keyed by DataKind. Owned by `ArrayDomain`; copied/merged alongside the domain.
212
class StackCellRegistry final {
157,654✔
213
    std::unordered_map<DataKind, offset_map_t> _maps;
214

215
  public:
216
    offset_map_t& get(const DataKind kind) { return _maps[kind]; }
2,785,128✔
217

218
    void merge_from(const StackCellRegistry& other) {
65,066✔
219
        if (this == &other) {
65,066✔
220
            return;
221
        }
222
        for (const auto& [kind, omap] : other._maps) {
1,465,151✔
223
            offset_map_t& dst = _maps[kind];
933,390✔
224
            for (const auto& [_off, cell_set] : omap._map) {
5,577,554✔
225
                for (const Cell& c : cell_set) {
9,649,002✔
226
                    dst.insert_cell(c);
5,004,838✔
227
                }
228
            }
229
        }
230
    }
231
};
232

233
std::shared_ptr<StackCellRegistry> make_stack_cell_registry() { return std::make_shared<StackCellRegistry>(); }
10,124✔
234

235
void ArrayDomain::initialize_numbers(const int lb, const int width) {
300✔
236
    num_bytes.reset(lb, width);
300✔
237
    cells_.get_mutable().get(DataKind::svalues).mk_cell(offset_t{gsl::narrow_cast<Index>(lb)}, width);
300✔
238
}
300✔
239

240
std::ostream& operator<<(std::ostream& o, offset_map_t& m) {
×
241
    if (m._map.empty()) {
×
242
        o << "empty";
×
243
    } else {
244
        for (const auto& cells : m._map | std::views::values) {
×
245
            o << "{";
×
246
            for (auto cit = cells.begin(), cet = cells.end(); cit != cet;) {
×
247
                o << *cit;
×
248
                ++cit;
×
249
                if (cit != cet) {
×
250
                    o << ",";
×
251
                }
252
            }
253
            o << "}\n";
×
254
        }
255
    }
256
    return o;
×
257
}
258

259
// Create a new cell that is a subset of an existing cell.
260
void ArrayDomain::split_cell(NumAbsDomain& inv, const DataKind kind, const int cell_start_index, const unsigned int len,
3,130✔
261
                             const bool big_endian) {
262
    assert(kind == DataKind::svalues || kind == DataKind::uvalues);
3,130✔
263

264
    // Get the values from the indicated stack range.
265
    const std::optional<LinearExpression> svalue =
1,565✔
266
        load(inv, DataKind::svalues, Interval{cell_start_index}, len, big_endian);
3,130✔
267
    const std::optional<LinearExpression> uvalue =
1,565✔
268
        load(inv, DataKind::uvalues, Interval{cell_start_index}, len, big_endian);
3,130✔
269

270
    // Create a new cell for that range.
271
    offset_map_t& offset_map = cells_.get_mutable().get(kind);
3,130✔
272
    const Cell new_cell = offset_map.mk_cell(offset_t{gsl::narrow_cast<Index>(cell_start_index)}, len);
3,130✔
273
    inv.assign(cell_var(DataKind::svalues, new_cell), svalue);
3,130✔
274
    inv.assign(cell_var(DataKind::uvalues, new_cell), uvalue);
4,695✔
275
}
3,130✔
276

277
// Prepare to havoc bytes in the middle of a cell by potentially splitting the cell if it is numeric,
278
// into the part to the left of the havoced portion, and the part to the right of the havoced portion.
279
void ArrayDomain::split_number_var(NumAbsDomain& inv, const DataKind kind, const Interval& ii,
200,828✔
280
                                   const Interval& elem_size, const bool big_endian) {
281
    assert(kind == DataKind::svalues || kind == DataKind::uvalues);
200,828✔
282
    offset_map_t& offset_map = cells_.get_mutable().get(kind);
200,828✔
283
    const std::optional<Number> n = ii.singleton();
200,828✔
284
    if (!n) {
200,828✔
285
        // We can only split a singleton offset.
286
        return;
40✔
287
    }
288
    const std::optional<Number> n_bytes = elem_size.singleton();
200,788✔
289
    if (!n_bytes) {
200,788✔
290
        // We can only split a singleton size.
291
        return;
292
    }
293
    const auto size = n_bytes->narrow<unsigned int>();
200,788✔
294
    const offset_t o(n->narrow<Index>());
200,788✔
295

296
    const std::vector<Cell> overlaps = offset_map.get_overlap_cells(o, size);
200,788✔
297
    for (const Cell& c : overlaps) {
269,140✔
298
        const auto [cell_start_index, cell_end_index] = cell_to_interval(c.offset, c.size).pair<int>();
68,352✔
299
        if (!this->num_bytes.all_num(cell_start_index, cell_end_index + 1) ||
98,558✔
300
            cell_end_index + 1UL < cell_start_index + sizeof(int64_t)) {
60,412✔
301
            // We can only split numeric cells of size 8 or less.
302
            continue;
65,430✔
303
        }
304

305
        if (!inv.eval_interval(cell_var(kind, c)).is_singleton()) {
16,419✔
306
            // We can only split cells with a singleton value.
307
            continue;
8,024✔
308
        }
309
        if (gsl::narrow_cast<Index>(cell_start_index) < o) {
2,922✔
310
            // Use the bytes to the left of the specified range.
311
            split_cell(inv, kind, cell_start_index, gsl::narrow<unsigned int>(o - cell_start_index), big_endian);
908✔
312
        }
313
        if (o + size < cell_end_index + 1UL) {
2,922✔
314
            // Use the bytes to the right of the specified range.
315
            split_cell(inv, kind, gsl::narrow<int>(o + size),
2,222✔
316
                       gsl::narrow<unsigned int>(cell_end_index - (o + size - 1)), big_endian);
2,222✔
317
        }
318
    }
319
}
200,788✔
320

321
// Find overlapping cells for the given index range and kill (havoc + remove) them.
322
// Returns the exact offset and size if the index and element size are both constant.
323
template <typename HavocFn>
324
static std::optional<std::pair<offset_t, unsigned>> kill_and_find_var(StackCellRegistry& cells,
1,233,758✔
325
                                                                      const HavocFn& havoc_var, const DataKind kind,
326
                                                                      const Interval& ii, const Interval& elem_size) {
327
    std::optional<std::pair<offset_t, unsigned>> res;
1,233,758✔
328

329
    offset_map_t& offset_map = cells.get(kind);
1,233,758✔
330
    std::vector<Cell> overlaps;
1,233,758✔
331
    if (const std::optional<Number> n = ii.singleton()) {
1,233,758✔
332
        if (const auto n_bytes = elem_size.singleton()) {
1,233,572✔
333
            auto size = n_bytes->narrow<unsigned int>();
1,233,572✔
334
            // -- Constant index: kill overlapping cells
335
            offset_t o(n->narrow<Index>());
1,233,572✔
336
            overlaps = offset_map.get_overlap_cells(o, size);
1,850,358✔
337
            res = std::make_pair(o, size);
1,233,572✔
338
        }
339
    }
340
    if (!res) {
1,233,758✔
341
        // -- Non-constant index: kill overlapping cells
342
        overlaps = offset_map.get_overlap_cells_symbolic_offset(ii | (ii + elem_size));
279✔
343
    }
344
    if (!overlaps.empty()) {
1,233,758✔
345
        // Forget the scalars from the relevant domain
346
        for (const auto& c : overlaps) {
148,704✔
347
            havoc_var(cell_var(kind, c));
94,124✔
348

349
            // Forget signed and unsigned values together.
350
            if (kind == DataKind::svalues) {
94,124✔
351
                havoc_var(cell_var(DataKind::uvalues, c));
35,048✔
352
            } else if (kind == DataKind::uvalues) {
59,076✔
353
                havoc_var(cell_var(DataKind::svalues, c));
33,350✔
354
            }
355
        }
356
        // Remove the cells. If needed again they will be re-created.
357
        offset_map -= overlaps;
54,580✔
358
    }
359
    return res;
1,850,637✔
360
}
1,233,758✔
361
static std::optional<std::tuple<int, int>> as_numbytes_range(const Interval& range, const int stack_size) {
19,978✔
362
    const Interval bounded_range = Interval{0, stack_size} & range;
19,978✔
363
    if (bounded_range.is_bottom()) {
19,978✔
364
        return {};
8✔
365
    }
366
    const auto bounds = bounded_range.pair<int>();
19,970✔
367
    const auto [lb, ub] = bounds;
19,970✔
368
    if (lb >= ub) {
19,970✔
369
        return {};
8✔
370
    }
371
    return bounds;
19,962✔
372
}
373

374
static std::optional<std::tuple<int, int>> as_numbytes_range(const Interval& index, const Interval& width,
13,584✔
375
                                                             const int stack_size) {
376
    const Interval range = index | (index + width);
13,584✔
377
    return as_numbytes_range(range, stack_size);
20,376✔
378
}
379

380
bool ArrayDomain::all_num_lb_ub(const Interval& lb, const Interval& ub) const {
6,394✔
381
    const auto range = as_numbytes_range(lb | ub, total_stack_size());
6,394✔
382
    if (!range.has_value()) {
6,394✔
383
        return false;
3✔
384
    }
385
    const auto [min_lb, max_ub] = *range;
6,388✔
386
    assert(min_lb < max_ub);
6,388✔
387
    return this->num_bytes.all_num(min_lb, max_ub);
6,388✔
388
}
389

390
bool ArrayDomain::all_num_width(const Interval& index, const Interval& width) const {
13,572✔
391
    const auto range = as_numbytes_range(index, width, total_stack_size());
13,572✔
392
    if (!range.has_value()) {
13,572✔
393
        return false;
5✔
394
    }
395
    const auto [min_lb, max_ub] = *range;
13,562✔
396
    assert(min_lb < max_ub);
13,562✔
397
    return this->num_bytes.all_num(min_lb, max_ub);
13,562✔
398
}
399

400
// Get the number of bytes, starting at offset, that are known to be numbers.
401
int ArrayDomain::min_all_num_size(const NumAbsDomain& inv, const Variable offset) const {
30,570✔
402
    const auto min_lb = inv.eval_interval(offset).lb().number();
45,855✔
403
    const auto max_ub = inv.eval_interval(offset).ub().number();
45,855✔
404
    if (!min_lb || !max_ub || !min_lb->fits<int32_t>() || !max_ub->fits<int32_t>()) {
30,570✔
405
        return 0;
50✔
406
    }
407
    const auto lb = min_lb->narrow<int>();
30,470✔
408
    const auto ub = max_ub->narrow<int>();
30,470✔
409
    return std::max(0, this->num_bytes.all_num_width(lb) - (ub - lb));
43,693✔
410
}
411

412
// Get one byte of a value.
413
std::optional<uint8_t> get_value_byte(const NumAbsDomain& inv, const offset_t o, const int width,
95,876✔
414
                                      const bool big_endian) {
415
    const Variable v = variable_registry.cell_var(DataKind::svalues, (o / width) * width, width);
95,876✔
416
    const std::optional<Number> t = inv.eval_interval(v).singleton();
95,876✔
417
    if (!t) {
95,876✔
418
        return {};
57,238✔
419
    }
420
    Index n = t->cast_to<Index>();
38,638✔
421

422
    // Convert value to bytes of the appropriate endian-ness.
423
    switch (width) {
38,638✔
424
    case sizeof(uint8_t): break;
130✔
425
    case sizeof(uint16_t):
278✔
426
        if (big_endian) {
278✔
427
            n = boost::endian::native_to_big<uint16_t>(n);
×
428
        } else {
429
            n = boost::endian::native_to_little<uint16_t>(n);
278✔
430
        }
431
        break;
139✔
432
    case sizeof(uint32_t):
1,934✔
433
        if (big_endian) {
1,934✔
434
            n = boost::endian::native_to_big<uint32_t>(n);
×
435
        } else {
436
            n = boost::endian::native_to_little<uint32_t>(n);
1,934✔
437
        }
438
        break;
967✔
439
    case sizeof(Index):
36,166✔
440
        if (big_endian) {
36,166✔
441
            n = boost::endian::native_to_big<Index>(n);
48✔
442
        } else {
443
            n = boost::endian::native_to_little<Index>(n);
18,059✔
444
        }
445
        break;
18,083✔
446
    default: CRAB_ERROR("Unexpected width ", width);
×
447
    }
448
    const auto bytes = reinterpret_cast<uint8_t*>(&n);
38,638✔
449
    return bytes[o % width];
38,638✔
450
}
451

452
std::optional<LinearExpression> ArrayDomain::load(const NumAbsDomain& inv, const DataKind kind, const Interval& i,
165,218✔
453
                                                  const int width, const bool big_endian) {
454
    if (const std::optional<Number> n = i.singleton()) {
165,218✔
455
        offset_map_t& offset_map = cells_.get_mutable().get(kind);
165,182✔
456
        const int64_t k = n->narrow<int64_t>();
165,182✔
457
        const offset_t o(k);
165,182✔
458
        const unsigned size = to_unsigned(width);
165,182✔
459
        if (const auto cell = offset_map.get_cell(o, size)) {
165,182✔
460
            return cell_var(kind, *cell);
211,956✔
461
        }
462
        if (kind == DataKind::svalues || kind == DataKind::uvalues) {
23,878✔
463
            // Copy bytes into result_buffer, taking into account that the
464
            // bytes might be in different stack variables and might be unaligned.
465
            uint8_t result_buffer[8];
466
            bool found = true;
43,197✔
467
            for (unsigned int index = 0; index < size; index++) {
62,516✔
468
                const offset_t byte_offset{o + index};
52,130✔
469
                std::optional<uint8_t> b = get_value_byte(inv, byte_offset, 8, big_endian);
52,130✔
470
                if (!b) {
52,130✔
471
                    b = get_value_byte(inv, byte_offset, 4, big_endian);
15,964✔
472
                    if (!b) {
15,964✔
473
                        b = get_value_byte(inv, byte_offset, 2, big_endian);
14,030✔
474
                        if (!b) {
14,030✔
475
                            b = get_value_byte(inv, byte_offset, 1, big_endian);
13,752✔
476
                        }
477
                    }
478
                }
479
                if (b) {
52,130✔
480
                    result_buffer[index] = *b;
38,638✔
481
                } else {
482
                    found = false;
13,492✔
483
                    break;
13,492✔
484
                }
485
            }
486
            if (found) {
18,685✔
487
                // We have an aligned result in result_buffer so we can now
488
                // convert to an integer.
489
                if (size == 1) {
10,386✔
490
                    return *result_buffer;
8,779✔
491
                }
492
                if (size == 2) {
9,328✔
493
                    uint16_t b = *reinterpret_cast<uint16_t*>(result_buffer);
2,474✔
494
                    if (big_endian) {
2,474✔
495
                        b = boost::endian::native_to_big<uint16_t>(b);
14✔
496
                    } else {
497
                        b = boost::endian::native_to_little<uint16_t>(b);
2,467✔
498
                    }
499
                    return b;
2,474✔
500
                }
501
                if (size == 4) {
6,854✔
502
                    uint32_t b = *reinterpret_cast<uint32_t*>(result_buffer);
4,602✔
503
                    if (big_endian) {
4,602✔
504
                        b = boost::endian::native_to_big<uint32_t>(b);
14✔
505
                    } else {
506
                        b = boost::endian::native_to_little<uint32_t>(b);
2,294✔
507
                    }
508
                    return b;
4,602✔
509
                }
510
                if (size == 8) {
2,252✔
511
                    Index b = *reinterpret_cast<Index*>(result_buffer);
116✔
512
                    if (big_endian) {
116✔
513
                        b = boost::endian::native_to_big<Index>(b);
6✔
514
                    } else {
515
                        b = boost::endian::native_to_little<Index>(b);
55✔
516
                    }
517
                    return kind == DataKind::uvalues ? Number(b) : Number(to_signed(b));
116✔
518
                }
519
            }
520
        }
521

522
        const std::vector<Cell> overlaps = offset_map.get_overlap_cells(o, size);
15,628✔
523
        if (overlaps.empty()) {
15,628✔
524
            const Cell c = offset_map.mk_cell(o, size);
8,656✔
525
            // Here it's ok to do assignment (instead of expand) because c is not a summarized variable.
526
            // Otherwise, it would be unsound.
527
            return cell_var(kind, c);
8,656✔
528
        }
529
        CRAB_WARN("Ignored read from cell ", kind, "[", o, "...", o + size - 1, "]", " because it overlaps with ",
6,972✔
530
                  overlaps.size(), " cells");
531
        /*
532
            TODO: we can apply here "Value Recomposition" a la Mine'06 (https://arxiv.org/pdf/cs/0703074.pdf)
533
                to construct values of some type from a sequence of bytes.
534
                It can be endian-independent but it would more precise if we choose between little- and big-endian.
535
        */
536
    } else {
15,628✔
537
        // TODO: we can be more precise here
538
        CRAB_WARN("array expansion: ignored array load because of non-constant array index ", i);
36✔
539
    }
540
    return {};
7,008✔
541
}
542

543
std::optional<LinearExpression> ArrayDomain::load_type(const Interval& i, const int width) {
48,824✔
544
    if (const std::optional<Number> n = i.singleton()) {
48,824✔
545
        offset_map_t& offset_map = cells_.get_mutable().get(DataKind::types);
48,824✔
546
        const int64_t k = n->narrow<int64_t>();
48,824✔
547
        auto [only_num, only_non_num] = num_bytes.uniformity(k, width);
48,824✔
548
        if (only_num) {
48,824✔
549
            return T_NUM;
31,866✔
550
        }
551
        if (!only_non_num || width != 8) {
16,958✔
552
            return {};
14✔
553
        }
554
        const offset_t o(k);
16,944✔
555
        const unsigned size = to_unsigned(width);
16,944✔
556
        if (const auto cell = offset_map.get_cell(o, size)) {
16,944✔
557
            return cell_var(DataKind::types, *cell);
25,398✔
558
        }
559
        const std::vector<Cell> overlaps = offset_map.get_overlap_cells(o, size);
12✔
560
        if (overlaps.empty()) {
12✔
561
            const Cell c = offset_map.mk_cell(o, size);
12✔
562
            // Here it's ok to do assignment (instead of expand) because c is not a summarized variable.
563
            // Otherwise, it would be unsound.
564
            return cell_var(DataKind::types, c);
12✔
565
        }
566
        CRAB_WARN("Ignored read from cell ", DataKind::types, "[", o, "...", o + size - 1, "]",
×
567
                  " because it overlaps with ", overlaps.size(), " cells");
568
        /*
569
            TODO: we can apply here "Value Recomposition" a la Mine'06 (https://arxiv.org/pdf/cs/0703074.pdf)
570
                to construct values of some type from a sequence of bytes.
571
                It can be endian-independent but it would more precise if we choose between little- and big-endian.
572
        */
573
    } else {
12✔
574
        // Check whether the kind is uniform across the entire interval.
575
        const auto lb = i.lb().number();
×
576
        const auto ub = i.ub().number();
×
577
        if (lb.has_value() && ub.has_value()) {
×
578
            const Number fullwidth = ub.value() - lb.value() + width;
×
579
            if (lb->fits<uint32_t>() && fullwidth.fits<uint32_t>()) {
×
580
                auto [only_num, only_non_num] =
×
581
                    num_bytes.uniformity(lb->narrow<uint32_t>(), fullwidth.narrow<uint32_t>());
×
582
                if (only_num) {
×
583
                    return T_NUM;
×
584
                }
585
            }
586
        }
587
    }
588
    return {};
×
589
}
590

591
// We are about to write to a given range of bytes on the stack.
592
// Any cells covering that range need to be removed, and any cells that only
593
// partially cover that range can be split such that any non-covered portions become new cells.
594
static std::optional<std::pair<offset_t, unsigned>>
595
split_and_find_var(ArrayDomain& array_domain, StackCellRegistry& cells, NumAbsDomain& inv, const DataKind kind,
1,099,774✔
596
                   const Interval& idx, const Interval& elem_size, const bool big_endian) {
597
    if (kind == DataKind::svalues || kind == DataKind::uvalues) {
1,099,774✔
598
        array_domain.split_number_var(inv, kind, idx, elem_size, big_endian);
200,828✔
599
    }
600
    return kill_and_find_var(cells, [&inv](const Variable v) { inv.havoc(v); }, kind, idx, elem_size);
1,237,434✔
601
}
602

603
std::optional<Variable> ArrayDomain::store(NumAbsDomain& inv, const DataKind kind, const Interval& idx,
137,862✔
604
                                           const Interval& elem_size, const bool big_endian) {
605
    if (auto maybe_cell = split_and_find_var(*this, cells_.get_mutable(), inv, kind, idx, elem_size, big_endian)) {
137,862✔
606
        // perform strong update
607
        auto [offset, size] = *maybe_cell;
137,838✔
608
        const Cell c = cells_.get_mutable().get(kind).mk_cell(offset, size);
137,838✔
609
        Variable v = cell_var(kind, c);
137,838✔
610
        return v;
137,838✔
611
    }
612
    return {};
24✔
613
}
614

615
std::optional<Variable> ArrayDomain::store_type(TypeDomain& inv, const Interval& idx, const Interval& width,
66,904✔
616
                                                const bool is_num) {
617
    constexpr auto kind = DataKind::types;
66,904✔
618
    if (auto maybe_cell = kill_and_find_var(
100,356✔
619
            cells_.get_mutable(), [&inv](const Variable v) { inv.havoc_type(v); }, kind, idx, width)) {
133,808✔
620
        // perform strong update
621
        auto [offset, size] = *maybe_cell;
66,892✔
622
        if (is_num) {
66,892✔
623
            num_bytes.reset(offset, size);
64,252✔
624
        } else {
625
            num_bytes.havoc(offset, size);
2,640✔
626
        }
627
        const Cell c = cells_.get_mutable().get(kind).mk_cell(offset, size);
66,892✔
628
        Variable v = cell_var(kind, c);
66,892✔
629
        return v;
66,892✔
630
    } else {
631
        using namespace dsl_syntax;
6✔
632
        // Weak update: cannot perform a strong update because the index is
633
        // not a singleton. Havoc the type cells in the range.
634
        const auto range = as_numbytes_range(idx, width, total_stack_size());
12✔
635
        if (!is_num && range.has_value()) {
12✔
636
            const auto [lb, ub] = *range;
4✔
637
            // A non-numeric value may overwrite previously numeric bytes,
638
            // so conservatively mark the range [lb, ub) as non-numeric. havoc's
639
            // second argument is a width, not an upper bound.
640
            num_bytes.havoc(lb, ub - lb);
4✔
641
        }
642
        // When is_num is true, the value being stored is numeric. Any byte
643
        // that gets written will still be numeric, and bytes not written
644
        // keep their existing status, so num_bytes is left unchanged.
645
    }
646
    return {};
12✔
647
}
648

649
void ArrayDomain::havoc(NumAbsDomain& inv, const DataKind kind, const Interval& idx, const Interval& elem_size,
961,912✔
650
                        const bool big_endian) {
651
    split_and_find_var(*this, cells_.get_mutable(), inv, kind, idx, elem_size, big_endian);
961,912✔
652
}
961,912✔
653

654
void ArrayDomain::havoc_type(TypeDomain& inv, const Interval& idx, const Interval& elem_size) {
67,080✔
655
    constexpr auto kind = DataKind::types;
67,080✔
656
    if (auto maybe_cell = kill_and_find_var(
100,620✔
657
            cells_.get_mutable(), [&inv](const Variable v) { inv.havoc_type(v); }, kind, idx, elem_size)) {
159,022✔
658
        auto [offset, size] = *maybe_cell;
67,070✔
659
        num_bytes.havoc(offset, size);
67,070✔
660
    }
661
}
67,080✔
662

663
void ArrayDomain::store_numbers(const Interval& _idx, const Interval& _width) {
7,482✔
664
    const std::optional<Number> idx_n = _idx.singleton();
7,482✔
665
    if (!idx_n) {
7,482✔
666
        CRAB_WARN("array expansion store range ignored because ", "lower bound is not constant");
×
667
        return;
×
668
    }
669

670
    const std::optional<Number> width = _width.singleton();
7,482✔
671
    if (!width) {
7,482✔
672
        CRAB_WARN("array expansion store range ignored because ", "upper bound is not constant");
×
673
        return;
×
674
    }
675

676
    if (*idx_n + *width > total_stack_size()) {
7,482✔
677
        CRAB_WARN("array expansion store range ignored because ", "the number of elements is larger than limit of ",
×
678
                  total_stack_size());
679
        return;
×
680
    }
681
    num_bytes.reset(idx_n->narrow<int>(), width->narrow<int>());
7,482✔
682
}
683

684
void ArrayDomain::set_to_top() { num_bytes.set_to_top(); }
×
685

686
bool ArrayDomain::is_top() const { return num_bytes.is_top(); }
×
687

688
StringInvariant ArrayDomain::to_set() const { return num_bytes.to_set(); }
1,032✔
689

690
bool ArrayDomain::operator<=(const ArrayDomain& other) const { return num_bytes <= other.num_bytes; }
720✔
691

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

694
void ArrayDomain::operator|=(const ArrayDomain& other) {
63,294✔
695
    num_bytes |= other.num_bytes;
63,294✔
696
    cells_.get_mutable().merge_from(*other.cells_);
63,294✔
697
}
63,294✔
698

699
void ArrayDomain::operator|=(ArrayDomain&& other) {
×
700
    num_bytes |= std::move(other.num_bytes);
×
701
    cells_.get_mutable().merge_from(*other.cells_);
×
702
}
×
703

704
// Lattice combinators build a fresh ArrayDomain whose cells map is the union of
705
// both sides' cells. Cell membership is purely advisory (it enables overlap
706
// detection and dedup of mk_cell calls); the underlying numeric domain's join
707
// determines abstract values, and it operates on globally-interned Variable
708
// names so two domains independently tracking the same cell agree on its name.
709
ArrayDomain ArrayDomain::operator|(const ArrayDomain& other) const {
×
710
    ArrayDomain res{num_bytes | other.num_bytes};
×
711
    res.cells_.get_mutable().merge_from(*cells_);
×
712
    res.cells_.get_mutable().merge_from(*other.cells_);
×
713
    return res;
×
714
}
×
715

716
ArrayDomain ArrayDomain::operator&(const ArrayDomain& other) const {
712✔
717
    ArrayDomain res{num_bytes & other.num_bytes};
712✔
718
    res.cells_.get_mutable().merge_from(*cells_);
712✔
719
    res.cells_.get_mutable().merge_from(*other.cells_);
712✔
720
    return res;
712✔
721
}
×
722

723
ArrayDomain ArrayDomain::widen(const ArrayDomain& other) const {
174✔
724
    ArrayDomain res{num_bytes | other.num_bytes};
174✔
725
    res.cells_.get_mutable().merge_from(*cells_);
174✔
726
    res.cells_.get_mutable().merge_from(*other.cells_);
174✔
727
    return res;
174✔
728
}
×
729

730
ArrayDomain ArrayDomain::narrow(const ArrayDomain& other) const {
×
731
    ArrayDomain res{num_bytes & other.num_bytes};
×
732
    res.cells_.get_mutable().merge_from(*cells_);
×
733
    res.cells_.get_mutable().merge_from(*other.cells_);
×
734
    return res;
×
735
}
×
736

737
std::ostream& operator<<(std::ostream& o, const ArrayDomain& dom) { return o << dom.num_bytes; }
6✔
738
} // namespace prevail
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc