rigless 0.2.1

Virtual hardware-in-the-loop runtime: run unmodified firmware on an emulated MCU with every peripheral, chip model and unit of virtual time in Rust, and a CPU core of its own that translates the firmware's instructions to host code
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! The target's memory: every region's bytes, which the runtime owns and the core maps.
//!
//! The core reaches a region's bytes directly once it has mapped them, at the region's own address
//! and at each address an alias gives it, so both are the same bytes. A region the guest can't
//! write is mapped read-only: the guest's store there reaches the access callback, which drops it,
//! or sends it on to the bus where a model programs the region, and the runtime's own writes, an
//! image placed or a snapshot put back, land all the same.
//!
//! The core keeps pointers into the bytes between calls, so this module never borrows them. Every
//! read and write here goes through the same pointer the core's do. It calls no C, so Miri checks it
//! with no core.
#![expect(
    unsafe_code,
    reason = "the core keeps pointers into the regions, so they're reached through raw pointers only"
)]

use std::ops::Range;
use std::ptr::NonNull;

use crate::emulator::Alias;
use crate::soc::{Endian, Region, Width};

/// One region and its bytes.
#[derive(Debug)]
struct Bytes {
    base: u64,
    /// Where the same bytes also answer.
    aliases: Vec<u64>,
    writable: bool,
    /// Whether a model of the part programs it, which keeps it as a writable one is kept.
    programmable: bool,
    /// What every byte reads once it's erased.
    erased: u8,
    /// The region's bytes, which this owns as the box they came from.
    contents: NonNull<u8>,
    size: usize,
}

// SAFETY: `Bytes` owns its allocation as a `Box<[u8]>` would, and nothing else holds a pointer
// into it but the core on the thread the run moves it to.
unsafe impl Send for Bytes {}

impl Drop for Bytes {
    fn drop(&mut self) {
        if self.size > 0 {
            // SAFETY: the allocation `Bytes::erased` made, with the same layout, freed once.
            unsafe { std::alloc::dealloc(self.contents.as_ptr(), layout(self.size)) }
        }
    }
}

/// How a region's bytes are allocated: page-aligned, so the core maps whole host pages of them.
fn layout(size: usize) -> std::alloc::Layout {
    std::alloc::Layout::from_size_align(size, PAGE_ALIGNMENT).expect("a region fits in memory")
}

/// The alignment of every region's bytes, and so the largest page a processor may map them in.
pub(crate) const PAGE_ALIGNMENT: usize = 4096;

impl Bytes {
    fn erased(region: &Region, aliases: Vec<u64>) -> Self {
        let size = usize::try_from(region.size).expect("a region fits in memory");
        let contents = if size == 0 {
            NonNull::dangling()
        } else {
            // SAFETY: a nonzero size.
            let contents = NonNull::new(unsafe { std::alloc::alloc_zeroed(layout(size)) })
                .unwrap_or_else(|| std::alloc::handle_alloc_error(layout(size)));
            if region.erased != 0 {
                // SAFETY: the `size` bytes just allocated.
                unsafe { contents.as_ptr().write_bytes(region.erased, size) }
            }
            contents
        };
        Self {
            base: region.base,
            aliases,
            writable: region.kind.is_writable(),
            programmable: region.programmer.is_some(),
            erased: region.erased,
            contents,
            size,
        }
    }

    /// The offset of `address` into this region, at its own address or an alias, when the whole
    /// `length` fits.
    fn offset(&self, address: u64, length: u64) -> Option<usize> {
        let size = self.size as u64;
        self.starts()
            .find(|&base| {
                address >= base
                    && address
                        .checked_add(length)
                        .zip(base.checked_add(size))
                        .is_some_and(|(end, limit)| end <= limit)
            })
            .map(|base| usize::try_from(address - base).expect("inside a region in memory"))
    }

    /// Whether a snapshot keeps the region: the guest writes it or a model programs it.
    fn kept(&self) -> bool {
        self.writable || self.programmable
    }

    /// The region's own address, then each alias.
    fn starts(&self) -> impl Iterator<Item = u64> {
        std::iter::once(self.base).chain(self.aliases.iter().copied())
    }

    /// The byte at `offset`, which the caller has checked is inside.
    fn at(&self, offset: usize) -> *mut u8 {
        self.contents.as_ptr().wrapping_add(offset)
    }

