aboutsummaryrefslogtreecommitdiff
path: root/lib/vnf/src
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mssola@mssola.com>2026-10-09 21:45:05 +0200
committerMiquel Sabaté Solà <mssola@mssola.com>2026-10-09 21:45:05 +0200
commitf6df4314945c1d37d314a7c88553b97b23c748e9 (patch)
tree20e71c89c25f469a83c52528dfdfe0ed71a8c3e1 /lib/vnf/src
parentaa5174fc055dc66e78a8aa7030c018e43cf588a1 (diff)
downloadtools.nes-f6df4314945c1d37d314a7c88553b97b23c748e9.tar.gz
tools.nes-f6df4314945c1d37d314a7c88553b97b23c748e9.zip
vnf,runrom: detect bad data access
This includes detection for: - Stack underflows (it was theoretically already there but it had never been properly tested). - Load/store out of bounds for a given variable known via the .nasm/ directory. - Load out of bounds on a set of data referenced via a raw label (see commit aa5174fc055d ("xixanta: Add an hint for the ending of data labels")). Signed-off-by: Miquel Sabaté Solà <mssola@mssola.com>
Diffstat (limited to 'lib/vnf/src')
-rw-r--r--lib/vnf/src/lib.rs65
1 files changed, 61 insertions, 4 deletions
diff --git a/lib/vnf/src/lib.rs b/lib/vnf/src/lib.rs
index e5e280a..a6e9b62 100644
--- a/lib/vnf/src/lib.rs
+++ b/lib/vnf/src/lib.rs
@@ -400,6 +400,11 @@ pub struct Machine {
/// The maximum amount of cycles that the machine should reach before
/// halting.
pub cycle_limit: Option<usize>,
+
+ /// List of addresses for which we have information on their upper and lower
+ /// bounds. This is used to detect overflows/underflows and any kind of bad
+ /// indexed access.
+ pub addresses: Vec<xixanta::assembler::Range>,
}
// On success, returns a vector of MemoryCell representing the RAM for a
@@ -516,6 +521,7 @@ impl Machine {
joypads: [Joypad::default(), Joypad::default()],
halt_on_brk: true,
cycle_limit: None,
+ addresses: vec![],
})
}
@@ -768,7 +774,13 @@ impl Machine {
/// Run until the program counter reaches the given 'address'.
pub fn until_address(&mut self, address: u16) -> Result<(), String> {
while self.pc != address as usize && self.active {
- self.next_iteration()?;
+ if let Err(e) = self.next_iteration() {
+ if self.verbose {
+ println!("\n@:");
+ self.report();
+ }
+ return Err(e);
+ }
}
Ok(())
@@ -833,7 +845,7 @@ impl Machine {
// And update the stack pointer if possible.
self.s -= 1;
- if self.s == self.policy.minimum_stack_value {
+ if self.s < self.policy.minimum_stack_value {
return Err("stack underflow!".to_string());
}
@@ -1422,6 +1434,51 @@ impl Machine {
Ok(byte)
}
+ // Given an address, try to fetch its range as it may appear in
+ // 'self.addresses'. This function will make a best effort to pick a Range
+ // if two or more appear to overlap.
+ fn fetch_address_range(&mut self, address: usize) -> Option<&xixanta::assembler::Range> {
+ let mut ret: Option<&xixanta::assembler::Range> = None;
+
+ for mr in &self.addresses {
+ if mr.range.contains(&address) {
+ if let Some(existing) = ret {
+ // If there is already a range that was picked, discard
+ // whichever is furthest from the start. Typically this
+ // happens in subroutines with data labels within it. In
+ // this case both the subroutine and the data label will
+ // overlap, but if the address is closest to the data label,
+ // then we can safely assume that it's the data label being
+ // referenced.
+ if existing.range.start > mr.range.start {
+ continue;
+ }
+ }
+ ret = Some(mr);
+ }
+ }
+ ret
+ }
+
+ // Returns the address pointed by the current instruction's value plus the
+ // given index while making sure that the final address does not reach out
+ // of bounds if it's a known address range.
+ fn safe_indexed_address(&mut self, index: usize) -> Result<usize, String> {
+ let base = self.current_instruction.value();
+ let target = base + index;
+
+ if let Some(range) = self.fetch_address_range(base)
+ && !range.range.contains(&target)
+ {
+ return Err(format!(
+ "out of bounds: indexed access to ${:04X} falls out of '{}' \
+ (${:02X}-${:02X}; bounded inclusively below and exclusively above)",
+ target, range.name, range.range.start, range.range.end
+ ));
+ }
+ Ok(target)
+ }
+
// Returns the effective address which the current instruction is
// targetting.
fn target_address(&mut self) -> Result<usize, String> {
@@ -1430,10 +1487,10 @@ impl Machine {
Ok(self.current_instruction.value())
}
AddressingMode::ZeropageIndexedX | AddressingMode::IndexedX => {
- Ok(self.current_instruction.value() + self.x as usize)
+ self.safe_indexed_address(self.x as usize)
}
AddressingMode::ZeropageIndexedY | AddressingMode::IndexedY => {
- Ok(self.current_instruction.value() + self.y as usize)
+ self.safe_indexed_address(self.y as usize)
}
AddressingMode::IndirectY => {
let ptr = self.current_instruction.value() as u16;