Skip to content

Commit ce5bcab

Browse files
committed
Add fuzzing, fix things that were discovered (#13)
Fixes #11, with three bugs discovered by fuzzing: - `_NIP2r` was incorrect in assembly, with two digits swapped - `_SWP2r` was incorrect in assembly, with a single digit mistake - `sft` was incorrect in the interpreter implementation, because wrapping_shl/shr doesn't return zeros if you shift out of range
1 parent baca465 commit ce5bcab

9 files changed

Lines changed: 279 additions & 26 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ members = [
66
"raven-cli",
77
"raven-gui",
88
]
9+
exclude = ["fuzz"]
910

1011
[workspace.dependencies]
1112
anyhow = "1.0.83"

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ The `raven-uxn` crate includes two implementations of the Uxn CPU:
2020
shims on either side), and runs 40-50% faster than the reference
2121
implementation
2222

23+
The native interpreter can be checked against the safe interpreter with fuzz
24+
testing:
25+
26+
```console
27+
cargo install cargo-fuzz # this only needs to be run once
28+
cargo +nightly fuzz run --release fuzz-native
29+
```
30+
2331
--------------------------------------------------------------------------------
2432

2533
The Varvara implementation (`raven-varvara`) includes all peripherals, and has
@@ -37,7 +45,7 @@ The repository includes two applications built on these libraries:
3745

3846
--------------------------------------------------------------------------------
3947

