Saturday, 4 October 2025

Simple Memory Protection Scheme

Using LLVM has me wanting to implement a simple scheme that enforces memory protection, so that I can more easily detect memory corruption events, in part because of the unfortunate (although understandable) way that LLVM stores its stack pointer in zero-page at addresses $02 and $03, which renders them succeptible to easy corruption, which then results in all manner of down-stream corruption.  

While protecting the stack pointer itself would require munging with LLVM's code generator, I can at least make it possible to write-protect code and read-only data segments, wired so that they simulate a BRK instruction if attempted to be written to. 

Tracking via Issue #921

I'm going to have to work out how to do this in a way that can survive freeze and unfreeze, ideally without changing the frozen process image. But that can wait.

We'll start by defining what I want it to do, which I think is fairly simple:

1. Allow two ranges, each of which define a write-protected region.

2. Two flags that enable each write protected region.

3. A bit field that indicates whether each region should trigger a hypervisor trap (to freezer), an BRK-like IRQ,  or something else.

It was all a lot more mucking about to get working, due to some weird glitching causing false-positives. I've fixed those, and I can now cause an interrupt on a write to either protected region. However, the writes are still occurring, at least to chip RAM, which is hardly ideal.

That would because we have this weird split regime that got added in when we moved from the old synthesis tool, whose name I can't even remember at the moment, to Vivado.  The old one allowed a slightly weird BRAM timing configuration that was deeply depended on in the CPU design, and it was solved by splitting the whole thing into these two separate processes. But this means that our detection of the write violation and our inhibiting of the write is now split over two separate processes.

So I need some simple way to fix this, without messing up timing.

Well, I've got it enforcing for chip RAM now, but not IO. But I can live with that.

I doubt that this will make it into development, but who knows.  But here's the registers (write-only) for this:

$FFD5000-1 = low address (inclusive) for write-protect region 0
$FFD5002-3 = high address (inclusive) for write-protect region 0
$FFD5004-5 = low address (inclusive) for write-protect region 1
$FFD5006-7 = high address (inclusive) for write-protect region 1
$FFD5008 bit 0 = enable write protection region 0
$FFD5008 bit 4 = enable write protection region 1
$FFD5008 bit 1-3 = write protection region 0 violation angle: 111 = nothing, 000 = simulate BRK, 001 = NMI, 010 = trigger freezer.
$FFD5008 bit 5-7 = write protection region 1 violation angle: 111 = nothing, 000 = simulate BRK, 001 = NMI, 010 = trigger freezer.

Writing to any of $FFD5000-$5007 disables write protection for both regions. As does entering the freezer.

So for example we can do:

sffd5000 0 8 10 8 0 0 0 0 1

And that will write-protect $0800-$0810 inclusive, and trigger a fake BRK (which will trigger the MEGA65 ROM monitor by default).

So now I can make a little shim for LLVM that extracts the address range of the code and rodata segments, and then enforces write-protection.  Well, it felt like it should be possible to do with the linker, but I didn't have the time to dig deep, so I just made some python that parses the map file for the program (which I already had to add symbol tables to allow debug symbols in the natively generated stack backtraces on BRK instruction) to also make the 9-byte vector of values to get put at $FFD5000 to setup the write protection.

And with the latest commits to everything, it now works -- and I get a stack backtrace generated whenever the code or read-only data area gets written to :)

So now it's back to fixing the remaining bugs with the LLVM transition for the telephony software... 



 

 

 


No comments:

Post a Comment