7 ms·
Here is code to circumvent Go's memory safety without importing unsafe. get() reads a byte at an arbitrary address and set() writes a byte at an arbitrary addr
by atomic128 2y ago
Here is code to circumvent Go's memory safety without importing unsafe.
get() reads a byte at an arbitrary address and set() writes a byte at an arbitrary address.
This is excerpted from BUGFIX 66 ("Hack This Site"):
func racer() {
var (
ptr1 *uintptr
ptr2 *byte
race any
done = make(chan struct{})
)
put := func(x any) {
for {
select {
case <-done:
return
default:
race = x
}
}
}
go put(ptr1)
go put(&ptr2)
for {
var ok bool
ptr1, ok = race.(*uintptr)
if ok && ptr1 != nil {
close(done)
break
}
}
get := func(addr uintptr) byte {
*ptr1 = addr
return *ptr2
}
set := func(addr uintptr, to byte) {
*ptr1 = addr
*ptr2 = to
}
if get(0xdeadbeef) == 111 {
set(0xbaaaaaad, 222)
}
}
- tptacek 2y ago"Without importing unsafe" is doing a lot of work for examples like this.
- atomic128 2y agoThis comes from a webpage where the challenge is to compromise the site, despite the fact that Go imports are disallowed (including unsafe). It's a puzzle game. To clarify, I think Go is magnificent and I use it for everything. The racer() code is just a curiosity.
- tptacek 2y agoRight, it's a cool trick. It's just not material to real threat models, which is what people imply when they say "Go isn't memory safe".
- atomic128 2y agoI agree.
- tialaramex 2y agoThe fact Go has UB under data races has practical implications for sufficiently complex concurrent software. If you can induce a race on a non-trivial object, that's UB instantly - you can probably blow up the Go runtime and all bets are off. I would not characterise this fact, which is a design choice in Go, as similar to say a Rust soundness bug, which will sooner or later just get fixed. They aren't going to somehow magically fix this problem in Go, it's part of the design.
- tptacek 2y agoMy point has nothing to do with whether the language will achieve "soundness". It's that this is behavior that has not over the last 15 years produced exploitable vulnerabilities, despite extremely high incentives for those vulnerabilities to be unearthed.
- pkolaczk 2y agoYou don’t need to blow up the runtime to cause a vulnerability due to a data race in Go: https://security.snyk.io/vuln/SNYK-DEBIAN13-GOLANGGITHUBGORESTYRESTY-6095006 https://security.snyk.io/vuln/SNYK-DEBIAN13-GOLANGGITHUBGORE...
- arp242 2y agoThat's a completely different type of vulnerability than the UB that's being talked about. > The call to sync.Pool.Get will then return a bytes.Buffer that hasn't had bytes.Buffer.Reset called on it. This dirty buffer will contain the HTTP request body from an unrelated request. This is just a good ol' logic error, that just so happens to also be a race.
- unscaled 2y agoThese type of UB bugs in Go are a bit of a red herring, since most race conditions arise from improper use of shared mutability, and would still be a problem even in the presence of full memory safety, for instance: https://github.com/golang/go/issues/37669 https://github.com/golang/go/issues/37669 https://github.com/golang/go/issues/48340 https://github.com/golang/go/issues/48340 These types of race conditions cannot happen in Rust. Not because Rust does not have UB, but because Rust does not allow multiple writable pointers ("mutable borrows") to the same memory region. If you want shared AND mutable access to memory, you must use a thread-safe construct such as Mutex or Cell — or drop into unsafe code. Rust does not prevent all types of errors of course. Dirty buffer reuse (as in the GP example) is still possible in Rust. You could still have situations where a buffer is returned to a pool without resetting it. But this could only be a pure logic error where you've forgot to reset the buffer and it would occur consistently and thus would be easy to reproduce and debug. In addition, with idiomatic Rust, you could enforce proper buffer cleanup in Rust by wrapping the Buffer with a type that implements Drop. More specifically, the vulnerability mentioned in GP is not possible in Rust. The description is a bit misleading, but the issue was not that the buffer was returned to the pool without being reset, but rather that the same buffer was returned to the pool TWICE under certain conditions, due to a data race. This is not possible in Rust. You cannot put the same owned buffer twice in a pool, due to Rust's move semantics (affine types). And if we want to be completely honest, you'd probably won't need to pool buffers in Rust to begin with, since you don't need to avoid garbage collection (there is none). In most cases, malloc is going to work good enough as your "pool". We have a serious problem as an industry, where there is a popular conception of memory safety and type safety as a binary property: a language is either safe or unsafe, either sound or unsound. But it's more of a spectrum, and not even a contiguous one at that. This comments thread is split between people who say that large size atomicity UB is not a major issue in practice and people willing to completely rule off Go's memory safety based on that. But we could just say Go sits near the safe end of the spectrum of memory safety — it certainly does far better than C. My security concerns with Go, after nearly 9 years of using are mostly about race conditions, memory leaks and lack of better mechanisms to enforce type safety (such as sum types and affine types).
- Yoric 2y agoWell, time will tell. As Go usage increases, it becomes a more tempting target, which means that more malicious third-parties will start poring over the code of the std library and the frameworks looking exactly for this kind of vulnerability. The same goes for Rust, Swift or Zig, of course.
- kaba0 2y agoHow is it not material? You only need to accidentally write and read a map at the same time in language that is supposedly for concurrency (which is why not the same as parallelism, in its case it does largely correlate). This is a ridiculous design issue with big ramifications.