40-
© 2024 Matthew Keeter
48+
© 2024-2025 Matthew Keeter
4149
Released under the [Mozilla Public License 2.0](https://github.com/mkeeter/fidget/blob/main/LICENSE.txt)
4250

4351
The repository includes ROMs compiled from the `uxnemu` reference

fuzz/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
target
2+
corpus
3+
artifacts
4+
coverage

fuzz/Cargo.lock

Lines changed: 128 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fuzz/Cargo.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "fuzz"
3+
version = "0.0.0"
4+
publish = false
5+
edition = "2021"
6+
7+
[package.metadata]
8+
cargo-fuzz = true
9+
10+
[dependencies]
11+
libfuzzer-sys = "0.4"
12+
uxn = { path = "../raven-uxn", package = "raven-uxn", features = ["native"] }
13+
14+
[[bin]]
15+
name = "fuzz-native"
16+
path = "src/native.rs"
17+
test = false
18+
doc = false
19+
bench = false

fuzz/src/native.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#![no_main]
2+
3+
use libfuzzer_sys::fuzz_target;
4+
use uxn::{Backend, EmptyDevice, Uxn, UxnRam};
5+
6+
fuzz_target!(|data: &[u8]| {
7+
let mut ram_v = UxnRam::new();
8+
let mut vm_v = Uxn::new(&mut ram_v, Backend::Interpreter);
9+
10+
let mut ram_n = UxnRam::new();
11+
let mut vm_n = Uxn::new(&mut ram_n, Backend::Native);
12+
13+
// Don't load any programs that require auxiliary memory
14+
if !vm_v.reset(data).is_empty() {
15+
return;
16+
}
17+
assert!(vm_n.reset(data).is_empty());
18+
19+
// Use the VM-backed evaluator, halting if we take more than 65K cycles
20+
let Some(pc_v) =
21+
vm_v.run_until(&mut EmptyDevice, 0x100, |_uxn, _dev, i| i > 65536)
22+
else {
23+
return;
24+
};
25+
let pc_n = vm_n.run(&mut EmptyDevice, 0x100);
26+
27+
let mut failed = false;
28+
29+
if pc_v != pc_n {
30+
println!("PC mismatch: {pc_v:#04x} != {pc_n:#04x}");
31+
failed = true;
32+
}
33+
for i in 0..=65535 {
34+
let a = vm_v.ram_read_byte(i);
35+
let b = vm_n.ram_read_byte(i);
36+
if a != b {
37+
println!("RAM mismatch at {i:#04x}: {a:#02x} != {b:#02x}");
38+
failed = true;
39+
}
40+
}
41+
if vm_v.ret() != vm_n.ret() {
42+
println!(
43+
"return mismatch:\n bytecode: {:?}\n native: {:?}",
44+
vm_v.ret(),
45+
vm_n.ret()
46+
);
47+
failed = true;
48+
}
49+
if vm_v.stack() != vm_n.stack() {
50+
println!(
51+
"stack mismatch:\n bytecode: {:?}\n native: {:?}",
52+
vm_v.stack(),
53+
vm_n.stack()
54+
);
55+
failed = true;
56+
}
57+
if failed {
58+
print!("Instructions:\n ");
59+
for (i, d) in data.iter().enumerate() {
60+
print!(
61+
"{}{}",
62+
if i == 0 { "" } else { " " },
63+
uxn::op::NAMES[usize::from(*d)]
64+
);
65+
}
66+
println!();
67+
panic!("mismatch found");
68+
}
69+
});

raven-uxn/src/lib.rs

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const fn ret(flags: u8) -> bool {
2020
pub const DEV_SIZE: usize = 16;
2121

2222
/// Simple circular stack, with room for 256 items
23-
#[derive(Debug)]
23+
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
2424
pub struct Stack {
2525
data: [u8; 256],
2626

@@ -147,17 +147,17 @@ impl Value {
147147
}
148148
}
149149
#[inline]
150-
fn wrapping_shr(&self, i: u32) -> Self {
150+
fn shr(&self, i: u32) -> Self {
151151
match self {
152-
Value::Short(v) => Value::Short(v.wrapping_shr(i)),
153-
Value::Byte(v) => Value::Byte(v.wrapping_shr(i)),
152+
Value::Short(v) => Value::Short(v.checked_shr(i).unwrap_or(0)),
153+
Value::Byte(v) => Value::Byte(v.checked_shr(i).unwrap_or(0)),
154154
}
155155
}
156156
#[inline]
157-
fn wrapping_shl(&self, i: u32) -> Self {
157+
fn shl(&self, i: u32) -> Self {
158158
match self {
159-
Value::Short(v) => Value::Short(v.wrapping_shl(i)),
160-
Value::Byte(v) => Value::Byte(v.wrapping_shl(i)),
159+
Value::Short(v) => Value::Short(v.checked_shl(i).unwrap_or(0)),
160+
Value::Byte(v) => Value::Byte(v.checked_shl(i).unwrap_or(0)),
161161
}
162162
}
163163
}
@@ -400,21 +400,45 @@ impl<'a> Uxn<'a> {
400400
#[inline]
401401
pub fn run<D: Device>(&mut self, dev: &mut D, mut pc: u16) -> u16 {
402402
match self.backend {
403-
Backend::Interpreter => {
404-
loop {
405-
let op = self.next(&mut pc);
406-
let Some(next) = self.op(op, dev, pc) else {
407-
break;
408-
};
409-
pc = next;
410-
}
411-
pc
412-
}
403+
Backend::Interpreter => loop {
404+
let op = self.next(&mut pc);
405+
let Some(next) = self.op(op, dev, pc) else {
406+
break pc;
407+
};
408+
pc = next;
409+
},
413410
#[cfg(feature = "native")]
414411
Backend::Native => native::entry(self, dev, pc),
415412
}
416413
}
417414

415+
/// Runs until the program terminates or we hit a stop condition
416+
///
417+
/// Returns the new program counter if the program terminated, or `None` if
418+
/// the stop condition was reached.
419+
///
420+
/// This function always uses the interpreter, ignoring
421+
/// [`self.backend`](Self::backend).
422+
#[inline]
423+
pub fn run_until<D: Device, F: Fn(&Self, &D, usize) -> bool>(
424+
&mut self,
425+
dev: &mut D,
426+
mut pc: u16,
427+
stop: F,
428+
) -> Option<u16> {
429+
for i in 0.. {
430+
let op = self.next(&mut pc);
431+
let Some(next) = self.op(op, dev, pc) else {
432+
return Some(pc);
433+
};
434+
pc = next;
435+
if stop(self, dev, i) {
436+
return None;
437+
}
438+
}
439+
unreachable!()
440+
}
441+
418442
/// Converts raw ports memory into a [`Ports`] object
419443
#[inline]
420444
pub fn dev<D: Ports>(&self) -> &D {
@@ -1638,7 +1662,7 @@ impl<'a> Uxn<'a> {
16381662
let shr = u32::from(shift & 0xF);
16391663
let shl = u32::from(shift >> 4);
16401664
let v = s.pop();
1641-
s.push(v.wrapping_shr(shr).wrapping_shl(shl));
1665+
s.push(v.shr(shr).shl(shl));
16421666
Some(pc)
16431667
}
16441668
}
@@ -1728,9 +1752,9 @@ pub use ram::UxnRam;
17281752

17291753
////////////////////////////////////////////////////////////////////////////////
17301754

1731-
#[cfg(test)]
1732-
#[allow(unused, non_upper_case_globals)]
1733-
mod op {
1755+
/// Opcode names and constants
1756+
#[allow(non_upper_case_globals, missing_docs)]
1757+
pub mod op {
17341758
pub const BRK: u8 = 0x0;
17351759
pub const INC: u8 = 0x1;
17361760
pub const POP: u8 = 0x2;

raven-uxn/src/native/aarch64.s

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -877,9 +877,9 @@ _NIP2r:
877877
ldrb w10, [x2, x3]
878878
rpop
879879
strb w9, [x2, x3]
880-
sub x11, x1, #1
880+
sub x11, x3, #1
881881
and x11, x11, #0xff
882-
strb w10, [x2, x31]
882+
strb w10, [x2, x11]
883883
next
884884

885885
_SWP2r:
@@ -889,7 +889,7 @@ _SWP2r:
889889
strb w12, [x2, x3]
890890

891891
rpeek w11, x9, 1
892-
rpeek w12, x10, 2
892+
rpeek w12, x10, 3
893893

894894
strb w11, [x2, x10]
895895
strb w12, [x2, x9]

0 commit comments

Comments
 (0)