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

Alan-Jowett / ebpf-verifier / 30559636792

27 Jul 2026 07:29AM UTC coverage: 86.764% (+0.1%) from 86.649%
30559636792

push

github

elazarg
Fix 32-bit soundness holes that let unsafe programs pass verification

Two independent classes of ALU32/JMP32 unsoundness allowed a crafted program to
be accepted while performing an out-of-bounds stack access.

MOVSX: the no-op fast path returned early for `wX sN= wX`, skipping the
zero-extension applied at the end of the function. The verifier kept a
sign-extended 64-bit value where the CPU produces a zero-extended 32-bit one, so
`r1 = r10; r2 = -1; w2 s8= r2; r1 += r2` was analyzed as r10 - 1 while the CPU
computes r10 + 0xFFFFFFFF, and the resulting out-of-bounds store was proved
in-bounds. The verifier already contradicted itself here: with r1 = -1,
`w2 s8= r1` yielded 0xFFFFFFFF but `w2 s8= r2` yielded -1 for the same operation
and input. Guard the fast path with bin.is64; the 64-bit form remains a no-op.

32-bit compares: the helpers emitted constraints over the 64-bit svalue/uvalue
variables after testing only the truncated 32-bit views. When a register's
64-bit value falls outside the 32-bit range those views say nothing about how the
64-bit variables are ordered, so branches that concrete executions do take were
proved dead, and unsafe code on them was never checked. Test the 64-bit intervals
instead, falling back to the existing no-constraint case. Precision is unchanged
for registers holding genuine 32-bit values, since the comparison itself pins the
operand's sign in the branches that assert one.

assume_unsigned_32bit_lt already guarded every branch this way, which is why only
the >, >=, s<, s<=, s> and s>= paths were affected.

Also drop the interval parameters these helpers no longer read.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Elazar Gershuni <elazarg@gmail.com>

19 of 19 new or added lines in 2 files covered. (100.0%)

107 existing lines in 5 files now uncovered.

9328 of 10751 relevant lines covered (86.76%)

6280124.66 hits per line

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

85.16
/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,826,751✔
35
    unsigned size{};
11,815,674✔
36

37
    bool operator==(const Cell&) const = default;
257,545✔
38
    auto operator<=>(const Cell&) const = default;
23,095,733✔
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,736,818✔
44
        assert(sz > 0 && "overlap query with zero width");
9,736,818✔
45
        return offset < o + sz && o < offset + size;
9,481,429✔
46
    }
47
};
48

49
static Interval cell_to_interval(const offset_t o, const unsigned size) {
69,456✔
50
    const Number lb{gsl::narrow<int>(o)};
69,456✔
51
    return {lb, lb + size - 1};
104,184✔
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) {
120✔
57
    return !(cell_to_interval(c.offset, c.size) & range).is_bottom();
120✔
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) {
712,944✔
63
    return variable_registry.cell_var(kind, c.offset, c.size);
712,944✔
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,279,460✔
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
    Cell mk_cell(offset_t o, unsigned size);
86

87
  public:
88
    offset_map_t() = default;
30,968✔
89

90
    [[nodiscard]]
91
    std::optional<Cell> get_cell(offset_t o, unsigned size);
92

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

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

100
    void operator-=(const std::vector<Cell>& cells) {
133,120✔
101
        for (const auto& c : cells) {
322,194✔
102
            this->operator-=(c);
189,074✔
103
        }
104
    }
133,120✔
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) {
189,074✔
116
    const offset_t key = c.offset;
189,074✔
117
    if (const auto it = _map.find(key); it != _map.end()) {
189,074✔
118
        it->second.erase(c);
189,074✔
119
        if (it->second.empty()) {
189,074✔
120
            _map.erase(it);
163,642✔
121
        }
122
    }
123
}
189,074✔
124