    /// Fills `into` from `offset`.
    fn copy_out(&self, offset: usize, into: &mut [u8]) {
        assert!(offset + into.len() <= self.size, "inside the region");
        // SAFETY: inside the allocation, and `into` is the caller's own, so no overlap.
        unsafe { std::ptr::copy_nonoverlapping(self.at(offset), into.as_mut_ptr(), into.len()) }
    }

    /// Writes `bytes` at `offset`.
    fn copy_in(&mut self, offset: usize, bytes: &[u8]) {
        assert!(offset + bytes.len() <= self.size, "inside the region");
        // SAFETY: as `copy_out`.
        unsafe { std::ptr::copy_nonoverlapping(bytes.as_ptr(), self.at(offset), bytes.len()) }
    }
}

/// Every region of one target.
#[derive(Debug)]
pub(crate) struct Memory {
    regions: Vec<Bytes>,
    /// The order a value's bytes are in.
    endian: Endian,
}

/// Memory a processor holds pointers into, from the moment it's mapped into it until the processor
/// is dropped. Its bytes must never move or be freed while the processor lives, so this lends
/// `&Memory`, and writes into its bytes where they are, but never lends `&mut Memory`, which could
/// replace it or take it.
#[derive(Debug)]
pub(crate) struct Mapped(Memory);

impl Mapped {
    pub(crate) fn new(memory: Memory) -> Self {
        Self(memory)
    }

    /// [`Memory::place`], in place.
    pub(crate) fn place(&mut self, address: u64, bytes: &[u8]) -> bool {
        self.0.place(address, bytes)
    }

    /// [`Memory::write`], in place.
    pub(crate) fn write(&mut self, address: u64, width: Width, value: u64) -> bool {
        self.0.write(address, width, value)
    }
}

impl std::ops::Deref for Mapped {
    type Target = Memory;

    fn deref(&self) -> &Memory {
        &self.0
    }
}

/// One mapping the core makes: where, how long, and whether the guest may write it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Mapping {
    pub(crate) start: u64,
    pub(crate) length: u64,
    pub(crate) writable: bool,
}

/// The low `count` bytes of `value` as a target that's `endian` keeps them in memory, in the first
/// `count` of the answer.
pub(crate) fn to_target(value: u64, count: usize, endian: Endian) -> [u8; 8] {
    let mut bytes = [0; 8];
    match endian {
        Endian::Little => bytes[..count].copy_from_slice(&value.to_le_bytes()[..count]),
        Endian::Big => bytes[..count].copy_from_slice(&value.to_be_bytes()[8 - count..]),
    }
    bytes
}

/// The value of `bytes` as a target that's `endian` keeps them in memory.
pub(crate) fn from_target(bytes: &[u8], endian: Endian) -> u64 {
    let mut word = [0; 8];
    match endian {
        Endian::Little => {
            word[..bytes.len()].copy_from_slice(bytes);
            u64::from_le_bytes(word)
        }
        Endian::Big => {
            word[8 - bytes.len()..].copy_from_slice(bytes);
            u64::from_be_bytes(word)
        }
    }
}

impl Memory {
    /// The regions of `map`, erased, with each alias placed beside the region it names.
    pub(crate) fn new(map: &[Region], aliases: &[Alias], endian: Endian) -> Self {
        Self {
            regions: map
                .iter()
                .map(|region| {
                    Bytes::erased(
                        region,
                        aliases
                            .iter()
                            .filter(|alias| alias.region == region.name)
                            .map(|alias| alias.at)
                            .collect(),
                    )
                })
                .collect(),
            endian,
        }
    }

    /// The order a value's bytes are in.
    pub(crate) fn endian(&self) -> Endian {
        self.endian
    }

    /// Maps every region, at its own address and at its aliases, into `processor`, which reads and
    /// writes them from then on.
    ///
    /// # Safety
    ///
    /// This outlives `processor`, its `Drop` included. Nothing else borrows a region's bytes while
    /// a method of the processor runs, as [`Processor::map`](super::Processor::map) asks.
    #[cfg(not(emulator_absent))]
    pub(crate) unsafe fn map_into(&self, processor: &mut dyn super::Processor) {
        for region in &self.regions {
            for start in region.starts() {
                // SAFETY: the region's own bytes, all `size` of them, whole pages in an allocation
                // aligned to one that never moves and is freed only when this is dropped, and the
                // caller's promise for the rest. `Memory` reaches them only between the
                // processor's calls and from the bus during them.
                unsafe { processor.map(start, region.size as u64, region.at(0), region.writable) };
            }
        }
    }

