Debugging an evil Go runtime bug
marcan.st
marcan.st
From my perspective this approach was pretty unique; going all the way down to debugging the hardware first may seem obvious to some, but it's a totally opposite approach to how I'd go about it. My mind would jump directly to producing a minimal test case. Would've never thought to mark off bad RAM with an obscure(ish?) GRUB 2 feature. Would've never thought to selectively flip a kernel flag for some parts of the code.
It's great to get these perspectives from people who really know how to dig down and debug deep.
That's not to say I spend a whole lot of time looking at the lower levels, but my quick mental checklist starts off down at physical points, and I try to quickly eliminate possibilities. In a lot of cases it's obvious it's a code / logic bug, and you can completely skip the lower layer stuff, but making it a conscious step pays off.
Btw, thanks about telling me about the badram Grub 2 feature, had no idea that exited.
> I tried setting GOMAXPROCS=1, which tells Go to only use a single OS-level thread to run Go code. This also stopped the crashes, again pointing strongly to a concurrency issue.
I think I would have stopped there.
Turns out somebody else did, too: https://twitter.com/bcantrill/status/774290166164754433?lang...
/edit: spelling
Neat. I wonder if that makes Rowhammer more likely to occur.
Think of it as SNR (signal-noise-ratio) -- increasing temperature increases thermal noise (there are other kinds), and with the same signal, it should actually reduce the efficiency of the side channel.
But it brings up a good question, I wonder if anyone has studied this...
This "at all times" is most relevant when you have interrupts in your execution that you can't predict. When an interrupt runs, you can't know how much of the redzone has been used, so you assume 128 bytes below the current RSP value.
> Also, how can a vDSO function "allocate" stack?
By updating RSP and using the stack properly, that is what I mean with allocating the stack. It's normal code.
> It would have to know about the current stack space as configured by the go runtime, and somehow dig into this go-runtime-specific record of the current stack limit?
No, you simply use push and pop as usual. The go routine sets up the stack, the bug in the blogpost is that the go runtime assumed the vDSO would only use a couple bytes on the stack, but it used more because it did a stack probe about 4 kilobytes into the stack.
>Isn't the only available option for any exported function just to /use/ pre-allocated stack space (by subtracting from rsp) - I don't see how it could possibly extend the pre-allocated stack.
Not at all, as previously mentioned, stack is simply the value in RSP, you can update that as you want. PUSH and POP the values you need and the OS will implicitly allocate memory for the stack as needed.
I was just wondering, with the Red Zone in the ABI specification requiring 128 bytes below RSP, wouldn't that also mean any function can assume the 128 bytes below RSP are actually mapped? If Go sets up a stack with only 104 bytes to go, then couldn't accessing rsp-105 to rsp-128 from the vDSO (which should be safely within the red zone) risk causing a segfault even if the function didn't do the race-y 4k stack probe?
Interrupts may use the kernelstack, you can use the userstack but this is trouble if they don't uphold the redzone, which they may not.
The redzone is a mere suggestion the compiler may apply when it exposes functions elsewhere. The stack is mostly free game.
When you are writing a kernel, the red zone is off, I may have worded this wrong previously. The userspace should use the redzone, the kernel can't, interrupts must not assume a redzone since they don't have the time to add or sub from the RSP.
>wouldn't that also mean any function can assume the 128 bytes below RSP are actually mapped?
The stack is always sort of mapped and not at all. Normally the stack region is unmapped in the page table. Should a process run the stack into an unmapped region, the OS will generally map this region if memory is available. And because programmers are silly, the OS will also happily map memory way above the last mapped page if accessed.
Go setting up a 104 byte stack only means that the runtime assumes the vDSO will not use more than 104 bytes of stack and another go routine may be allocated just after those 104 bytes. So if the vDSO does it's stack probe 4k into foreign stack just under the current thread stack, it will cause a race condition on the memory write.
The stack probe is there to prevent overflowing the stack into the heap, there is always a guard page between user memory and user stack which will kill the task if it is accessed. So the probe will jump into this guard page and kill the program if any funny business is going on.
Also, regarding:
> Interrupts may use the kernelstack, you can use the userstack but this is trouble if they don't uphold the redzone, which they may not.
I don't see how this is possible without corrupting the redzone. The CPU pushes the return RIP and the flags onto RSP when an interrupt occurs, which would overwrite parts of the redzone, so the damage would be done on the userspace stack even before the first instruction of the interrupt handler is executed)
Not technically since the vDSO does not use more than 104 bytes of stack. The problem was the compiler assuming wrongly there might be more to have and writing data above this limit.
The actual stack usage of a vDSO is not documented afaik.
To think that some experienced programmers I know declare that concurrency is easy.
A) other (probably better?) engineers have created abstractions for them, and
B) they've never had to debug a truly difficult issue related to concurrency
Maybe the segfault only occurs on a write?
[0]: See https://lkml.org/lkml/2017/11/10/348
> This code is so wrong I don't even no where to start. ... I suppose we could try to make the kernel fail to build at all on a broken configuration like this.
Edit: to give an example: if a function uses 6K of stack space, then -fstack-check will probe pages from [SP+4K] to [SP+10K]. [SP] through [SP+4K] are assumed already probed by the call chain, and the [SP+4K] through [SP+6K] region is being probed explicitly. In the end, nothing gets skipped over.
It's also not at all clear that this design works is signals are involved unless there's explicit runtime support.
I don't think signals change anything. You can think of them as just function calls that skip over the redzone, then keep probing. The redzone is < 1 page so you should still wind up touching every page.