125
[[nodiscard]]
126
std::vector<Cell> offset_map_t::get_overlap_cells_symbolic_offset(const Interval& range) {
202✔
127
    std::vector<Cell> out;
202✔
128
    for (const auto& o_cells : _map | std::views::values) {
322✔
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()) {
120✔
136
            // Cells are sorted by (offset, size); last element has the largest size.
137
            const Cell& largest_cell = *o_cells.rbegin();
120✔
138
            if (symbolic_overlap(largest_cell, range)) {
120✔
139
                for (const auto& c : o_cells) {
134✔
140
                    out.push_back(c);
70✔
141
                }
142
            }
143
        }
144
    }
145
    return out;
202✔
146
}
×
147

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

150
std::optional<Cell> offset_map_t::get_cell(const offset_t o, const unsigned size) {
1,644,002✔
151
    if (const auto it = _map.find(o); it != _map.end()) {
1,644,002✔
152
        if (const auto cit = it->second.find(Cell(o, size)); cit != it->second.end()) {
288,696✔
153
            return *cit;
250,810✔
154
        }
155
    }
156
    return {};
1,393,192✔
157
}
158

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

162
    if (const auto maybe_c = get_cell(o, size)) {
220,764✔
UNCOV
163
        return *maybe_c;
×
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);
220,764✔
168
    insert_cell(c);
220,764✔
169
    return c;
220,764✔
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,460,970✔
174
    std::vector<Cell> out;
1,460,970✔
175
    const Cell query_cell(o, size);
1,460,970✔
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,746,647✔
182
        --it;