    /// Every mapping the core makes, each region at its own address and at its aliases.
    pub(crate) fn mappings(&self) -> Vec<Mapping> {
        self.regions
            .iter()
            .flat_map(|region| {
                region.starts().map(|start| Mapping {
                    start,
                    length: region.size as u64,
                    writable: region.writable,
                })
            })
            .collect()
    }

    /// The first mapping that isn't whole pages of `page` bytes. The core maps memory a page at a
    /// time and reaches all of a page it maps, so a region that ends inside a page would let the
    /// guest past its end.
    pub(crate) fn partial_page(&self, page: u64) -> Option<Mapping> {
        self.mappings().into_iter().find(|mapping| {
            mapping.length == 0
                || !mapping.start.is_multiple_of(page)
                || !mapping.length.is_multiple_of(page)
        })
    }

    /// The host address of the byte at `address`, as the core keeps one.
    #[cfg(test)]
    fn host(&self, address: u64) -> Option<*mut u8> {
        self.regions.iter().find_map(|region| {
            let offset = region.offset(address, 1)?;
            Some(region.at(offset))
        })
    }

    /// Each region's base and bytes: the ones the guest writes and the ones a model programs, and
    /// with `read_only` every other one too.
    pub(crate) fn contents(&self, read_only: bool) -> Vec<(u64, Vec<u8>)> {
        self.regions
            .iter()
            .filter(|region| read_only || region.kept())
            .map(|region| {
                let mut bytes = vec![0; region.size];
                region.copy_out(0, &mut bytes);
                (region.base, bytes)
            })
            .collect()
    }

    /// Each region a snapshot doesn't keep, by its base, as it starts: every byte erased.
    pub(crate) fn erased_unkept(&self) -> Vec<(u64, Vec<u8>)> {
        self.regions
            .iter()
            .filter(|region| !region.kept())
            .map(|region| (region.base, vec![region.erased; region.size]))
            .collect()
    }

    /// Whether `address` is memory the guest can't write.
    pub(crate) fn read_only(&self, address: u64) -> bool {
        self.holds(address, 1, |region| !region.writable)
    }

    /// Whether a region `keep` picks has every byte of `length` from `address`, at its own address
    /// or an alias.
    fn holds(&self, address: u64, length: u64, keep: impl Fn(&Bytes) -> bool) -> bool {
        self.regions
            .iter()
            .any(|region| keep(region) && region.offset(address, length).is_some())
    }

    /// `width` from `address`, in the target's byte order, or `None` where no region has every
    /// byte.
    pub(crate) fn read(&self, address: u64, width: Width) -> Option<u64> {
        let bytes = usize::try_from(width.bytes()).expect("a width fits");
        self.regions.iter().find_map(|region| {
            let offset = region.offset(address, bytes as u64)?;
            let mut word = [0; 8];
            region.copy_out(offset, &mut word[..bytes]);
            Some(from_target(&word[..bytes], self.endian))
        })
    }

    /// Writes `width` of `value` at `address`, in the target's byte order, whether or not the guest
    /// may write there. Answers whether a region had every byte.
    pub(crate) fn write(&mut self, address: u64, width: Width, value: u64) -> bool {
        let bytes = usize::try_from(width.bytes()).expect("a width fits");
        self.place(address, &to_target(value, bytes, self.endian)[..bytes])
    }

    /// Writes `bytes` at `address`, whether or not the guest may write there. Answers whether one
    /// region had every byte.
    pub(crate) fn place(&mut self, address: u64, bytes: &[u8]) -> bool {
        self.regions.iter_mut().any(|region| {
            let Some(offset) = region.offset(address, bytes.len() as u64) else {
                return false;
            };
            region.copy_in(offset, bytes);
            true
        })
    }

    /// Whether a model of the part programs the region holding the whole of `width` at `address`,
    /// at the region's own address. The bus knows a region there and nowhere else, so a store
    /// through an alias is dropped as it is in any other memory the guest can't write.
    pub(crate) fn programmable(&self, address: u64, width: Width) -> bool {
        let end = address.checked_add(u64::from(width.bytes()));
        self.regions.iter().any(|region| {
            region.programmable
                && address >= region.base
                && end.is_some_and(|end| end <= region.base + region.size as u64)
        })
    }

    /// Whether a snapshot keeps the region holding `address`: one the guest writes or a model
    /// programs.
    pub(crate) fn kept(&self, address: u64) -> bool {
        self.holds(address, 1, Bytes::kept)
    }

    /// Whether `address` is in a region the guest can write, where its writes are kept.
    pub(crate) fn writable(&self, address: u64) -> bool {
        self.holds(address, 1, |region| region.writable)
    }

    /// The spans of `bytes` that differ from what memory holds from `address`, as offsets into
    /// `bytes`, a few dozen bytes at a time, or `None` where no region has every byte. Putting back
    /// only these leaves the code the core translated from the rest of a region where it is.
    pub(crate) fn changed(&self, address: u64, bytes: &[u8]) -> Option<Vec<Range<usize>>> {
        const CHUNK: usize = 64;
        let (region, offset) = self
            .regions
            .iter()
            .find_map(|region| Some((region, region.offset(address, bytes.len() as u64)?)))?;
        let mut held = vec![0; bytes.len()];
        region.copy_out(offset, &mut held);
        let mut spans: Vec<Range<usize>> = Vec::new();
        for (index, (now, then)) in held.chunks(CHUNK).zip(bytes.chunks(CHUNK)).enumerate() {
            if now == then {
                continue;
            }
            let start = index * CHUNK;
            match spans.last_mut() {
                Some(last) if last.end == start => last.end = start + then.len(),
                _ => spans.push(start..start + then.len()),
            }
        }
        Some(spans)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn memory() -> Memory {
        Memory::new(
            &[
                Region::flash("flash", 0x0800_0000, 0x1000).erased_to(0xFF),
                Region::ram("sram", 0x2000_0000, 0x100),
            ],
            &[Alias {
                region: "flash",
                at: 0,
            }],
            Endian::Little,
        )
    }

    /// `contents` copies out each region whole, by its base, and the ones the guest can't write
    /// only when asked for.
    #[test]
    fn contents_copies_out_each_region_whole() {
        let mut memory = memory();
        assert!(memory.place(0x0800_0004, &[1, 2]));
        assert!(memory.place(0x2000_00FE, &[3, 4]));
        let writable = memory.contents(false);
        assert_eq!(writable.len(), 1);
        let (base, ram) = &writable[0];
        assert_eq!((*base, ram.len()), (0x2000_0000, 0x100));
        assert_eq!(&ram[0xFE..], [3, 4]);
        let every = memory.contents(true);
        assert_eq!(
            every
                .iter()
                .map(|(base, bytes)| (*base, bytes.len()))
                .collect::<Vec<_>>(),
            [(0x0800_0000, 0x1000), (0x2000_0000, 0x100)]
        );
        assert_eq!(&every[0].1[4..6], [1, 2]);
    }

    /// A region a model programs is kept as a writable one is, and only it answers to a program, which
    /// has to fit inside it at its own address.
    #[test]
    fn a_programmed_region_is_kept_and_programmable_where_another_read_only_one_is_not() {
        let memory = Memory::new(
            &[
                Region::flash("flash", 0x0800_0000, 0x1000).programmed_by("PROG"),
                Region::system("system", 0x0900_0000, 0x100),
                Region::ram("sram", 0x2000_0000, 0x100),
            ],
            &[Alias {
                region: "flash",
                at: 0,
            }],
            Endian::Little,
        );
        assert!(memory.programmable(0x0800_0FFC, Width::W32));
        assert!(
            !memory.programmable(0x0800_0FFE, Width::W32),
            "runs past the end"
        );
        assert!(!memory.programmable(0x10, Width::W32), "through the alias");
        assert!(memory.kept(0x10), "the same bytes");
        assert!(!memory.programmable(0x0900_0000, Width::W32));
        assert!(
            !memory.programmable(0x2000_0000, Width::W32),
            "the guest writes it itself"
        );
        let kept: Vec<u64> = memory
            .contents(false)
            .iter()
            .map(|(base, _)| *base)
            .collect();
        assert_eq!(kept, [0x0800_0000, 0x2000_0000]);
        assert!(memory.kept(0x0800_0000) && memory.kept(0x2000_0000));
        assert!(!memory.kept(0x0900_0000));
        let erased: Vec<u64> = memory
            .erased_unkept()
            .iter()
            .map(|(base, _)| *base)
            .collect();
        assert_eq!(erased, [0x0900_0000], "a snapshot keeps the programmed one");
    }

    /// `changed` gives the spans that differ, a chunk at a time, with neighbors joined, and nothing
    /// where memory has no region for every byte.
    #[test]
    fn changed_gives_the_chunks_that_differ() {
        let memory = memory();
        let mut bytes = vec![0xFF; 0x1000];
        assert_eq!(memory.changed(0x0800_0000, &bytes), Some(Vec::new()));
        bytes[0x41] = 0;
        bytes[0x80] = 0;
        bytes[0xFFF] = 0;
        assert_eq!(
            memory.changed(0x0800_0000, &bytes),
            Some(vec![0x40..0xC0, 0xFC0..0x1000])
        );
        assert_eq!(memory.changed(0x0800_0FF0, &[0xFF; 0x20]), None);
    }

    /// Each region starts at its erased value, and `erased_unkept` gives back the ones the guest
    /// can't write that way, whatever was placed in them since.
    #[test]
    fn a_region_starts_erased_and_is_given_back_erased() {
        let mut memory = memory();
        assert_eq!(memory.read(0x0800_0FFC, Width::W32), Some(0xFFFF_FFFF));
        assert_eq!(memory.read(0x2000_0000, Width::W32), Some(0));
        assert!(memory.place(0x0800_0004, &[1, 2]));
        assert_eq!(memory.erased_unkept(), [(0x0800_0000, vec![0xFF; 0x1000])]);
    }

    #[test]
    fn an_alias_is_the_same_bytes() {
        let mut memory = memory();
        assert!(memory.write(0x0800_0010, Width::W32, 0xDEAD_BEEF));
        assert_eq!(memory.read(0x10, Width::W32), Some(0xDEAD_BEEF));
        assert_eq!(memory.host(0x10), memory.host(0x0800_0010));
    }

    #[test]
    fn every_region_is_mapped_at_its_address_and_its_aliases() {
        let mappings = memory().mappings();
        assert_eq!(
            mappings,
            [
                Mapping {
                    start: 0x0800_0000,
                    length: 0x1000,
                    writable: false
                },
                Mapping {
                    start: 0,
                    length: 0x1000,
                    writable: false
                },
                Mapping {
                    start: 0x2000_0000,
                    length: 0x100,
                    writable: true
                },
            ]
        );
    }

    #[test]
    fn a_region_that_ends_inside_a_page_is_found() {
        assert_eq!(memory().partial_page(0x100), None);
        assert_eq!(
            memory().partial_page(0x400).map(|mapping| mapping.start),
            Some(0x2000_0000)
        );
    }

    /// The core keeps a pointer into a region and writes through it between the runtime's own reads
    /// and writes, and both see the other's, which is what Miri holds this module to.
    #[test]
    fn a_pointer_the_core_keeps_and_the_runtimes_own_access_share_the_bytes() {
        let mut memory = memory();
        let kept = memory.host(0x2000_0010).unwrap();
        assert!(memory.place(0x2000_0010, &[1, 2]));
        // SAFETY: inside the region, which outlives the pointer.
        assert_eq!(unsafe { kept.read() }, 1);
        // SAFETY: as above.
        unsafe { kept.write(9) };
        assert_eq!(memory.read(0x2000_0010, Width::W8), Some(9));
        assert!(memory.write(0x2000_0010, Width::W8, 7));
        // SAFETY: as above.
        assert_eq!(unsafe { kept.read() }, 7);
    }

    #[test]
    fn an_access_that_runs_off_a_region_is_nothing() {
        let memory = memory();
        assert_eq!(memory.read(0x2000_00FE, Width::W32), None);
        assert_eq!(memory.read(0x3000_0000, Width::W8), None);
        assert!(memory.read_only(0x10));
        assert!(!memory.read_only(0x2000_0000));
        assert!(memory.writable(0x2000_0000));
    }
}