Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

That is not a violation of memory safety, that's a violation of concurrency safety, which Go doesn't promise (and of course, Rust does.)



Segfaults are very much a memory safety issue. You are correct that concurrency is the cause here, but that doesn't mean it's not a memory safety issue.

That said, most people still call Go memory safe even in spite of this being possible, because, well, https://go.dev/ref/mem

> While programmers should write Go programs without data races, there are limitations to what a Go implementation can do in response to a data race. An implementation may always react to a data race by reporting the race and terminating the program. Otherwise, each read of a single-word-sized or sub-word-sized memory location must observe a value actually written to that location (perhaps by a concurrent executing goroutine) and not yet overwritten. These implementation constraints make Go more like Java or JavaScript, in that most races have a limited number of outcomes, and less like C and C++, where the meaning of any program with a race is entirely undefined, and the compiler may do anything at all.

That last sentence is the most important part. Java in particular specifically defines that tears may happen in a similar fashion, see 17.6 and 17.7 of https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.htm...

I believe that most JVMs implement dynamic dispatch in a similar manner to C++, that is, classes are on the heap, and have a vtable pointer inside of them. Whereas Go's interfaces can work like Rust's trait objects, where they're a pair of (data pointer, vtable pointer). So the behavior we see here with Go is unlikely to be possible in Java, because the tear wouldn't corrupt the vtable pointer, because it's inside what's pointed at by the initial pointer, rather than being right after it in memory.

These bugs do happen, but they have a more limited blast radius than ones in languages that are clearly unsafe, and so it feels wrong to lump Go in with them even though in some strict sense you may want to categorize it the other way.


Sure, that's all true. It does limit Go's memory safety guarantees. However, I still believe that just because Java and other languages can give better guarantees around the blast radius of concurrency bugs does not mean that Go's definition of memory safety is invalid. I believe you can justifiably call Go memory-safe with unsafe concurrency. This may give people the wrong idea about where exactly Go fits in on the spectrum of "safe" coding (since, like you mentioned, some languages have unsafe concurrency that is still safer,) but it's not like it's that far off.

On the other hand, though, in practice, I've wound up using Go in production quite a lot, and these bugs are excessively rare. And I don't mean concurrency bugs: Go's concurrency facilities kind of suck, so those are certainly not excessively rare, even if they're less common than I would have expected. However... not all Go concurrency bugs can possibly segfault. I'd argue most of them can't, at least not on most common platforms.

So how severely you treat this lapse is going to come down to taste. I see the appeal of Rust's iron-clad guarantees around limiting the blast radius, but of course everything comes with limitations. I believe that any discussion about the limitations of guarantees like these should have some emphasis on the real impact. e.g. It's easy enough to see that the issues with memory management in C and C++ are serious based on the security track record of programs written in C and C++, I think we're still yet to fully understand how much of an impact Go's lack of safe concurrency will impact Go software in the long run.


> On the other hand, though, in practice, I've wound up using Go in production quite a lot, and these bugs are excessively rare.

I both want to agree with this, but also point to things like https://www.uber.com/en-CA/blog/data-race-patterns-in-go/, which found a bunch of bugs. They don't really contextualize it in terms of other kinds of bugs, so it's really hard to say from just this how rare they actually are. One of the insidious parts of non-segfaulting data race bugs is that you may not notice them until you do, so they're easy to under-report. Hence the checker used in the above study.

> not all Go concurrency bugs can possibly segfault. I'd argue most of them can't, at least not on most common platforms.

For sure, absolutely. And I do think that's meaningful and important.

> I think we're still yet to fully understand how much of an impact Go's lack of safe concurrency will impact Go software in the long run.

Yep, and I do suspect it'll be closer to Java than to C.


The Uber page does a pretty good job of summing it up. The only thing I'd add is that there has been a little bit of effort to reduce footguns since they've posted this article; as one example, the issue with accidentally capturing range for variables is now fixed in the language[1]. On top of having a built-in (runtime) race detector since 1.1 and runtime concurrent map access detection since 1.6, Go is also adding more tools to make testing concurrent code easier, which should also help ensure potentially racy code is at least tested[2] (ideally, with the race detector on.) Accidentally capturing named return values is now caught by a popular linting tool[3]. There is also gVisor's checklocks analyzer, which, with the help of annotations, can catch many misuses of mutexes and data protected by mutexes[4]. (This would be a lot nicer as a language feature, but oh well.)

I don't know if I'd evangelize for adopting Go on the scale that Uber has: I think Go works best for shared-nothing architectures and gets gradually less compelling as you dig into more complex concurrency. That said, since Uber is an early adopter, there is a decent chance that what they have learned will help future organizations avoid repeating some of the same issues, via improvements to tooling and the language.

[1]: https://go.dev/blog/loopvar-preview

[2]: https://go.dev/blog/synctest

[3]: https://github.com/mgechev/revive/blob/HEAD/RULES_DESCRIPTIO...

[4]: https://pkg.go.dev/gvisor.dev/gvisor/tools/checklocks


Ah, that's great info, thank you :)


> Segfaults are very much a memory safety issue.

How can a segfault lead to attack or exploitation?

Edit: Answering my own question (from https://go.dev/ref/mem):

Reads of memory locations larger than a single machine word are encouraged but not required to meet the same semantics as word-sized memory locations, observing a single allowed write w. For performance reasons, implementations may instead treat larger operations as a set of individual machine-word-sized operations in an unspecified order. This means that races on multiword data structures can lead to inconsistent values not corresponding to a single write. When the values depend on the consistency of internal (pointer, length) or (pointer, type) pairs, as can be the case for interface values, maps, slices, and strings in most Go implementations, such races can in turn lead to arbitrary memory corruption.


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.




Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: