Interesting.
To quote the NSA [1], "Some examples of memory safe languages are Python, Java, C#, Go, Delphi/Object Pascal, Swift, Ruby, Rust, and Ada. Memory safe languages provide differing degrees of memory usage protections, so available code hardening defenses, such as compiler options, tool analysis, and operating system configurations, should be used for their protections as well."
The narrow definition of memory safety here is:
Go has garbage collection, so you won't have memory leaks or use-after-free.
Go is powerful enough that beginners can cause segfaults by accidentally abusing internals, okay.
I'm not sure this is a very redeeming property of Go: Being able to crash the GC, without the flexibility of manual memory management.
But I'm not sure I'd categorize it as "not memory safe" for the same reason C/C++ aren't (a trade-off).
Because I don't believe that you can generally leverage this for the kinds of memory exploits made in C/C++.
I recall that some ML dialects (Standard ML and OCaml) have a library function Obj.magic : 'a -> 'b which escapes the type system. Using this can easily cause segfaults. Does that mean Standard ML and OCaml are not memory safe? Generally, no, they're extremely safe if you avoid that feature, which is most likely. This is arguably less safe than Go, since you most likely won't accidentally run that function.
[1]: https://media.defense.gov/2022/Nov/10/2003112742/-1/-1/0/CSI...
> In any language, if you don't use a thread safe container and mutate it from multiple threads you'll get problems.
Yes I agree there will be problems but what kind of problems do you get? Can you potentially get a memory safety problem, or are you guaranteed that the problem is not a memory safety problem?
The point is that thread safety problems in Go lead to memory safety problems. That's not the case in Java. You can crash the whole Go program by doing that, but you cannot crash the JVM by doing the same thing.
It behave exactly like Java or C# which are also memory safe.
Go programs can literally segfault from a data race. That's no memory safety.
In Java, all primitive types (including Object pointers) are atomically modified. And since all Java writes are primitives (Java doesn't have structs), you can never corrupt a data structure at the Java level. Of course, you can still corrupt it at a logical level (break an invariant established in the constructor), but not at the language level.
Go has a guarantee that word-sized reads/writes are atomic, but Go has plenty of larger objects than that. In particular, interface values are "fat pointers" and exceed the word-size on all platforms, so interface writes are not atomic. Which means another thread can observe an interface value having a vtable from one object but data from another, and can then execute a method from one object on data from another object, potentially re-interpreting fields as values of other types.
If this were the case, then surely someone could construct a program with goroutines, loops and a handful of interface variables—that would predictably fail, right? I wouldn't know how to make one. Could you, or ChatGPT for that matter, make one for demo's sake?
There is this document from Golang devs itself[1], that says:
> 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.
Fair, this matches what everyone is saying in this thread. But I am still curious to see this in practice.
Edit: I found this example from Dave Cheney: https://dave.cheney.net/2014/06/27/ice-cream-makers-and-data.... I am curious if I can replicate this in e.g.: Java.
Edit 2: I can definitely replicate the same bug in Scala, so it is not like Go is unique for the example in that blog post.
Could you share some details on the program and the execution environment? Per my understanding of the Java memory model, a JVM should not experience this problem. Reads and writes to references (and to all 32 bit values) are explicitly guaranteed to be atomic, even if they are not declared volatile.
import java.util.concurrent.Executors
import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future}
trait IceCreamMaker {
def hello(): Unit
}
class Ben(name: String) extends IceCreamMaker {
override def hello(): Unit = {
println(s"Ben says, 'Hello my name is $name'")
}
}
class Jerry(name: String) extends IceCreamMaker {
override def hello(): Unit = {
println(s"Jerry says, 'Hello my name is $name'")
}
}
object Main {
implicit val context: ExecutionContextExecutor = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(2))
def main(args: Array[String]): Unit = {
val ben = new Ben("Ben")
val jerry = new Ben("jerry")
var maker: IceCreamMaker = ben
def loop0: Future[Future[Future[Future[Any]]]] = {
maker = ben
Future { loop1 }
}
def loop1: Future[Future[Future[Any]]] = {
maker = jerry
Future { loop0 }
}
Future { loop0 }
while (true) {
maker.hello()
}
}
}
Here. I am not saying that JVM shouldn't have a stronger memory model, after thinking for a while I think the issue is the program itself. But feel free to try to understand.https://go.dev/play/p/_EJ4EvYntr2
When you run this you will see that occasionally it prints something other than 11 or 100. If it doesn't happen in one run, run it again a few times.
An equivalent Java program will never print anything else.
I understand this to mean the runtime's internal state, not visible to user code. If so, in general we should expect almost any sort of crash mode to be possible. Seems fair enough to call this "memory-unsafe".
What I think is happening here is another instance of a pattern that recurs all the time in communities like this: a term of art was created, "memory safety", to address the concept of languages that don't have buffer overflows, integer overflows, use-after-frees, double frees, controllable uninitialized pointers, and all the other memory lifecycle vulnerabilities. People unfamiliar with the state of the art heard the term, liked it, and have axiomatically derived their own definition for it. They like their definition better, and are not open to the idea that the term exists to serve a purpose orthogonal to their arguments.
Another recent instance of the same phenomenon: "zero trust".
Just as happened in the Zero Trust Wars of 2022, people, hearing the industry definition and intent of the term, scramble to reconcile their axiomatic definition with the state of the art, convincing themselves they were right all along.
The problem they have in this particular argument is: where are the vulnerabilities? Go is not a niche language. It is a high-profile target and has been for over a decade. I saw Go security talks at OWASP Chicago(!) in 2012(!). People have all sorts of hypotheses about how a memory corruption vulnerability --- not "memory corruption", but a vulnerability stemming from it, implying valuable attacker control over the result of whatever bad thing happened --- might sneak into a Go program. Practitioners hear those axiomatic arguments, try to reconcile them with empirical reality, and: it just doesn't hold up.
Just for whatever it's worth to hear this, if at Black Hat 2025 someone does to Go what James Kettle does to web frameworks ever year and introduces a widespread repeatable pattern of memory exploitability in Go race conditions, about half of my message board psyche will be really irritated (I'll have been wrong!), but the other half of my message board psyche will be fucking thrilled (there will be so much to talk about!) and all of my vulnerability researcher psyche will be doing somersaults (there will be so many new targets to hit!). On net, I'm rooting for myself being wrong. But if I had to bet: we're not going to see that talk, not at BH 2025, or 2026, or 2027. I'm probably not wrong about this.
What definition are you using that you seem to think is the one definition of memory safety that is canonical?
> don't have buffer overflows, integer overflows, use-after-frees, double frees, controllable uninitialized pointers, and all the other memory lifecycle vulnerabilities
Any guarantees about this are dependent on the language not having undefined behavior in its safe subset. Once you have undefined behavior any other guarantees made about memory safety are significantly weakened.
> where are the vulnerabilities?
I don't know of any other than code written to demonstrate the concept. But I imagine if you look at any large Golang codebase you will find race condition bugs. So the fact that you have potential undefined behavior resulting from an extremely common coding error seems like it might be something to be concerned about (to me at least). Especially given how little Golang helps you write safe concurrent code.
That's not to say that Go is therefore totally useless and everyone should stop using it now because it's "insecure". But it also seems ... unwise ... to me to just pretend it's nothing because it is hard to exploit or that we don't have any (known) examples of it being exploited.
The argument is not about whether languages admit vulnerabilities --- all of them do. The argument is about whether they admit the vulnerabilities that motivate the term of art "memory safety". Go does not, at least not in any non-contrived scenario not involving "unsafe" or FFI.
As for definitions, I like what Alex wrote about this; or, you can look at ISRG's writing about it.
https://alexgaynor.net/2023/oct/02/defining-the-memory-safet...
Too bad it's not.
"a programming language which, by default, allows code to introduce memory-related vulnerabilities (use after free, buffer over/under flow, use of uninitialized memory, type confusion) and undefined behavior,"
Which was my whole point. The "and undefined behavior" part is absolutely essential to the definition because you can't guarantee the first part without it.
> Go does not, at least not in any non-contrived scenario not involving "unsafe" or FFI.
It absolutely does. You can find examples of code that triggers undefined behavior in this thread. You can hand-wave them away as being "contrived" (which is literally correct) but that doesn't just make the problem go away.
We had a whole collective meltdown (pun intended) over spectre and meltdown a few years back even though AFAICT there have never been any known exploits in the wild. But most people who are knowledgeable in these things still took it seriously because they realized that:
1. Just because you haven't found a specific instance of an in-the-wild exploit doesn't mean there isn't one 2. You don't want to necessarily wait because if something it exploitable in theory is will almost certainly be exploited sooner or later. And it takes time to sort this stuff out
Is this relevant to your average working dev slinging micro-services in go? No, probably not and it's probably not something you should even think about for the most part. But if you're writing a complex, mission critical system with very hard security requirements? Yeah maybe you should worry about little bit about the fact that Go is, by the definition you yourself provided me, not a memory safe language.