Not all segfaults necessarily point to exploitable bugs, but a segfault is usually very suspicious. On common architectures, you get a segmentation fault when there is a memory access violation. Which usually means you've either read from, written to, or tried to execute code at an address that is not readable, not writeable or not executable in your address space. That is suspicious because unless your program is intentionally doing that (which is relatively rare, and obviously in that case you would want to explicitly catch it with a signal handler) it suggests that some assumption your program is making about memory somewhere is incorrect. Like Go says, arbitrary memory corruption.
Is that exploitable? It depends. It's easier to assume that it is than hope that it isn't.
However, while it is a more serious category of issue, I have two reasons to suggest people don't over-index on it:
- Concurrency bugs that can not lead to segmentation faults are by no means safe, they can still lead to exploits of arbitrary severity. Ones that can are more dangerous since they can violate Go's own safety guarantees, but so can the "unsafe" package, so you need to put it into some perspective.
- Concurrency bugs that can are likely to be less common. In my experience, it is not extremely common to re-assign shared map or interface values in Go. If you are sharing a value of map, slice, string or interface and do plan on re-assigning it (thus causing the hazard in question) you can work around this problem trivially by adding a tiny bit of indirection, using an atomic pointer to the value instead, and re-assigning that pointer instead. Making a new value each time is no big deal since all of the fat pointers in question are still relatively small (just 2-3 machine words) though it incurs more allocations and pointer indirections so YMMV.
And of course I recommend using all applicable linters, the checklocks analyzer from gVisor, and careful encapsulation of shared memory where possible. Even better is to avoid it entirely if you can.
Of course, as much as I love Go, some types of program are going to need lots of hairy shared memory and mutations interweaving. And for that, Rust is the obvious best choice.
Yeah, I was just thinking if an implementation has the propensity to abort or fail early with a segfault, that's better than running with memory corruption and far more difficult to exploit. It's not clear from the upthread example how soon it fails after corruption so there is potentially a narrow window where such a bug could be exploited if found in the wild with the apropos attack surface.
Ah I see what you mean. To be fair, it is still true that not every bug that can lead to a segfault is exploitable, including this one potentially, but on the other hand, I think the point is that Go's memory safety guarantees always prevent segmentation faults: by the time you've hit a segmentation fault, you have definitely broken the type system and nothing is guaranteed anymore W.R.T. memory safety. So any bug that causes a segmentation fault is definitely immediately suspect. I think that's the point they were going for, at least.
Is that exploitable? It depends. It's easier to assume that it is than hope that it isn't.
However, while it is a more serious category of issue, I have two reasons to suggest people don't over-index on it:
- Concurrency bugs that can not lead to segmentation faults are by no means safe, they can still lead to exploits of arbitrary severity. Ones that can are more dangerous since they can violate Go's own safety guarantees, but so can the "unsafe" package, so you need to put it into some perspective.
- Concurrency bugs that can are likely to be less common. In my experience, it is not extremely common to re-assign shared map or interface values in Go. If you are sharing a value of map, slice, string or interface and do plan on re-assigning it (thus causing the hazard in question) you can work around this problem trivially by adding a tiny bit of indirection, using an atomic pointer to the value instead, and re-assigning that pointer instead. Making a new value each time is no big deal since all of the fat pointers in question are still relatively small (just 2-3 machine words) though it incurs more allocations and pointer indirections so YMMV.
And of course I recommend using all applicable linters, the checklocks analyzer from gVisor, and careful encapsulation of shared memory where possible. Even better is to avoid it entirely if you can.
Of course, as much as I love Go, some types of program are going to need lots of hairy shared memory and mutations interweaving. And for that, Rust is the obvious best choice.