8,555,192✔
183
        for (const Cell& x : it->second) {
17,720,368✔
184
            if (x.overlap(o, size) && x != query_cell) {
13,882,075✔
185
                out.push_back(x);
112,126✔
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,249,575✔
195
        bool any_overlap = false;
542,036✔
196
        for (const Cell& x : it->second) {
1,113,678✔
197
            if (x.overlap(o, size)) {
602,074✔
198
                out.push_back(x);
60,864✔
199
                any_overlap = true;
30,432✔
200
            }
201
        }
202
        if (!any_overlap) {
542,036✔
203
            break;
241,958✔
204
        }
205
    }
206

207
    return out;
2,191,455✔
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 {
158,989✔
213
    std::unordered_map<DataKind, offset_map_t> _maps;
214

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

218
    void merge_from(const StackCellRegistry& other) {
66,830✔
219
        if (this == &other) {
66,830✔
220
            return;
221
        }
222
        for (const auto& [kind, omap] : other._maps) {
1,505,885✔
223
            offset_map_t& dst = _maps[kind];
959,370✔
224
            for (const auto& [_off, cell_set] : omap._map) {
5,730,692✔
225
                for (const Cell& c : cell_set) {
9,919,254✔
226
                    dst.insert_cell(c);
5,147,932✔
227
                }
228
            }
229
        }
230
    }
231
};
232

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

235
void ArrayDomain::initialize_numbers(const int lb, const int width) {
310✔
236
    num_bytes.reset(lb, width);
310✔
237
    cells_.get_mutable().get(DataKind::svalues).mk_cell(offset_t{gsl::narrow_cast<Index>(lb)}, width);
310✔
238
}
310✔
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,118✔
261
                             const bool big_endian) {
262
    assert(kind == DataKind::svalues || kind == DataKind::uvalues);
3,118✔
263

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

270
    // Create a new cell for that range.
271
    offset_map_t& offset_map = cells_.get_mutable().get(kind);
3,118✔
272
    const Cell new_cell = offset_map.mk_cell(offset_t{gsl::narrow_cast<Index>(cell_start_index)}, len);
3,118✔
273
    inv.assign(cell_var(DataKind::svalues, new_cell), svalue);
3,118✔
274
    inv.assign(cell_var(DataKind::uvalues, new_cell), uvalue);
4,677✔
275
}
3,118✔
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,
201,886✔
280
                                   const Interval& elem_size, const bool big_endian) {
281
    assert(kind == DataKind::svalues || kind == DataKind::uvalues);
201,886✔
282
    offset_map_t& offset_map = cells_.get_mutable().get(kind);
201,886✔
283
    const std::optional<Number> n = ii.singleton();
201,886✔
284
    if (!n || !n->fits<Index>()) {
201,886✔
285
        // We can only split a singleton offset. A negative offset is an out-of-bounds
286
        // access below the stack frame: there is no cell to split and narrowing it to
287
        // the unsigned cell-offset type would throw, so leave it for the checker.
288
        return;
52✔
289
    }
290
    const std::optional<Number> n_bytes = elem_size.singleton();
201,834✔
291
    if (!n_bytes) {
201,834✔
292
        // We can only split a singleton size.
293
        return;
294
    }
295
    const auto size = n_bytes->narrow<unsigned int>();
201,834✔
296
    const offset_t o(n->narrow<Index>());
201,834✔
297

298
    const std::vector<Cell> overlaps = offset_map.get_overlap_cells(o, size);
201,834✔
299
    for (const Cell& c : overlaps) {
271,170✔
300
        const auto [cell_start_index, cell_end_index] = cell_to_interval(c.offset, c.size).pair<int>();
69,336✔
301
        if (!this->num_bytes.all_num(cell_start_index, cell_end_index + 1) ||
100,358✔
302
            cell_end_index + 1UL < cell_start_index + sizeof(int64_t)) {
62,044✔
303
            // We can only split numeric cells of size 8 or less.
304
            continue;
66,426✔
305
        }
306

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

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

331
    offset_map_t& offset_map = cells.get(kind);
1,240,330✔
332
    std::vector<Cell> overlaps;
1,240,330✔
333
    // A strong update requires an offset representable by the unsigned cell-offset
334
    // type. An out-of-range constant (in particular a negative offset) stays on the
335
    // weak path below: although the access starts outside the tracked stack, its
336
    // possible byte range may still overlap a tracked cell.
337
    if (const auto n = ii.singleton(); n && n->fits<Index>()) {
1,240,330✔
338
        if (const auto n_bytes = elem_size.singleton()) {
1,240,128✔
339
            auto size = n_bytes->narrow<unsigned int>();
1,240,128✔
340
            // -- Constant index: kill overlapping cells
341
            offset_t o(n->narrow<Index>());
1,240,128✔
342
            overlaps = offset_map.get_overlap_cells(o, size);
1,860,192✔
343
            // get_overlap_cells deliberately excludes the exact-match cell (o, size),
344
            // which is correct for loads (get_cell handles it separately) but wrong for
345
            // a kill: a pure-havoc caller (e.g. storing an uninitialized register over an
346
            // exact-match cell) must forget the stale value/type, so include it here.
347
            if (const auto exact = offset_map.get_cell(o, size)) {
1,240,128✔
348
                overlaps.push_back(*exact);
93,692✔
349
            }
350
            res = std::make_pair(o, size);
1,240,128✔
351
        }
352
    }
353
    if (!res) {
1,240,330✔
354
        // -- Non-constant index: kill overlapping cells
355
        // elem_size is a non-negative byte count (ValidSize enforces this for
356
        // dynamic helper widths), so ii + elem_size is the exclusive end.
357
        // symbolic_overlap expects an inclusive interval of byte offsets; subtract one
358
        // to avoid spuriously killing a cell that starts exactly at the end. This also
359
        // makes a negative constant offset safe without narrowing: [-8, -8] with width
360
        // 8 touches [-8, -1], while offset -4 with width 8 may touch [0, 3].
361
        overlaps = offset_map.get_overlap_cells_symbolic_offset(ii | (ii + elem_size - Interval{1}));
303✔
362
    }
363
    if (!overlaps.empty()) {
1,240,330✔
364
        // Forget the scalars from the relevant domain
365
        for (const auto& c : overlaps) {
322,194✔
366
            havoc_var(cell_var(kind, c));
189,074✔
367

368
            // Forget signed and unsigned values together.
369
            if (kind == DataKind::svalues) {
189,074✔
370
                havoc_var(cell_var(DataKind::uvalues, c));
67,318✔
371
            } else if (kind == DataKind::uvalues) {
121,756✔
372
                havoc_var(cell_var(DataKind::svalues, c));
64,872✔
373
            }
374
        }
375
        // Remove the cells. If needed again they will be re-created.
376
        offset_map -= overlaps;
133,120✔
377
    }
378
    return res;
1,860,495✔
379
}
1,240,330✔
380
static std::optional<std::tuple<int, int>> as_numbytes_range(const Interval& range, const int stack_size) {
20,098✔
381
    const Interval bounded_range = Interval{0, stack_size} & range;
20,098✔
382
    if (bounded_range.is_bottom()) {
20,098✔
383
        return {};
8✔
384
    }
385
    const auto bounds = bounded_range.pair<int>();
20,090✔
386
    const auto [lb, ub] = bounds;
20,090✔
387
    if (lb >= ub) {
20,090✔
388
        return {};
10✔
389
    }
390
    return bounds;
20,080✔
391
}
392

393
static std::optional<std::tuple<int, int>> as_numbytes_range(const Interval& index, const Interval& width,
13,688✔
394
                                                             const int stack_size) {
395
    const Interval range = index | (index + width);
13,688✔
396
    return as_numbytes_range(range, stack_size);
20,532✔
397
}
398

399
bool ArrayDomain::all_num_lb_ub(const Interval& lb, const Interval& ub) const {
6,410✔
400
    const auto range = as_numbytes_range(lb | ub, total_stack_size());
6,410✔
401
    if (!range.has_value()) {
6,410✔
402
        return false;
3✔
403
    }
404
    const auto [min_lb, max_ub] = *range;
6,404✔
405
    assert(min_lb < max_ub);
6,404✔
406
    return this->num_bytes.all_num(min_lb, max_ub);
6,404✔
407
}
408

409
bool ArrayDomain::all_num_width(const Interval& index, const Interval& width) const {
13,674✔
410
    const auto range = as_numbytes_range(index, width, total_stack_size());
13,674✔
411
    if (!range.has_value()) {
13,674✔
412
        return false;
5✔
413
    }
414
    const auto [min_lb, max_ub] = *range;
13,664✔
415
    assert(min_lb < max_ub);
13,664✔
416
    return this->num_bytes.all_num(min_lb, max_ub);
13,664✔
417
}
418

419
// Get the number of bytes, starting at offset, that are known to be numbers.
420
int ArrayDomain::min_all_num_size(const NumAbsDomain& inv, const Variable offset) const {
30,818✔
421
    const auto min_lb = inv.eval_interval(offset).lb().number();
46,227✔
422
    const auto max_ub = inv.eval_interval(offset).ub().number();
46,227✔
423
    if (!min_lb || !max_ub || !min_lb->fits<int32_t>() || !max_ub->fits<int32_t>()) {
30,818✔
424
        return 0;
53✔
425
    }
426
    const auto lb = min_lb->narrow<int>();
30,712✔
427
    const auto ub = max_ub->narrow<int>();
30,712✔
428
    return std::max(0, this->num_bytes.all_num_width(lb) - (ub - lb));
44,034✔
429
}
430

431
// Get one byte of a value.
432
std::optional<uint8_t> get_value_byte(const NumAbsDomain& inv, const offset_t o, const int width,
105,780✔
433
                                      const bool big_endian) {
434
    const Variable v = variable_registry.cell_var(DataKind::svalues, (o / width) * width, width);
105,780✔
435
    const std::optional<Number> t = inv.eval_interval(v).singleton();
105,780✔
436
    if (!t) {
105,780✔
437
        return {};
69,502✔
438
    }
439
    Index n = t->cast_to<Index>();
36,278✔
440

441
    // Convert value to bytes of the appropriate endian-ness.
442
    switch (width) {
36,278✔
443
    case sizeof(uint8_t): break;
130✔
444
    case sizeof(uint16_t):
74✔
445
        if (big_endian) {
74✔
446
            n = boost::endian::native_to_big<uint16_t>(n);
×
447
        } else {
448
            n = boost::endian::native_to_little<uint16_t>(n);
74✔
449
        }
450
        break;
37✔
451
    case sizeof(uint32_t):
1,238✔
452
        if (big_endian) {
1,238✔
UNCOV
453
            n = boost::endian::native_to_big<uint32_t>(n);
×
454
        } else {
455
            n = boost::endian::native_to_little<uint32_t>(n);
1,238✔
456
        }
457
        break;
619✔
458
    case sizeof(Index):
34,706✔
459
        if (big_endian) {
34,706✔
460
            n = boost::endian::native_to_big<Index>(n);
48✔
461
        } else {
462
            n = boost::endian::native_to_little<Index>(n);
17,329✔
463
        }
464
        break;
17,353✔
UNCOV
465
    default: CRAB_ERROR("Unexpected width ", width);
×
466
    }
467
    const auto bytes = reinterpret_cast<uint8_t*>(&n);
36,278✔
468
    return bytes[o % width];
36,278✔
469
}
470

471
std::optional<LinearExpression> ArrayDomain::load(const NumAbsDomain& inv, const DataKind kind, const Interval& i,
166,120✔
472
                                                  const int width, const bool big_endian) {
473
    if (const std::optional<Number> n = i.singleton()) {
166,120✔
474
        offset_map_t& offset_map = cells_.get_mutable().get(kind);
166,084✔
475
        const int64_t k = n->narrow<int64_t>();
166,084✔
476
        const offset_t o(k);
166,084✔
477
        const unsigned size = to_unsigned(width);
166,084✔
478
        if (const auto cell = offset_map.get_cell(o, size)) {
166,084✔
479
            return cell_var(kind, *cell);
210,159✔
480
        }
481
        if (kind == DataKind::svalues || kind == DataKind::uvalues) {
25,978✔
482
            // Copy bytes into result_buffer, taking into account that the
483
            // bytes might be in different stack variables and might be unaligned.
484
            uint8_t result_buffer[8];
485
            bool found = true;
44,117✔
486
            for (unsigned int index = 0; index < size; index++) {
62,256✔
487
                const offset_t byte_offset{o + index};
53,112✔
488
                std::optional<uint8_t> b = get_value_byte(inv, byte_offset, 8, big_endian);
53,112✔
489
                if (!b) {
53,112✔
490
                    b = get_value_byte(inv, byte_offset, 4, big_endian);
18,406✔
491
                    if (!b) {
18,406✔
492
                        b = get_value_byte(inv, byte_offset, 2, big_endian);
17,168✔
493
                        if (!b) {
17,168✔
494
                            b = get_value_byte(inv, byte_offset, 1, big_endian);
17,094✔
495
                        }
496
                    }
497
                }
498
                if (b) {
53,112✔
499
                    result_buffer[index] = *b;
36,278✔
500
                } else {
501
                    found = false;
16,834✔
502
                    break;
16,834✔
503
                }
504
            }
505
            if (found) {
21,406✔
506
                // We have an aligned result in result_buffer so we can now
507
                // convert to an integer.
508
                if (size == 1) {
9,144✔
509
                    return *result_buffer;
7,435✔
510
                }
511
                if (size == 2) {
8,242✔
512
                    uint16_t b = *reinterpret_cast<uint16_t*>(result_buffer);
1,442✔
513
                    if (big_endian) {
1,442✔
514
                        b = boost::endian::native_to_big<uint16_t>(b);
14✔
515
                    } else {
516
                        b = boost::endian::native_to_little<uint16_t>(b);
1,435✔
517
                    }
518
                    return b;
1,442✔
519
                }
520
                if (size == 4) {
6,800✔
521
                    uint32_t b = *reinterpret_cast<uint32_t*>(result_buffer);
4,524✔
522
                    if (big_endian) {
4,524✔
523
                        b = boost::endian::native_to_big<uint32_t>(b);
14✔
524
                    } else {
525
                        b = boost::endian::native_to_little<uint32_t>(b);
2,255✔
526
                    }
527
                    return b;
4,524✔
528
                }
529
                if (size == 8) {
2,276✔
530
                    Index b = *reinterpret_cast<Index*>(result_buffer);
116✔
531
                    if (big_endian) {
116✔
532
                        b = boost::endian::native_to_big<Index>(b);
6✔
533
                    } else {
534
                        b = boost::endian::native_to_little<Index>(b);
55✔
535
                    }
536
                    return kind == DataKind::uvalues ? Number(b) : Number(to_signed(b));
116✔
537
                }
538
            }
539
        }
540

541
        const std::vector<Cell> overlaps = offset_map.get_overlap_cells(o, size);
18,994✔
542
        if (overlaps.empty()) {
18,994✔
543
            const Cell c = offset_map.mk_cell(o, size);
11,812✔
544
            // Here it's ok to do assignment (instead of expand) because c is not a summarized variable.
545
            // Otherwise, it would be unsound.
546
            return cell_var(kind, c);
11,812✔
547
        }
548
        CRAB_WARN("Ignored read from cell ", kind, "[", o, "...", o + size - 1, "]", " because it overlaps with ",
7,182✔
549
                  overlaps.size(), " cells");
550
        /*
551
            TODO: we can apply here "Value Recomposition" a la Mine'06 (https://arxiv.org/pdf/cs/0703074.pdf)
552
                to construct values of some type from a sequence of bytes.
553
                It can be endian-independent but it would more precise if we choose between little- and big-endian.
554
        */
555
    } else {
18,994✔
556
        // TODO: we can be more precise here
557
        CRAB_WARN("array expansion: ignored array load because of non-constant array index ", i);
36✔
558
    }
559
    return {};
7,218✔
560
}
561

562
std::optional<LinearExpression> ArrayDomain::load_type(const Interval& i, const int width) {
49,122✔
563
    if (const std::optional<Number> n = i.singleton()) {
49,122✔
564
        offset_map_t& offset_map = cells_.get_mutable().get(DataKind::types);
49,122✔
565
        const int64_t k = n->narrow<int64_t>();
49,122✔
566
        auto [only_num, only_non_num] = num_bytes.uniformity(k, width);
49,122✔
567
        if (only_num) {
49,122✔
568
            return T_NUM;
32,082✔
569
        }
570
        if (!only_non_num || width != 8) {
17,040✔
571
            return {};
14✔
572
        }
573
        const offset_t o(k);
17,026✔
574
        const unsigned size = to_unsigned(width);
17,026✔
575
        if (const auto cell = offset_map.get_cell(o, size)) {
17,026✔
576
            return cell_var(DataKind::types, *cell);
25,518✔
577
        }
578
        const std::vector<Cell> overlaps = offset_map.get_overlap_cells(o, size);
14✔
579
        if (overlaps.empty()) {
14✔
580
            const Cell c = offset_map.mk_cell(o, size);
14✔
581
            // Here it's ok to do assignment (instead of expand) because c is not a summarized variable.
582
            // Otherwise, it would be unsound.
583
            return cell_var(DataKind::types, c);
14✔
584
        }
UNCOV
585
        CRAB_WARN("Ignored read from cell ", DataKind::types, "[", o, "...", o + size - 1, "]",
×
586
                  " because it overlaps with ", overlaps.size(), " cells");
587
        /*
588
            TODO: we can apply here "Value Recomposition" a la Mine'06 (https://arxiv.org/pdf/cs/0703074.pdf)
589
                to construct values of some type from a sequence of bytes.
590
                It can be endian-independent but it would more precise if we choose between little- and big-endian.
591
        */
592
    } else {
14✔
593
        // Check whether the kind is uniform across the entire interval.
UNCOV
594
        const auto lb = i.lb().number();
×
UNCOV
595
        const auto ub = i.ub().number();
×
UNCOV
596
        if (lb.has_value() && ub.has_value()) {
×
UNCOV
597
            const Number fullwidth = ub.value() - lb.value() + width;
×
UNCOV
598
            if (lb->fits<uint32_t>() && fullwidth.fits<uint32_t>()) {
×
UNCOV
599
                auto [only_num, only_non_num] =
×
UNCOV
600
                    num_bytes.uniformity(lb->narrow<uint32_t>(), fullwidth.narrow<uint32_t>());
×
UNCOV
601
                if (only_num) {
×
UNCOV
602
                    return T_NUM;
×
603
                }
604
            }
605
        }
606
    }
UNCOV
607
    return {};
×
608
}
609

610
// We are about to write to a given range of bytes on the stack.
611
// Any cells covering that range need to be removed, and any cells that only
612
// partially cover that range can be split such that any non-covered portions become new cells.
613
static std::optional<std::pair<offset_t, unsigned>>
614
split_and_find_var(ArrayDomain& array_domain, StackCellRegistry& cells, NumAbsDomain& inv, const DataKind kind,
1,105,824✔
615
                   const Interval& idx, const Interval& elem_size, const bool big_endian) {
616
    if (kind == DataKind::svalues || kind == DataKind::uvalues) {
1,105,824✔
617
        array_domain.split_number_var(inv, kind, idx, elem_size, big_endian);
201,886✔
618
    }
619
    return kill_and_find_var(cells, [&inv](const Variable v) { inv.havoc(v); }, kind, idx, elem_size);
1,372,490✔
620
}
621

622
std::optional<Variable> ArrayDomain::store(NumAbsDomain& inv, const DataKind kind, const Interval& idx,
138,388✔
623
                                           const Interval& elem_size, const bool big_endian) {
624
    if (auto maybe_cell = split_and_find_var(*this, cells_.get_mutable(), inv, kind, idx, elem_size, big_endian)) {
138,388✔
625
        // perform strong update
626
        auto [offset, size] = *maybe_cell;
138,360✔
627
        const Cell c = cells_.get_mutable().get(kind).mk_cell(offset, size);
138,360✔
628
        Variable v = cell_var(kind, c);
138,360✔
629
        return v;
138,360✔
630
    }
631
    return {};
28✔
632
}
633

634
std::optional<Variable> ArrayDomain::store_type(TypeDomain& inv, const Interval& idx, const Interval& width,
67,164✔
635
                                                const bool is_num) {
636
    constexpr auto kind = DataKind::types;
67,164✔
637
    if (auto maybe_cell = kill_and_find_var(
100,746✔
638
            cells_.get_mutable(), [&inv](const Variable v) { inv.havoc_type(v); }, kind, idx, width)) {
134,328✔
639
        // perform strong update
640
        auto [offset, size] = *maybe_cell;
67,150✔
641
        if (is_num) {
67,150✔
642
            num_bytes.reset(offset, size);
64,510✔
643
        } else {
644
            num_bytes.havoc(offset, size);
2,640✔
645
        }
646
        const Cell c = cells_.get_mutable().get(kind).mk_cell(offset, size);
67,150✔
647
        Variable v = cell_var(kind, c);
67,150✔
648
        return v;
67,150✔
649
    } else {
650
        using namespace dsl_syntax;
7✔
651
        // Weak update: cannot perform a strong update because the index is
652
        // not a singleton. Havoc the type cells in the range.
653
        const auto range = as_numbytes_range(idx, width, total_stack_size());
14✔
654
        if (!is_num && range.has_value()) {
14✔
655
            const auto [lb, ub] = *range;
4✔
656
            // A non-numeric value may overwrite previously numeric bytes,
657
            // so conservatively mark the range [lb, ub) as non-numeric. havoc's
658
            // second argument is a width, not an upper bound.
659
            num_bytes.havoc(lb, ub - lb);
4✔
660
        }
661
        // When is_num is true, the value being stored is numeric. Any byte
662
        // that gets written will still be numeric, and bytes not written
663
        // keep their existing status, so num_bytes is left unchanged.
664
    }
665
    return {};
14✔
666
}
667

668
void ArrayDomain::havoc(NumAbsDomain& inv, const DataKind kind, const Interval& idx, const Interval& elem_size,
967,436✔
669
                        const bool big_endian) {
670
    split_and_find_var(*this, cells_.get_mutable(), inv, kind, idx, elem_size, big_endian);
967,436✔
671
}
967,436✔
672

673
void ArrayDomain::havoc_type(TypeDomain& inv, const Interval& idx, const Interval& elem_size) {
67,342✔
674
    constexpr auto kind = DataKind::types;
67,342✔
675
    if (auto maybe_cell = kill_and_find_var(
101,013✔
676
            cells_.get_mutable(), [&inv](const Variable v) { inv.havoc_type(v); }, kind, idx, elem_size)) {
189,282✔
677
        auto [offset, size] = *maybe_cell;
67,330✔
678
        num_bytes.havoc(offset, size);
67,330✔
679
    }
680
}
67,342✔
681

682
void ArrayDomain::store_numbers(const Interval& _idx, const Interval& _width) {
7,638✔
683
    const std::optional<Number> idx_n = _idx.singleton();
7,638✔
684
    if (!idx_n) {
7,638✔
UNCOV
685
        CRAB_WARN("array expansion store range ignored because ", "lower bound is not constant");
×
686
        return;
×
687
    }
688

689
    const std::optional<Number> width = _width.singleton();
7,638✔
690
    if (!width) {
7,638✔
UNCOV
691
        CRAB_WARN("array expansion store range ignored because ", "upper bound is not constant");
×
692
        return;
×
693
    }
694

695
    if (*idx_n + *width > total_stack_size()) {
7,638✔
UNCOV
696
        CRAB_WARN("array expansion store range ignored because ", "the number of elements is larger than limit of ",
×
697
                  total_stack_size());
UNCOV
698
        return;
×
699
    }
700
    num_bytes.reset(idx_n->narrow<int>(), width->narrow<int>());
7,638✔
701
}
702

UNCOV
703
void ArrayDomain::set_to_top() { num_bytes.set_to_top(); }
×
704

UNCOV
705
bool ArrayDomain::is_top() const { return num_bytes.is_top(); }
×
706

707
StringInvariant ArrayDomain::to_set() const { return num_bytes.to_set(); }
1,068✔
708

709
bool ArrayDomain::operator<=(const ArrayDomain& other) const { return num_bytes <= other.num_bytes; }
736✔
710

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

713
void ArrayDomain::operator|=(const ArrayDomain& other) {
65,046✔
714
    num_bytes |= other.num_bytes;
65,046✔
715
    cells_.get_mutable().merge_from(*other.cells_);
65,046✔
716
}
65,046✔
717

UNCOV
718
void ArrayDomain::operator|=(ArrayDomain&& other) {
×
UNCOV
719
    num_bytes |= std::move(other.num_bytes);
×
UNCOV
720
    cells_.get_mutable().merge_from(*other.cells_);
×
721
}
×
722

723
// Lattice combinators build a fresh ArrayDomain whose cells map is the union of
724
// both sides' cells. Cell membership is purely advisory (it enables overlap
725
// detection and dedup of mk_cell calls); the underlying numeric domain's join
726
// determines abstract values, and it operates on globally-interned Variable
727
// names so two domains independently tracking the same cell agree on its name.
728
ArrayDomain ArrayDomain::operator|(const ArrayDomain& other) const {
×
UNCOV
729
    ArrayDomain res{num_bytes | other.num_bytes};
×
730
    res.cells_.get_mutable().merge_from(*cells_);
×
731
    res.cells_.get_mutable().merge_from(*other.cells_);
×
732
    return res;
×
733
}
×
734

735
ArrayDomain ArrayDomain::operator&(const ArrayDomain& other) const {
714✔
736
    ArrayDomain res{num_bytes & other.num_bytes};
714✔
737
    res.cells_.get_mutable().merge_from(*cells_);
714✔
738
    res.cells_.get_mutable().merge_from(*other.cells_);
714✔
739
    return res;
714✔
UNCOV
740
}
×
741

742
ArrayDomain ArrayDomain::widen(const ArrayDomain& other) const {
178✔
743
    ArrayDomain res{num_bytes | other.num_bytes};
178✔
744
    res.cells_.get_mutable().merge_from(*cells_);
178✔
745
    res.cells_.get_mutable().merge_from(*other.cells_);
178✔
746
    return res;
178✔
UNCOV
747
}
×
748

UNCOV
749
ArrayDomain ArrayDomain::narrow(const ArrayDomain& other) const {
×
UNCOV
750
    ArrayDomain res{num_bytes & other.num_bytes};
×
UNCOV
751
    res.cells_.get_mutable().merge_from(*cells_);
×
UNCOV
752
    res.cells_.get_mutable().merge_from(*other.cells_);
×
UNCOV
753
    return res;
×
UNCOV
754
}
×
755

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