The NX bit is not just about security
Posted by torutofu 4 days ago
Comments
Comment by repiret 2 days ago
It's crazy that Armv8/9-A permits speculative instruction fetches to device memory. Crazy enough that I had to look it up to believe it:
"Hardware does not prevent speculative instruction fetches from a memory location with any of the Device memory attributes unless the memory location is also marked as execute-never for all Exception levels." - ARM DDI 0487K.a § B2.15.2
The Armv7-M spec is less clear. It does say: "The architecture does not permit speculative accesses to memory marked as Device," in contrast to Armv8/9-A which qualifies a similar statement with "data accesses". But then it later says "To ensure correctness, read-sensitive locations must be marked as non-executable". (This is all from ARM DDI 0403E.e § A3.5.7)
I don't have v7-A older handy to compare what they say.
Comment by asveikau 2 days ago
But then, a lot of correctness bugs like that are also security problems.
Comment by sleirsgoevy 1 day ago
Comment by dmitrygr 2 days ago
https://support.arm.com/documentation/100941/0101/Memory-typ...
Comment by achierius 2 days ago
A nitpick, but this is only true for some vendors, depending on their license, and is very much company-by-company. Many vendors, even big names like Meta, don't have the ability to roll their own. And even for the ones who do, 'customize at will' is a bit strong, as ARM very much does want to maintain uniformity across userspace implementations. E.g. Nvidia shouldn't add new traps for architecturally-legal behavior, since then code compiled for Apple hardware wouldn't work on Grace. Or worse, not trap for architecturally-illegal behavior, since then code compiled for Grace might not work for anyone else at all!
Comment by adrian_b 2 days ago
So by "vendors" it was indeed meant "some vendors" who can afford to not care much about compatibility with the specification.
Comment by my123 1 day ago
iirc it's a documented feature now - FEAT_E2H0, https://support.arm.com/documentation/109697/2025_12/Feature...
And it was retroactively defined to be allowed starting from Armv8.0.
Apple designs pre-date the ID register bit for it being a thing so it takes a quirk there however.
Comment by sleirsgoevy 1 day ago
Comment by achierius 1 day ago
Regardless what I mean to convey is that ARM would prefer such deviations to be rare, and especially for them to not be visible from userspace. Deviations will always exist if only due to hardware bugs, so contracts can only do so much.
Comment by mubbicles 2 days ago
Comment by anyfoo 1 day ago
In the case that I know that has a 1:1 identity mapping of the address space, the physical address space doesn't actually have anything at 0 either, so it still doesn't need to be mapped.
Comment by sleirsgoevy 1 day ago
Comment by ValdikSS 3 hours ago
It could not make it work reliable. No matter which barries I set, I could not understand why is it not reliable, and had to remove the modifying code in the end.
I guess I hit the issue you've described, and now I know. Thanks!
Comment by eqvinox 2 days ago
Comment by phire 1 day ago
It's simply speculative execution. Which doesn't look any different to regular execution. The fetcher has no idea that its predicted branch is about to invalidated and flushed, otherwise it would never have issued that fetch.
Actually, on a modern OoO core, [0] it's very rare for the instruction fetcher to not be doing speculative fetches. Even when it's not predicting a branch, the fact that it has "predicted" the lack of a branch is speculative in itself. It assumes it didn't fetch a branch in the last cycle, but it can't be sure until after instruction decoding, which takes at least 2 cycles (more on larger L1i caches).
About the only time the instruction fetcher is not doing speculative fetching is for a single cycle after each miss-predicted branch.
[0] Or even something technically in-order, like the Cortex A53 cores here. They might issue in-order, but because of how they implement dual issue, they look somewhat close to a simple OoO core... I suspect they actually do register renaming. And (most importantly) importantly they have a branch predictor.
Comment by repiret 1 day ago
Comment by phire 1 day ago
But the original question was asking why disabling data prefetching to a memory region didn't automatically disable instruction prefetching at the same time.
And the answer is that speculative execution is a completely different mechanism that I'm not even sure can be disabled, at least not per memory region.
Comment by renox 1 day ago
Comment by sleirsgoevy 1 day ago
Comment by phire 1 day ago
The "how" it does dual-issue is not documented at all, so I'm speculating. There is no smoking gun saying "register renaming". While it would be possible to implement its known capabilities without any kind of renaming, it would be so much simpler to implement it with register renaming.
The thing is.. once your forwarding network and hazard detection gets complicated enough, it basically becomes a janky form of register renaming. So it's cleaner to just implement proper register renaming, and actually saves hardware.
While the A53 might issue in-order, the pipelines have different lengths and they don't finish executing in order.
I find the fact the A53 can speculate a few instructions past a cache miss to be very interesting, along with the fact it can issue two writes to the same logical register in a single cycle.
Also, the smoking gun is that the A510 (same lineage as the A53) is documented to do out-of-order issue (see chipsandcheese again [1]), so it must be doing register renaming. ARM still insist on still calling it an in-order core because it's OoO is so much more limited to modern OoO cores, but it's more OoO than early PowerPC designs (including the G3) that everyone is happy applying the OoO label to.
[0] https://chipsandcheese.com/p/arms-cortex-a53-tiny-but-import... [1] https://chipsandcheese.com/p/arms-cortex-a510-two-kids-in-a-...
Comment by achierius 2 days ago
Comment by adrian_b 2 days ago
To the front end, one needs to pipe only appropriate logical combinations of the flags that describe the memory properties, not the flags as they are stored in some control register.
So a control register should contain flags that are meaningful for the programmer, while the kind of flags that hardware happens to need can be generated with a few logic gates from them and routed through hardware wherever they are needed.
It makes more sense to classify the memory in a few types, which must be specified by the programmer, including a "device memory" a.k.a. "memory-mapped peripherals" type, instead of having to specify for each memory area a long set of attributes about each kind of access that may be allowed, or not.
This is how it is done in x86-64.
Comment by sleirsgoevy 1 day ago
Comment by adrian_b 1 day ago
Standard UARTs are accessed in the x86 I/O space, which is uncacheable and strongly ordered, not in the normal memory space.
Non-standard UARTs that are on PCIe add-on cards might be mapped into memory, but when the computer boots, the BIOS already maps the PCIe memory as uncacheable, with the Memory Type Range Registers.
So if you do nothing, you get uncacheable memory for your UART, as you should.
To map the UART in normal memory, i.e. write-back cacheable, you must do this explicitly in the UART device driver, either by allocating a dedicated MTRR for it, or more likely, by locking a memory page for it in the virtual address translation tables and using the PAT bits to set the memory type (Page Attribute Table).
If you really mapped the UART as normal memory, i.e. write-back cacheable, it can work correctly only if you are incredibly lucky, because the writes to the UART control registers will not happen when you do them in your program, so I do not believe that you did that. With normal memory, not only you cannot predict when the UART registers will be written, but if you do multiple writes to the same register, all but the last will be omitted, so there is no way for the UART to work correctly.
On x86-64, any memory-mapped peripheral must be mapped into uncacheable memory, except for memory banks that are located on PCIe cards, like the GPU memories, which should be mapped as write-combining memory.
Comment by tripdout 2 days ago
Comment by zephen 2 days ago
When it's done incompetently as on this ARM implementation, then you can't even run perfectly good and correct code, because the CPU will attempt speculative execution on a location that you never asked it to execute code at, and then bork itself when it realizes that can't possibly work.
Naturally, this is the sort of problem that requires tedious dissection of what exactly happened, and copious amounts of alcohol.
Comment by MBCook 2 days ago
Interrupt(?) fires to trigger hypervisor, hypervisor figures out what it needs to do, jumps to that code, does its job, returns.
The “figured out what it needs to do” is the issue right? So what was actually happening was:
Same start… CPU predicts what hypervisor will do, speculatively loads instructions from mispredicted branch target, that wrong instruction reads memory(?) against the “no data prefetch” settings for that part of memory, CPU blows up/halts/whatever.
The fix is to mark the area the branch was mispredicted to in such a way that the CPU won’t prefetch instructions. Thus that won’t be run and prefetch data, thus no violation. CPU execution continues taking the correct branch and everything is fine.
Comment by sleirsgoevy 1 day ago
It does not really matter how execution ended up in the HV in the first place. The misprediction happens due to having a branch-to-register instruction.
Comment by MBCook 1 day ago
Comment by zephen 1 day ago
He had to also mark the memory as inaccessible for code loads.
Which, honestly is kinda stupid. Because nobody asked the processor to start executing there. It just took it upon itself to try to start executing there, later decided that was a bad thing, and then borked itself.
People talk about how terrible x86 is, but honestly, one of the reasons that x86 won for decades was because of making things that programmers did that might be suboptimal still work, even if a bit slowly (misaligned data accesses, for example). ARM does that now for that specific case, but didn't before 2002.
So there's an implementation tradeoff for whether you decide to spend transistors to reduce the number of sharp edges on the tool. Obviously ARM just doesn't give a shit about this particular sharp edge.
Comment by repiret 2 days ago
The Armv8/9-A architecture reference manual is clear that speculative instruction fetches are permitted in Device memory unless that memory is also marked NX. So if your hardware has side-effects from a certain address, but it maps it as Device non-NX memory, then your code is not "perfectly good and correct". Assigning correct memory attributes is one of the many things needed for correct code.
Comment by zephen 1 day ago
Sure, but (a) the speculation is supposed to be transparent; and (b) ARM's response to a failure in speculative execution is arguably broken.
> The Armv8/9-A architecture reference manual
Sure, but before then marking memory as not readable would make it non-executable, and also mostly before then there was no or minimal speculation.
> then your code is not "perfectly good and correct".
Certainly it won't work on that machine. It might have worked perfectly well on prior machines, and we could argue all day about how reasonable ARM's implementation decisions were, but I will merely say that a documented flaw is, in fact, still a flaw.
Comment by Nail2680 2 days ago
Comment by RiverCrochet 2 days ago
Comment by jnwatson 2 days ago
Comment by adrian_b 2 days ago
So Arm did not add another means to disable this kind of speculative execution, after Spectre was discovered, but they just reused the existing NX flag, expanding its functionality.
Comment by samus 1 day ago
Comment by yuxinking 1 day ago
Comment by crazy1_ 2 days ago
Comment by Nail2680 2 days ago
Comment by neerajsi 2 days ago
JIT is important to Apple platforms, and they seem to manage to make it work well enough even with the need for explicit invalidation.
Comment by achierius 2 days ago
Comment by Nail2680 2 days ago
Comment by Nail2680 2 days ago
Comment by RiverCrochet 2 days ago
Comment by Nail2680 2 days ago
Comment by RiverCrochet 1 day ago
Now, if the RAM in the CPU cache gets computational ability as well, that all changes, but not sure really that would be better than SIMD/SEE/AVX stuff or expanding what can be done with the new tile registers Intel's rolling out.
Comment by Nail2680 2 days ago
Comment by eqvinox 2 days ago
(Or did you confuse cache flushing with TLB flushing? The remap does the latter, not the former.)
Comment by Nail2680 2 days ago