aboutsummaryrefslogtreecommitdiff
path: root/lib/xixanta
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mikisabate@gmail.com>2025-09-03 19:08:05 +0200
committerMiquel Sabaté Solà <mikisabate@gmail.com>2025-09-03 20:45:54 +0200
commit9dcbf460ff692c049e4d327afcbdbc39ba4e8c65 (patch)
tree1d96e2bccc566a3092768da691e7ca47881880de /lib/xixanta
parenta3989fb9c5424f2f93c67669102ee8cb61303c24 (diff)
downloadtools.nes-9dcbf460ff692c049e4d327afcbdbc39ba4e8c65.tar.gz
tools.nes-9dcbf460ff692c049e4d327afcbdbc39ba4e8c65.zip
Validate that memory access is done via variables
The address sanitizer is now able to detect whenever in an instruction a memory access is done without using variables. This is now detected for all instructions except for branching, which falls outside of this scope. Moreover, simple arithmetics is allowed and bounds are checked for simple cases. That being said, more involved bound checks should be done with other tools (e.g. emulators). Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
Diffstat (limited to 'lib/xixanta')
-rw-r--r--lib/xixanta/src/assembler.rs136
-rw-r--r--lib/xixanta/src/node.rs30
2 files changed, 157 insertions, 9 deletions
diff --git a/lib/xixanta/src/assembler.rs b/lib/xixanta/src/assembler.rs
index f63e785..09f0c9d 100644
--- a/lib/xixanta/src/assembler.rs
+++ b/lib/xixanta/src/assembler.rs
@@ -1,5 +1,8 @@
use crate::mapping::{get_mapping_configuration, Mapping};
-use crate::node::{CommentType, ControlType, EchoKind, NodeType, OperationType, PNode, PString};
+use crate::node::{
+ is_asan_friendly_name, CommentType, ControlType, EchoKind, NodeType, OperationType, PNode,
+ PString,
+};
use crate::object::{Bundle, Context, Object, ObjectType};
use crate::opcodes::{AddressingMode, INSTRUCTIONS};
use crate::parser::Parser;
@@ -856,9 +859,10 @@ impl<'a> Assembler<'a> {
}
// If it doesn't have the proper prefix, skip as well.
- if !name.starts_with("zp_") && !name.starts_with("m_") && !name.starts_with("wr_") {
+ if !is_asan_friendly_name(name) {
continue;
}
+ let actual_name = name.split("::").last().unwrap_or("");
// Build up the memory range object for this bundle.
let val = bundle.bundle.value() as usize;
@@ -902,7 +906,7 @@ impl<'a> Assembler<'a> {
// Increase the counters for memory usage on either RAM slot and
// check for bounds.
- if name.starts_with("zp_") || name.starts_with("m_") {
+ if actual_name.starts_with("zp_") || actual_name.starts_with("m_") {
memory.total_internal_ram += bundle.asan_reserve as usize;
if memory.total_internal_ram > 0x800 {
@@ -913,7 +917,7 @@ impl<'a> Assembler<'a> {
source: self.sources[0].clone(),
});
}
- } else if name.starts_with("wr_") {
+ } else if actual_name.starts_with("wr_") {
memory.total_working_ram += bundle.asan_reserve as usize;
if memory.total_working_ram > 0x2000 {
@@ -2318,7 +2322,8 @@ impl<'a> Assembler<'a> {
});
}
- let val = self.evaluate_node(left.left.as_ref().unwrap())?;
+ let evaluated_node = left.left.as_ref().unwrap();
+ let val = self.evaluate_node(evaluated_node)?;
if val.size != 1 {
return Err(Error {
message: "address can only be one byte long on indirect Y addressing"
@@ -2328,6 +2333,7 @@ impl<'a> Assembler<'a> {
global: false,
});
}
+ self.asan_check_arm(evaluated_node, &val)?;
return Ok((AddressingMode::IndirectY, val));
}
Err(Error {
@@ -2340,7 +2346,8 @@ impl<'a> Assembler<'a> {
None => match left.right.as_ref() {
Some(right) => {
if right.value.value.trim().to_lowercase() == "x" {
- let val = self.evaluate_node(left.left.as_ref().unwrap())?;
+ let evaluated_node = left.left.as_ref().unwrap();
+ let val = self.evaluate_node(evaluated_node)?;
if val.size != 1 {
return Err(Error {
message:
@@ -2351,6 +2358,7 @@ impl<'a> Assembler<'a> {
global: false,
});
}
+ self.asan_check_arm(evaluated_node, &val)?;
return Ok((AddressingMode::IndirectX, val));
}
Err(Error {
@@ -2361,7 +2369,8 @@ impl<'a> Assembler<'a> {
})
}
None => {
- let val = self.evaluate_node(left.left.as_ref().unwrap())?;
+ let evaluated_node = left.left.as_ref().unwrap();
+ let val = self.evaluate_node(evaluated_node)?;
if val.size != 2 {
return Err(Error {
message: "expecting a full 16-bit address".to_string(),
@@ -2370,6 +2379,7 @@ impl<'a> Assembler<'a> {
global: false,
});
}
+ self.asan_check_arm(evaluated_node, &val)?;
Ok((AddressingMode::Indirect, val))
}
},
@@ -2398,6 +2408,8 @@ impl<'a> Assembler<'a> {
let right = node.right.as_ref().unwrap();
match right.value.value.to_lowercase().trim() {
"x" => {
+ self.asan_check_arm(left, &val)?;
+
// If the size == 2 but we can fit it on a single byte (i.e.
// because the second byte is just 0x00), then just "compress"
// this instruction. Note that this is only valid if the value
@@ -2429,6 +2441,8 @@ impl<'a> Assembler<'a> {
}
}
"y" => {
+ self.asan_check_arm(left, &val)?;
+
// Same optimization as with the "x" case.
if val.size == 1 || (val.resolved && val.bytes[1] == 0x00) {
// Similar to the case on "x" indexing.
@@ -2471,10 +2485,13 @@ impl<'a> Assembler<'a> {
Some(LiteralMode::Hexadecimal) => {
// As for checking the most significant byte, it's the same
// optimization as with absolute to zeropage indexed addressing.
- if base.is_branch()
- || val.size == 1
+ if base.is_branch() {
+ val.size = 1;
+ Ok((AddressingMode::RelativeOrZeropage, val))
+ } else if val.size == 1
|| (val.bytes[1] == 0x00 && !matches!(base.value.value.as_str(), "jmp" | "jsr"))
{
+ self.asan_check_arm(left_arm, &val)?;
val.size = 1;
Ok((AddressingMode::RelativeOrZeropage, val))
} else {
@@ -2508,6 +2525,107 @@ impl<'a> Assembler<'a> {
}
}
+ // Check the `node` considering is an instructions' "left" arm. More than
+ // that, it assumes that it's a memory access. Hence, only call this
+ // function if the addressing mode allows for it. Moreover, a `value` is
+ // also supplied to perform further checks on the given memory access.
+ //
+ // Note that it will only run if the address sanitizer is enabled, and it
+ // will check that any access only occurs as a variable or a simple pointer
+ // arithmetic. Also note that this function assumes that it's not being
+ // called by `jmp` or `jsr` kind of instructions on the absolute addressing
+ // mode, as they operate on another level.
+ fn asan_check_arm(&mut self, node: &PNode, value: &Bundle) -> Result<(), Error> {
+ if !self.asan_enabled {
+ return Ok(());
+ }
+
+ match node.node_type {
+ NodeType::Value => {
+ let name = &node.value.value;
+ if !is_asan_friendly_name(name) {
+ // If it's referencing an actual ObjectType::Address, then
+ // let it be (e.g. 'lda palettes, x'; where 'palettes' is a
+ // legitimate name even if not 'is_asan_friendly_name').
+ if let Ok(var) = self.context.get_variable(&node.value, &self.mappings) {
+ if matches!(var.object_type, ObjectType::Address) {
+ return Ok(());
+ }
+ }
+
+ // Everything has been exhausted, this is actually not a
+ // good name access.
+ self.warnings.push(Error {
+ line: node.value.line,
+ message: format!(
+ "accessing a memory region without a proper name ('{name}')"
+ ),
+ source: self.source_for(node),
+ global: false,
+ });
+ }
+ Ok(())
+ }
+ NodeType::Operation(OperationType::Add) | NodeType::Operation(OperationType::Sub) => {
+ // Get the name of the variable involved.
+ let left_name = &node.left.as_ref().unwrap().value.value;
+ let right_name = &node.left.as_ref().unwrap().value.value;
+ let name = if is_asan_friendly_name(left_name) {
+ node.left.as_ref().unwrap()
+ } else if is_asan_friendly_name(right_name) {
+ node.right.as_ref().unwrap()
+ } else {
+ self.warnings.push(Error {
+ line: node.value.line,
+ message: "accessing a memory region without a proper name".to_string(),
+ source: self.source_for(node),
+ global: false,
+ });
+ return Ok(());
+ };
+
+ // Fetch the variable. If it doesn't exist or its value hasn't
+ // been resolved yet, we will ignore this check.
+ match self.context.get_variable(&name.value, &self.mappings) {
+ Ok(object) => {
+ // If it has a resolved value, then check that the
+ // pointer arithmetics are within reserved bounds.
+ if object.bundle.resolved {
+ let original_value = object.bundle.value() as usize;
+ let given_value = value.value() as usize;
+ let end = original_value + object.asan_reserve as usize;
+
+ if given_value < original_value || given_value >= end {
+ let range = MemoryRange {
+ name: name.value.value.clone(),
+ range: (original_value..end),
+ };
+ return Err(Error {
+ line: node.value.line,
+ message: format!("out of bounds memory access for {range}"),
+ source: self.source_for(node),
+ global: false,
+ });
+ }
+ }
+ Ok(())
+ }
+ Err(_) => Ok(()),
+ }
+ }
+ _ => {
+ self.warnings.push(Error {
+ line: node.value.line,
+ message: "accessing a memory region without using a variable".to_string(),
+ source: self.source_for(node),
+ global: false,
+ });
+
+ Ok(())
+ }
+ }
+ }
+
fn to_relative_address(&self, node: &PNode, bundle: &mut Bundle) -> Result<(), Error> {
if !bundle.resolved {
return Ok(());
diff --git a/lib/xixanta/src/node.rs b/lib/xixanta/src/node.rs
index 7cfc107..465dde2 100644
--- a/lib/xixanta/src/node.rs
+++ b/lib/xixanta/src/node.rs
@@ -1,5 +1,13 @@
use std::fmt;
+/// Returns true if this String follows the naming
+/// conventions as expected by the address sanitizer.
+pub fn is_asan_friendly_name(string: &str) -> bool {
+ let name = string.split("::").last().unwrap_or("");
+
+ name.starts_with("zp_") || name.starts_with("wr_") || name.starts_with("m_")
+}
+
/// A Positioned String. That is, a String which also has information on the
/// line number and the column range.
#[derive(Debug, Default, Clone, PartialEq)]
@@ -456,3 +464,25 @@ impl PNode {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn is_asan_friendly_name_test() {
+ let tests = vec![
+ ("zp_name", true),
+ ("name", false),
+ ("Scope::zp_name", true),
+ ("Scope::Inner::zp_good", true),
+ ("Scope::Inner::bad", false),
+ ];
+
+ for (name, expect) in tests {
+ let string = String::from(name);
+
+ assert_eq!(is_asan_friendly_name(&string), expect);
+ }
+ }
+}