Every time this conversation comes up, I'm reminded of my team at Dropbox, where it was a rite of passage for new engineers to introduce a segfault in our Go server by not synchronizing writes to a data structure.
Swift has (had?) the same issue and I had to write a program to illustrate that Swift is (was?) perfectly happy to segfault under shared access to data structures.
Go has never been memory-safe (in the Rust and Java sense) and it's wild to me that it got branded as such.
tptacek · 1d ago
Right, the issue here is that the "Rust and Java sense" of memory safety is not the actual meaning of the term. People talk as if "memory safety" was a PLT axiom. It's not; it's a software security term of art.
This is just two groups of people talking past each other.
It's not as if Go programmers are unaware of the distinction you're talking about. It's literally the premise of the language; it's the basis for "share by communicating, don't communicate by sharing". Obviously, that didn't work out, and modern Go does a lot of sharing and needs a lot of synchronization. But: everybody understands that.
moefh · 19h ago
I agree that there are two groups here talking past each other. I think it would help a lot to clarify this:
> the issue here is that the "Rust and Java sense" of memory safety is not the actual meaning of the term
So what is the actual meaning? Is it simply "there are no cases of actual exploited bugs in the wild"?
Because in another comment you wrote:
> a term of art was created to describe something complicated; in this case, "memory safety", to describe the property of programming languages that don't admit to memory corruption vulnerabilities, such as stack and heap overflows, use-after-frees, and type confusions. Later, people uninvolved with the popularization of the term took the term and tried to define it from first principles, arriving at a place different than the term of art.
But type confusion is exactly what has been demonstrated in the post's example. So what kind of memory safety does Go actually provide, in the term of art sense?
tptacek · 19h ago
It's a contrived type confusion bug. It reads 42h because that address is hardcoded, and it does something that ordinary code doesn't do.
If you were engaged to do a software security assessment for an established firm that used Go (or Python, or any of the other mainstream languages that do shared-memory concurrency and don't have Rust's type system), and you said "this code is memory-unsafe", showing them this example, you would not be taken seriously.
If people want to make PLT arguments about Rust's correctness advantages, I will step out of the way and let them do that. But this article makes a security claim, and that claim is in the practical sense false.
ralfj · 8h ago
> It reads 42h because that address is hardcoded,
It is trivial to change this example into an arbitrary int2ptr cast.
> Go (or Python, or any of the other mainstream languages that do shared-memory concurrency and don't have Rust's type system),
As the article discusses, only Go has this issue. Python and Java and JavaScript and so on are all memory-safe. Maybe you are mixing up "language has data races" and "data races can cause the language itself to be broken"?
> If people want to make PLT arguments about Rust's correctness advantages, I will step out of the way and let them do that. But this article makes a security claim, and that claim is in the practical sense false.
This article makes a claim about the term "memory safety". You are making the claim that that's a security term. I admit I am not familiar with the full history of the term "memory safety", but I do know that "type safety" has been used in PLT for many decades, so it's not like all "safety" terms are somehow in the security domain.
I am curious what your definition of "memory safety" is such that Go satisfies the definition. Wikipedia defines it as
> Memory safety is the state of being protected from various software bugs and security vulnerabilities when dealing with memory access, such as buffer overflows and dangling pointers.
My example shows that Go does not enforce memory safety according to that definition -- and not through some sort of oversight or accident, but by design. Out-of-bounds reads and writes are possible in Go. The example might be contrived, but the entire point of memory safety guarantees is that it doesn't matter how contrived the code is.
I'm completely fine with Go making that choice, but I am not fine with Go then claiming to be memory safe in the same sense that Java or Rust are, when it is demonstrably not the case.
tptacek · 8h ago
The problem isn't that you couldn't hardcode a scarier value; it's that you have to demonstrate a plausible scenario in realistic code where an attacker controls both the value and the address it's written to.
While you're wondering why I keep claiming Go is a memory-safe language, you can also go ask the ISRG, which says the same thing I am at (checks notes) https://www.memorysafety.org/.
ralfj · 8h ago
> While you're wondering why I keep claiming Go is a memory-safe language, you can also go ask the ISRG, which says the same thing I am at
And yet Go violates the definition they give -- it doesn't prevent out-of-bounds accesses. (And just to be sure we're talking about the same thing, I'm specifically talking about Go here. All the other languages on their list are actually memory safe, as far as I know.)
> you have to demonstrate a plausible scenario in realistic code where an attacker controls both the value and the address it's written to.
So your definition of memory safety includes some notion of "plausible" and "realistic"? Neither https://www.memorysafety.org/docs/memory-safety/ nor Wikipedia have such a qualification in their definition. It would help if you could just spell out your definition in full, rather than having us guess.
HAL3000 · 5h ago
> So your definition of memory safety includes some notion of "plausible" and "realistic"? Neither https://www.memorysafety.org/docs/memory-safety/ nor Wikipedia have such a qualification in their definition. It would help if you could just spell out your definition in full, rather than having us guess.
This is a strawman argument, you're arguing semantics here. You're a smart person, so you know exactly what he means. The perception created by your article is that people shouldn't use Go because it's not memory-safe. But the average developer hearing "not memory-safe" thinks of C/C++ level issues, with RCEs everywhere.
Unless you can show a realistic way this could be exploited for RCE in actual programs, you're just making noise. Further down the thread, you admit yourself that you're in a PLT research bubble and it shows.
dcow · 4h ago
You definitely shouldn’t use Go, but it’s not because of the discussion in TFA. I jest, rhetorically.
Seriously, why are we bashing a researcher for being academic? This makes no fucking sense. Nobody claimed anywhere that people should stop using Go.
esoterae · 2h ago
Did you not notice how this started over someone saying "That's not the definition of memory safety" and then prevaricating about the bush when asked to provide their definition? Your theory that this is an argument over semantics is correct, but not fully understood.
hdevalence · 4h ago
> you're arguing semantics here
Yes, semantics — what do things mean, exactly? — is the subject of the discussion here and is actually quite important in general.
ralfj · 3h ago
> The perception created by your article is that people shouldn't use Go because it's not memory-safe.
Uh, where exactly am I saying or implying that?
I am, in fact, saying that Go is much closer to memory-safe languages than to C, safety-wise.
But I am arguing that the term "memory safe" should only be used for languages that actually went through the effort of thinking this problem through to the end and plugging all the holes through which memory safety violates can sneak in. Go is 99% there, but it's falling slightly short of the goal. I think that's a useful distinction, and I am disappointed that it is regularly swept under the rug, which is why I wrote this blog post. You are free to disagree, I never expected to convince everyone. But I think I gave some people some new food for thought, and that's all I can hope for.
tptacek · 8h ago
You're just wrong about this. The ability to write contrived code that does an out-of-bounds write, or to induce crashes, doesn't violate the notion of "memory safety" as an ordinary term of art.
ralfj · 8h ago
Yeah I understand that that's how you like to use the term, you've been very clear about that. What I am curious about is whether that's just you. Because the source you gave last time, https://www.memorysafety.org/docs/memory-safety/, doesn't agree with what you are saying, and neither does Wikipedia.
I am honestly curious here. I am a PLT researcher so I am in a bubble where people use the term consistently with how I use it. You are the first person I meet (for some notion of "meet" ;) that uses the term differently. But without external sources it's hard to judge how wide-spread your definition (that you still haven't spelled out...) is.
Yeah, Go is often listed with memory-safe languages, I know that. And yet when people define memory safety, Go usually fails to satisfy that definition. That's why I was asking for a definition of memory safety that would include Go.
tptacek · 8h ago
Again: my usage of the term is widespread enough that the ISRG uses it to refer to Go as well, as does, well, basically everybody else in the industry. I think you've just message-boarded yourself into believing this is a live debate. There is no sequence of words you're going to come up with to convince me that everybody is wrong when they say "Go is a memory safe language".
ralfj · 7h ago
You keep making arguments by assertion without giving sources, so :shrug: yeah this isn't going to go anywhere.
I think we actually agree on all of the factual points here, we just don't agree on how languages should be categorized/labeled according to their guarantees in both a theoretical and a practical sense, and that's largely a subjective matter anyway. So, happy to agree to disagree here.
zozbot234 · 8h ago
Then the "security" definition is totally useless, because even C can be memory safe. What about pointers, malloc(), free(), unchecked enums etc. etc.? Oh, those are just "contrived" language features you're not really supposed to use. You can write FORTRAN in any language!
tptacek · 8h ago
C is the archetypical memory-unsafe language. When you've reached the point where you're simultaneously arguing that C is memory-safe and Go isn't, you should recognize you've made a wrong turn somewhere.
zozbot234 · 8h ago
My point with those comparisons is that you have not bothered to define a reasonable and verifiable standard for what counts as "contrived" code - which is what ultimately seems to determine whether a language is memory safe, according to your definition.
rowanG077 · 8h ago
Did you consider that the organization can be wrong?
> Memory safety is a property of some programming languages that prevents programmers from introducing certain types of bugs related to how memory is used. Since memory safety bugs are often security issues, memory safe languages are more secure than languages that are not memory safe.
That is the definition they give. Since Go does not "prevent programmers from introducing certain types of bugs related to how memory is used." it does not fall under this definition. They can list go as memory safe, but then either they disagree with their own definition or made the mistake of adding Go to that list. Memory safety is not a spectrum. You are either memory safe or unsafe. The spectrum is in the unsafety. Go is obviously less unsafe than C for example.
nicoburns · 11h ago
You are however replying to thread where a Dropbox engineer calls it "a right of passage" to introduce such bugs to their codebase. Which suggests that it is by no means unheard of for these problems to crop up in real-world code.
tptacek · 9h ago
Again: introducing surprising correctness bugs? Crashing programs? Absolutely. I don't know how many different ways I can say that my concern here is the misuse of a security term of art. Dropbox engineers do not have as a rite of passage introducing or finding RCE vulnerabilities in Go code. Would that it were so! My job would be much more interesting.
Denial of service can absolutely be a security issue, as can any correctness bug if it leads to unintended behavior or corrupted data.
tptacek · 8h ago
If that's where we're at, where unhandled exceptions are the security issues we're hanging on, I'll consider my argument won.
zozbot234 · 8h ago
That might be a reasonable argument if you were guaranteed an unhandled exception in this instance. Unfortunately that's not the case.
tptacek · 8h ago
If you could demonstrate something better than that, we wouldn't be arguing about the severity of DOS attacks.
samus · 10h ago
Doesn't Dropbox write a lot of Python extensions in C for speedup?
pclmulqdq · 5h ago
Excuse me, but in this thread we are bashing go, not making logical arguments.
ioasuncvinvaer · 2h ago
What is the argument?
pclmulqdq · 1h ago
That Dropbox is very happy with unsafe languages despite the top-level comment.
blub · 10h ago
Many FAANG & co engineers are overrated.
If every new hire is introducing concurrency bugs in a Golang codebase, refactor, do better review and maybe use concurrency questions instead of leetcode.
I’ll take tptacek’s word over most FAANG type on such topics if we’re doing appeals to authority. The guy is very practical, unlike the Rust community which is incredibly focused on theoretical correctness instead of real-world experiences.
jacquesm · 10h ago
My own takeaway after looking at corporate codebases for four decades is that the state of the art in software development at banks, governments, insurance companies, airlines, health care and so on is such that I long for the time before the internet.
Sure, those mainframes from the 80's weren't bullet proof either. But you first had to get to them. And even if the data traveled in plain text on leased lines (point-to-point but not actually point-to-point (that would require a lot of digging), no multiplexing) you had to physically move to the country where they were located to eavesdrop on them, and injecting data into the stream was a much harder problem.
stickfigure · 17h ago
I have recently come to the conclusion that everything I ever thought was "contrived" is currently standard practice in some large presently existing organization.
tptacek · 17h ago
Take that to the Apple bounty program with your crasher bug and tell them they should pay out as if you'd confirmed RCE, see how it goes. This is an engineering question; it's not about vibes.
It's not even always the case that corrupted data structures (or even pointers) in C code are exploitable. You need attacker control of data and where it goes in memory. It's far less often the case in Python or Go --- in fact, it's basically never the case. As evidence for that claim: the zero memory corruption RCEs in all of shipping Go code, of which there is a lot.
NitpickLawyer · 15h ago
Dunno about Apple, but goog sometimes pays out bugs that are "theoretical" in the way you describe. That is, you show that there's a bug somewhere, but you can't "reach" it from user data. They'll pay less than a PoC, obviously, but will pay. YMMV, etc.
socalgal2 · 18h ago
> But: everybody understands that.
Everybody does not understand that otherwise there would be zero of these issues in shipping code.
This is the problem with the C++ crowd hoping to save their language. Maybe they'll finally figure out some --disallow-all-ub-and-be-memory-safe-and-thread-safe flag but at the moment it's still insanely trivial to make a mistake and return a reference to some value on the stack or any number of other issues.
The answer can not be "just write flawless code and you'll never have these issues" but at the moment that's all C++, and Go, from this article has.
tptacek · 17h ago
Again: if you want to make that claim about correctness bugs, that's fine, I get it. But if you're trying to claim that naive Go code has memory safety security bugs: no, that is simply not true.
saurik · 16h ago
I cannot find anyone in this thread (nor in the article) making the claim you are arguing against, though... the reason for the example isn't "this demonstrates all Go code is wrong", but merely that "you can't assume that all Go code is correct merely because it is written in Go"; now, most code written in Go might, in fact, be safe, and it might even be difficult to write broken Go code; but, I certainly have come across a LOT of people who are claiming that we don't even have to analyze their code for mistakes because it is written in Go, which is not the case, because people do, in fact, share stuff all over the place and Go, in fact, doesn't prevent you from all possible ways of writing broken code. To convince these people that they have to stop making that assumption requires merely any example of code which fails, and to those people these examples are, in fact, elucidating. Of course, clearly, this isn't showing a security issue, but it isn't claiming to be; and like, obviously, this isn't something that would be sent to a bug bounty program, as who would it even be sent to? I dunno... you seem to have decided you want to win a really minute pedantic point against someone who doesn't exist, and it makes this whole thing very confusing.
ngrilly · 13h ago
The terms correctness (from a PLT perspective) and safety (from a security perspective) are not equivalent and interchangeable. I see them mixed up too much in this discussion.
ralfj · 8h ago
PLT has used the term "type safety" for a very long time -- so "safety" does not imply a security perspective. And yes it is indeed very different from correctness. But the article doesn't claim that memory safety should imply correctness -- that would be ridiculous, obviously you can write buggy programs in memory-safe languages. The article claims that Go is not memory-safe.
ngrilly · 7h ago
I was referring to comments mentioning correctness and safety as interchangeable terms. The article doesn’t mix them up.
ralfj · 3h ago
Fair, I misunderstood then. :)
blub · 9h ago
This comment highlights a very important philosophical difference between the Rust community and the communities of other languages:
- in other languages, it’s understood that perhaps the language is vulnerable to certain errors and one should attempt to mitigate them. But more importantly, those errors are one class of bug and bugs can happen. Set up infra to detect and recover.
- in Rust the code must be safe, must be written in a certain way, must be proven correct to the largest extent possible at compile time.
This leads to the very serious, solemn attitude typical of Rust developers.
But the reality is that most people just don’t care that much about a particular type of error as opposed to other errors.
J_Shelby_J · 5h ago
> This leads to the very serious, solemn attitude typical of Rust developers.
Opposite really. I like rust because I can be care free and have fun.
zozbot234 · 9h ago
> ... it's understood that perhaps the language is vulnerable to certain errors and one should attempt to mitigate them. But more importantly, those errors are one class of bug and bugs can happen. Set up infra to detect and recover.
> in Rust the code must be safe, must be written in a certain way, must be proven correct to the largest extent possible at compile time.
Only for the Safe Rust subset. Rust has the 'unsafe' keyword that shows exactly where the former case does apply. (And even then, only for possible memory unsoundness. Rust does not attempt to fix all possible errors.)
pclmulqdq · 5h ago
A not-so-secret secret of Rust is that liberal use of 'unsafe' is pretty much required for certain classes of high-performance code.
aystatic · 1h ago
imo if you're sprinkling around `unsafe` in your codebase "liberally", you're holding it wrong. In general it's really not that hard to encapsulate most unsafety into a wide-contract abstraction; I’d argue where Rust really shines is when you take advantage of the type system and static analyzer to automatically uphold invariants for you
dev_l1x_be · 13h ago
> But: everybody understands that.
I had to convince Go people that you can segfault with Go. Or you mean the language designers with using everybody?
pclmulqdq · 1h ago
You can segfault in Rust, too - there's a whole subset of the language marked "unsafe" that people ignore when making "safe language" arguments. The question is how difficult is it to have a segfault, and in Go it's honestly pretty hard. It's arguably harder in Rust but it's not impossible.
Ygg2 · 22h ago
> People talk as if "memory safety" was a PLT axiom. It's not; it's a software security term of art.
It's been in usage for PLT for at least twenty years[1]. You are at least two decades late to the party.
Software is memory-safe if (a) it never references a memory location outside the address space allocated by or that entity, and (b) it never executes intstruction outside code area created by the compiler and linker within that address space.
Not GP, but that definition seems not to be the one in use when describing languages like Rust--or even tools like valgrind. Those tools value a definition of "memory safety" that is a superset (a big one) of the definition referenced in that paper: safety as preventing incorrect memory accesses within a program, regardless of whether those accesses are out of bounds/segmentation violations.
adgjlsfhk1 · 8h ago
it's not, but for a very subtle reason. To prove memory safety, you need to know that the program never encounters UB (since at that point you have nothing known about the program)
Wowfunhappy · 19h ago
...by that definition, can a C program be memory safe as long as it doesn't have any relevant bugs, despite the choice of language? (I realize that in practice, most people are not aware of every bug that exists in their program.)
Waterluvian · 19h ago
This is way outside my domain but isn’t the answer: yes, if the code is formally proven safe?
Doesn’t NASA have an incredibly strict, specific set of standards for writing safety critical C that helps with writing programs that can be formalized?
okanat · 19h ago
There are safety recommendations / best practice standards like CERT. None of them will prevent you from making intentional looking but logically unsound memory unsafe operations with C and C++. The code can be very indistinguishable from safe code. The things that C and C++ allow you to do basically makes code written in those languages impossible to fully formally prove. Although there are subsets, the basic integer operations and primitive types are messed up with C. So without uprooting how basic integer and pointer types work, it is impossible to make C and C++ safer. Such change will make all C and C++ programs invalid.
C and C++ always defaults to minimum amount of safety for maximum allowance of the compiler interpretation. The priority of the language designers of them is keeping existing terrible code running as long as possible first, letting compilers interpret the source code as freely as possible second.
That's why many military and aerospace code actually uses much safer and significantly more formally verifiable Ada.
spookie · 12h ago
> impossible to formally prove
If you assume the entire lang, yes. If you use a large subset, no. Furthermore, compiler interpretation might actually be sane! There are more compilers out there than GCC, Clang or MSVC. I suspect many assumptions are being made on this claim.
uecker · 15h ago
There is also a lot of formally verified in C.
Waterluvian · 17h ago
Thanks for explaining that. It really fills in a lot for me.
Can a C program be memory safe as long as it doesn't have any relevant bugs? Yes, and you can even prove this about some C programs using tools like CBMC.
ratmice · 19h ago
It just seems like a bad definition (or at least ambiguous), it should say "cannot", or some such excluding term.
By the definition as given if a program flips a coin and performs an illegal memory access,
are runs where the access does not occur memory safe?
Ygg2 · 11h ago
Sure. It can be. In the same way, a C program can be provably correct. I.e., for all inputs it doesn't exhibit unexpected behavior. Memory safety and correctness are properties of the program being executed.
But a memory-safe program != memory safe language. Memory safe language helps you maintain memory-safety by reducing the chances to cause memory unsafety.
blub · 10h ago
The problem Rust has is that it’s not enough to be memory safe, because lots of languages are memory safe and have been for decades.
Hence the focus on fearless concurrency or other small-scale idioms like match in an attempt to present Rust as an overall better language compared to other safe languages like Go, which is proving to be a solid competitor and is much easier to learn and understand.
zozbot234 · 10h ago
Except that Swift also has safe concurrency now. It's not just Rust. Golang is actually a very nice language for problems where you're inherently dependent on high-performance GC and concurrency, so there's no need to present it as "better" for everything. Nevertheless its concurrency model is far from foolproof when compared to e.g. Swift.
junebash · 1d ago
Swift is in the process of fixing this, but it’s a slow and painful transition; there’s an awful lot of unsafe code in the wild that wasn’t unsafe until recently.
cosmic_cheese · 1d ago
One of the biggest hurdles is just getting all the iOS/macOS/etc APIs up to speed with the thread safety improvements. It won’t make refactoring all that application code any easier, but as things stand even if you’ve done that, you’re going to run into problems anywhere your code makes contact with UI code because there’s a lot of AppKit and UIKit that have yet to make the transition.
RetpolineDrama · 1d ago
Swift 6 is only painful if you wrote a ton of terrible Swift 5, and even then Swift 5 has had modes where you could gracefully adopt the Swift 6 safety mechanisms for a long time (years?)
~130k LoC Swift app was converted from 5 -> 6 for us in about 3 days.
jamil7 · 14h ago
Yes and no, our app is considerably larger than 130k LoC. While we’ve migrated some modules there are some parts that do a lot of multithreaded work that we probably will never migrate because they’d need to essentially be rewritten and the tradeoff isn’t really worth it for us.
isodev · 13h ago
It's also painful if you wrote good Swift 5 code but now suddenly you need to closely follow Apple's progress on porting their own frameworks, filling your code base with #if and control flow just to make the compiler happy.
ardit33 · 16h ago
It is still incomplete and a mess. I don't think they thought through the actual main cases Swift is used for (ios apps), and built a hypothetical generic way which is failing on most clients.
Hence lots of workarounds, and ways to get around it (The actor system). The isolated/nonisolated types are a bit contrived and causing real productivity loss, when the old way was really just 'everything ui in main thread, everything that takes time, use a dispatch queue, and call main when done'.
Swift is strating to look more like old java beans. (if you are old enough to remember this, most swift developers are too young). Doing some of the same mistakes.
Anyways, they are trying to reinvent 'safe concurrency' while almost throwing the baby with the bathwater, and making swift even more complex and harder to get into.
There is ways to go. For simple apps, the new concurrency is easy to adopt. But for anything that is less than trivial, it becomes a lot of work, to the point that it might not make it worth it.
pjmlp · 7h ago
Their goal was always to be able to evolve to the point of being able fully replace C, Objective-C and C++ with Swift, it has been on their documentation and plenty of WWDC sessions since the early days.
isodev · 13h ago
You're getting downvoted but I fully agree. The problem with Swift's safety has now moved to the tooling. While your code doesn't fail so often at runtime (still does, because the underlying system SDKs are not all migrated), the compiler itself often fails. Even the latest developer snapshot with Swift 6.2 it's quite easy to make it panic with just... "weird syntax".
A much bigger problem I think are the way concurrency settings are provided via flags. It's no longer possible to know what a piece of code does without knowing the exact build settings. For example, depending on Xcode project flags, a snippet may always run on the main loop, or not at all or on a dedicated actor all together.
A piece of code in a library (SPM) can build just fine in one project but fail to build in another project due to concurrency settings. The amount of overhead makes this very much unusable in a production / high pressure environment.
CJefferson · 11h ago
Before Rust, I'd reached the personal conclusion that large-scale thread-safe software was almost impossible -- certainly it required the highest levels of software engineering. Multi-process code was a much more reasonable option for mere mortals.
Rust on the other hand solves that. There is code you can't write easily in Rust, but just yesterday I took a rust iteration, changed 'iter()' to 'par_iter()', and given it compiled I had high confidence it was going to work (which it did).
potato-peeler · 1d ago
I am curious. Generally basic structures like map are not thread safe and care has to be taken while modifying it. This is pretty well documented in go spec. In your case in dropbox, what was essentially going on?
tsimionescu · 15h ago
I think the surprise here is that failing to synchronize writes leads to a SEGFAULT, not a panic or an error. This is the point GP was making, that Go is not fully memory safe in the presence of unsynchronized concurrent writes. By contrast, in Java or C#, unsynchronized writes will either throw an exception (if you're lucky and they get detected) or let the program continue with some unexpected values (possibly ones that violate some invariants). Getting a SEGFAULT can only happen if you're explicitly using native code, raw memory access APIs, or found a bug in the runtime.
elcritch · 12h ago
Segfault sounds better than running with inconsistent data.
tsimionescu · 11h ago
No, SEGFAULT means you're lucky and your corrupted memory caused you to access something the OS knew you can't access. But every SEGFAULT means that you have a memory safety violation, and so if you get unlucky, the exact same code that SEGFAULTED once will read oe write random objects in your memory (which might include code areas, GC data structures, etc).
Inconsistent data is pretty bad, but it's not as bad as memory corruption.
to11mtm · 4h ago
Well it depends on what we mean by 'inconsistent'.
In C# For example, if a structure is over CPU arch Word size (i.e. 32 or 64 bits) then you could have a torn read if it's being written. However object refs themselves are always word size so you'll never have a torn pointer read on those.
However, in either case there is still a need in multithreaded environments to remember the CPU's memory ordering rules and put proper fences (or, to be safe, locks, since memory barrier rules are different between ARM and x86 for example).
But that second bit is a fairly hard problem to solve for without having the right type of modelling around your compiler.
tsimionescu · 37m ago
When I said "inconsistent", I was referring to things like getting a length field updated by one thread, but the actual list contents by another - if you have thread safety violations you will end up with exactly this type of issue in any language that allows unsafe threading code (Rust wouldn't outside `unsafe` blocks, for example), even in fully memory safe ones like Java or C#, and even without any bugs in the VM.
qcnguy · 12h ago
Go/C programs that race can also run with inconsistent data. Nothing guarantees you a segfault under torn writes.
In practice, Java programs tend to pick up on data races very quickly because they mutate some collection and the collections framework has safety checks for this.
foldr · 12h ago
The inconsistent data thing could happen in Go too. A segfault is not guaranteed, it’s just one of the more likely possibilities.
Someone · 11h ago
> A segfault is not guaranteed, it’s just one of the more likely possibilities.
Is it? It will depend on the code, but my gut feeling is that you typically would get a few (if not lot of) unnoticed non-segfaulting issues before you get the segfaulting one that tells you straight in your face that you have a problem.
foldr · 10h ago
It probably depends on how exactly the corruption happens. If you overwrite a pointer with an integer value, then the integer is statistically unlikely to correspond to a valid memory address. On the other hand, if you overwrite a pointer with a pointer, or an integer with an integer, all bets are off.
maxlybbert · 23h ago
I thought the same thing. Maybe the point of the story isn’t “we were surprised to learn you had to synchronize access” but instead “we all thought we were careful, but each of us made this mistake no matter how careful we tried to be.”
nine_k · 19h ago
In Java, there are separate synchronized collections, because acquiring a lock takes time. Normally one uses thread-unsafe collections. Java also gives a very ergonomic way to run any fragment under a lock (the `synchronized` operator).
Rust avoids all this entirely, by using its type system.
layer8 · 10h ago
Java has separate synchronized collections only because that was initially the default, until people realized that it doesn’t help for the common cases of check-and-modify operations or of having consistency invariants with state outside a single collections (besides the performance impact). In practice, synchronized collections are rarely useful, and instead accesses are synchronized externally.
I have a hard time believing that it's common to create SEGFAULT in Go, I worked with the language for a very long time and don't remember a single time where I've seen that. ( and i've seen many data race )
Not synchronizing writes on most data structure does not create a SEGFAULT, you have to be in a very specific condition to create one, those conditions are extremely rares and un-usual ( from the programmer perspective).
In OP blog to triggers one he's doing one of those condition in an infinite loop.
You really have to go hunting for a segfault in Go. The critical sentence in OP article is: in practice, of course, safety is not binary, it is a spectrum, and on that spectrum Go is much closer to a typical safe language than to C. OP just has a vested interest in proving safety of languages and is making a big deal where in practice there is none. People are not making loads of unsafe programs in Go nor deploying as such because it would be pretty quickly detected. This is much different to C and C++.
rowanG077 · 8h ago
I'm pretty surprised by some other comments in this thread saying this is a rare occurrence in go. In my experience it's not rare at all.
commandersaki · 17h ago
To put things in perspective, I posit to you, how many memory unsafe things can you do in Go that isn’t a variant of the same thing?
Or put another way what is the likelihood that a go program is memory unsafe?
tapirl · 13h ago
Listens your team had not sufficient review capacity at that time.
pjmlp · 1d ago
It is kind of wild that for a 21st century programming language, the amount of stuff in Go that should have been but never was, but hey Docker and Kubernetes.
9rx · 1d ago
On the flip side, what would be the point? There are already a million other languages that have everything and the kitchen sink.
Not going down the same road is the only reason it didn't end up on the pile of obscure languages nobody uses.
pjmlp · 1d ago
The only reason it didn't end on pile of obscure languages nobody uses, it called Google, followed by luck with Docker and Kubernetes adoption on the market, after they decided to rewrite from Python and Java respectively into Go, after Go heads joined their teams.
Case in point, Limbo and Oberon-2, the languages that influenced its design, and authors were involved with.
tsimionescu · 15h ago
I don't think that's the (only) reason Go became popular. The huge thing about Go is the runtime: it's the only language runtime available today, at least in any language with a large org behind it, that offers (a) GC, (b) fast start-up time, (c) static types, (d) fast execution, and (e) multi-threading.
This is a killer combination for any team looking to write code for auto-scalable microservices, to run for example on Kubernetes. Java is not great in this niche because of its slow startup time, relatively large memory overhead, and the need for warm-up before code actually starts executing fast (so scaling up and down has a very large cost for Java services). .NET has similar problems, and also a huge container size. Python is far too slow, and not typed. TypeScript is single threaded, and still has a pretty hefty runtime. OCaml doesn't have any large org behind it, is quite obscure syntax, and was still single-threaded at the time Kubernetes started. Haskell has similar issues, and is also large and slow starting. Rust, C++, C all require manual memory management.
So, it's no surprise that Go was used for Kubernetes services themselves, and it's no surprise that people designing for Kubernetes mostly chose to write their new stuff in Go. Go the language, with its antiquated design, is actually quite secondary to all of that. But Go's runtime is completely unmatched in this space.
The issue is that some people still fighting against the concepts ML family languages (primarily SML) introduced. Go implemented go routines and channels from CSP (https://en.wikipedia.org/wiki/Communicating_sequential_proce...) but dragged a lot on influence from C (understandable) into the language.
I think Rust opted for the best combinations (some CSP, a lot of ML and a bit of C++).
tsimionescu · 11h ago
Not sure what you mean about F# - being a CLR language, it has the same runtime issues as C# (and IronPython, managed C++, etc).
The article you quote is a toy example - if you write a C# or F# web API server, you'll see that it takes up way more space than a Go one with similar functionality (and has way higher memory overhead as well). A Go API web server is maybe 10MB on disk, with no dependencies (that is, you can run it perfectly in a container that is defined as `FROM scratch; COPY my-go-exec /my-go-exec `). The equivalent Java or .NET container is somewhere around 2-400MB at the minimum.
As for the syntax and constructs, I don't care so much. If OCaml or SML had comparable support and a comparable ecosystem to Go, I'd bet plenty of people would have chosen them instead.
qcnguy · 12h ago
Kubernetes services are one of the places where you don't care about startup time. Likewise for Docker itself. These are the things that do the scaling, normally.
Go is not particularly fast. People often see that Java gets faster as it runs and thinks, oh, it must be slow at the start then. But when you compare like with like, Go ends up being stuck at the bottom of the curve that Java then sharply climbs. The difference in GC quality is big, or at least, used to be? The only place where you really notice the difference is command line tools, and Java has GraalVM for that.
tsimionescu · 6h ago
> Kubernetes services are one of the places where you don't care about startup time.
There are some kubernetes services that scale up and down. And even for those that don't normally, if they have some kind of failure, the difference between taking a millisecond to get back up and taking a second can actually matter for a web host.
> Go is not particularly fast. People often see that Java gets faster as it runs and thinks, oh, it must be slow at the start then. But when you compare like with like, Go ends up being stuck at the bottom of the curve that Java then sharply climbs.
Go starts up much faster than Java. And Go code runs measurably faster than interpreted Java code, even though it's slower than the JITed code you'll eventually have if your JVM runs long enpigh. But un-JITed Java code is very slow, more comparable to Python than JITed Java or with Go . This has nothing to do with the GC - where I do agree Go is mediocre at best.
zozbot234 · 6h ago
I wouldn't call the Go GC mediocre, it's one of the few fully concurrent GC's in common use. It probably has significantly lower memory demand than Java/NET for comparable workloads.
pebal · 6h ago
This isn't fully concurrent GC. It pauses mutators threads and delegates them to perform some of the work for the GC.
Animats · 1d ago
The strength of Go is not the language. It's that the libraries you need for web back-end stuff are written, maintained, and used in production by Google. All the obscure cases get exercised in production due to sheer volume of internal usage.
At one time, Go maps were not thread-safe. Was that fixed?
Yoric · 1d ago
I'd be surprised if the JSON module was used within Google, though. It's neither particularly fast nor particularly convenient nor particularly suited to properly handle edge cases. But it's still in the stdlib for compatibility reasons.
No comments yet
9rx · 1d ago
> At one time, Go maps were not thread-safe. Was that fixed?
sync.Map was added, but isn't intended to be a general purpose map.
——
The Map type is specialized. Most code should use a plain Go map instead, with separate locking or coordination, for better type safety and to make it easier to maintain other invariants along with the map content.
The Map type is optimized for two common use cases: (1) when the entry for a given key is only ever written once but read many times, as in caches that only grow, or (2) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys. In these two cases, use of a Map may significantly reduce lock contention compared to a Go map paired with a separate Mutex or RWMutex.
pjmlp · 1d ago
Exactly, by Google.
9rx · 1d ago
> The only reason it didn't end on pile of obscure languages nobody uses, it called Google
Dart ended up on the pile of languages nobody uses. And Carbon? What's Carbon? Exactly!
> Case in point, Limbo and Oberon-2, the languages that influenced its design
Agreed. Limbo and Oberon-2, as primitive as they may look now, had the kitchen sinks of their time. Why wouldn't they have ended up on the pile of languages nobody uses?
pjmlp · 1d ago
People love to bring those as counter examples, without actually knowing a single fact about them.
Dart was a victim of internal politics between the Chrome team, Dart team, AdWords moving away from GWT wanting AngularDart (see Angular documentary), and the Web in general.
Had Chrome team kept pushing DartVM, it might have been quite different story.
Carbon, good example of failure to actually know what the team purposes are. It is officially a research project for Google themselves, where the team is the first to advise using Rust or another MSL.
One just needs to actually spend like a couple of minutes on their wiki, but I guess that is asking too much on modern times.
Limbo and Oberon-2 were definitely not kitchen sinks of their time, their failure was that neither Bell Labs in 1996, nor ETHZ in 1992, were that relevant for the programming language community in the industry.
9rx · 1d ago
> Had Chrome team kept pushing DartVM, it might have been quite different story.
Trouble with that line of thinking is that Google never pushed Go either. It didn't even bother to use it internally (outside from the occasional side project here and there). Google paid some salaries. I'll give you that. But it has paid salaries for a lot of different languages. That is not some kind of secret sauce.
> It is officially a research project for Google themselves
It's not just a research project. It is officially "not ready for use", but its roadmap has a clear "ready for use" plan in the coming months. Rust was also "not ready for use" when it hit the streets, it officially being a Mozilla research project, but every second discussion on HN was about it and what is to come. And that was without Google backing. If what you say is true, why isn't Carbon being shouted from every rooftop right now?
I know you're struggling to grasp at straws here, but let's just be honest for a moment: If it hasn't caught attention already, it isn't going to. Just another language to add to the pile.
pjmlp · 16h ago
I guess Kubernetes doesn't count as Google pushing it then.
It is officially "not ready to use", it isn't a strawman as people keep complaining about nothing.
9rx · 7h ago
Kubernetes is written in Go (-ish. More like written in Java using Go syntax, especially in its early days when you claim it did some kind of pushing). But what kind of push are you dreaming up that it offered? If anything, Kubernetes leaves you to question if you ever want to write software again. The idea that it made anyone, let alone legions of people required to make something a "success", think "This is great. I should write my program in Go too!" is laughable.
stouset · 1d ago
Having the weight of Google behind it is the primary reason it didn't end up on the pile of obscure languages nobody uses.
9rx · 1d ago
Oh...? Dart never gained much steam. And let's not forget about Carbon! Can you name even just one person who has tried Carbon? Have more than a handful of people even heard of Carbon?
I will grant you that Carbon is still in its infancy, but when Rust was in the same youthful stage we never heard an end to all the people playing with it. You, even if not tried it yourself, definitely knew about it.
You've made up a fun idea, but reality doesn't support it. Google has not shown its weight carries anything. They have really struggled to get any for-profit business units off the ground since they gained the weight, never mind their hobbies! If anything, Google is detrimental to a project.
pjmlp · 1d ago
Already explained in another thread, learn the politics of Dart, and Carbon is still on the drawing board.
geodel · 23h ago
So failures are some deep valid reasons whereas success is developers don't know any better language.
pjmlp · 16h ago
Many don't, otherwise they would not do stuff like using Python for performance workloads.
Lets also not forget Rob Pike famous quote regarding simple minds, as target audience.
As for Go, Kubernetes made it unavoidable, it is like UNIX for C, Web for JavaScript, and so forth.
9rx · 6h ago
> Many don't, otherwise they would not do stuff like using Python for performance workloads.
While you fairly point out that many fall into Python because they learned about it in school and never bother to look beyond, Go has had no such equivalent. For you to choose it, you have to actively seek it out, where you are going to also see all the other programming languages you could also choose.
> As for Go, Kubernetes made it unavoidable, it is like UNIX for C, Web for JavaScript, and so forth.
UNIX's programming interface is a set of C functions. You are right that C is the path of least resistance to use it.
The web's programming interface is Javascript. You are right that Javascript is the path of least resistance to use it.
Kubernetes' programming interface is a "REST API" – or your program running inside a container, if you want to look at it from the other direction. In what way is Go the path of least resistance to use it?
9rx · 1d ago
Already...? Said "explanation" was posted over an hour after the comment replied to here.
If only Google put their weight into a watch, maybe you'd have one?
Oh wait. They did! Google can't successfully turn their weight into much of anything. Go's success, if we can call it that, clearly happened in spite of Google.
stouset · 20h ago
Googlers aren’t expected to wear a Google-branded watch at work. They are expected to write go. Having an entire Google’s worth of programmers using your programming language isn’t exactly a minor influence.
9rx · 17h ago
> They are expected to write go.
Like who? Outside of Go itself, which is really more of a community project — albeit with the chief maintainers still on Google's payroll, almost nothing at Google is written in Go. In fact, Pike once gave a talk reflecting on why it didn't succeed in that space, and noted that it was the "Python and Ruby programmers" who actually ended up adopting it.
Google makes money selling services (i.e. Google Cloud) that run Kubernetes, Docker, etc. If it weren't for that, it is unlikely that Google would even be continuing to maintain it at this point. It was an interesting experiment, perhaps, but ultimately a failure within Google. As before, it was the Python and (probably most especially) Ruby communities that ended up leaning into it.
Which isn't surprising in hindsight. Go offered those who were using Python and Ruby a language that was in the same kind of vein, while solving many of the pain points they were experiencing with Python and Ruby (awful deployment strategies, terrible concurrency stories, trouble with performance, etc.) These developers were never going to use Haskell. They wanted Ruby with less problems.
And that's what Go gave them — at least to the extent of being better than any other attempt to do the same. Since it solved real problems people had, without forcing them into new programming paradigms, it was adopted. Choosing a technology based on starry-eyed fandom and arbitrary feelings might be how you go about navigating this world, but that doesn't extrapolate.
geodel · 17h ago
> They are expected to write go.
This got to be a joke right. The only thing I hear is at Google no one likes Go. Most software is in C++, Rust, Java or Kotlin.
stouset · 20h ago
Necessary and sufficient are two separate concepts.
Yoric · 1d ago
Well, that and the slight fact that it bears Google's brand name.
I personally appreciate Go as a research experiment. Plenty of very interesting ideas, just as, for instance, Haskell. I don't particularly like it as a development language, but I can understand why some people do.
9rx · 1d ago
> Plenty of very interesting ideas
Is there? When you get down to it, it is really just a faster Python. Which is exactly what it was said to be when it was released. Their goal was to create a "dynamically-typed" language that was more performant. It is likely that it wouldn't have had a static type system at all if they figured out how to achieve on the performance end without needing types.
You can tell who is clueless when you hear someone say its type system is lacking. I mean, technically it is, but it is supposed to be. Like saying Javascript or Ruby's type system is lacking.
pjmlp · 1d ago
Faster Python with a very small set of its capabilities.
Yoric · 1d ago
Two examples of interesting ideas:
- using zero values as an optimization mechanism;
- (non-)pointers and passing self by copy.
I mean, I hate both mechanisms, but intellectually, I find them quite interesting.
Also, I'd not classify it as a faster Python. It's more of a cousin of Obj-C if the authors of Obj-C had fallen in love of Erlang instead of Smalltalk.
bombela · 3h ago
Those ideas where already present in C/C++ decades prior. BSS program area. And passing struct by value vs pointer.
nosefrog · 11h ago
Hi Chad!
shadowgovt · 1d ago
Mostly because it was a remarkable improvement over what came before (and what came before was hilariously fragile).
int_19h · 15h ago
It was certainly not a remarkable improvement in the sense of being memory safe even in the face of race conditions. As the article points out, Java and C# both managed to do that, and both predate Go.
pjmlp · 1d ago
Only for those not paying attention outside mainstream, or too young to remember former languages.
the_plus_one · 1d ago
> or too young to remember former languages.
Do you have any good examples? Not trying to argue, just genuinely curious as someone who hasn't been in this field for decades.
LtWorf · 1d ago
Basically go was designed ignoring all the research and progress that had been made in programming languages until then.
It was designed with contempt for developers, for example disallowing developers to create generic data structures, or lacking a decent way of error checking that is not extremely error prone and verbose.
shadowgovt · 1d ago
I'm certainly not disagreeing, but I will note that by definition, most people are in the mainstream, so something being a remarkable improvement over what came before (in the mainstream) is a remarkable improvement (for most people).
gok · 20h ago
Java is not memory-safe in the Rust sense.
ferreiratb · 20h ago
Can you elaborate on that?
wpollock · 19h ago
A Java program can share mutable state between threads without synchronization, and it will compile and run. In Rust, such a program will not compile.
int_19h · 15h ago
Yes, but even so you will never see e.g. an invalid pointer value as the result of a torn memory write. Basically, no matter what you do with threads in Java, it will not segfault.
TFA's point is that (safe) Rust is also like that, but achieves it by restricting all cases where a torn write could be observed through its type system instead of VM's memory model.
dontlaugh · 13h ago
More specifically, Rust prevents data races.
adamwk · 1d ago
Crashing on shared access is the safe thing to do
mirashii · 1d ago
An intentional exit by a runtime is a safe crash. A segfault is not, and is here a clear sign that memory safety has been violated.
adamwk · 1d ago
I guess I was thinking specifically of the swift case where values have exclusive access enforcement. Normally caught by a compiler, they will safely crash if the compiler didn’t catch it. I think the only way to segfault would be by using Unsafe*Pointer types, which are explicitly marked unsafe
Gibbon1 · 1d ago
Yeah it's not the segfault that's bad, it's when it's when the write to address 0x20001854 succeeds and now some hapless postal clerk is going to jail.
sapiogram · 21h ago
"Crashing" is a very positive spin. "The heap getting the corrupted until it was killed by the operating system" is another interpretation.
LtWorf · 1d ago
a segfault is completely unintentional. Had the kernel been older it could be used to execute code.
> Faulted trying to access 0x10 - the offset in the string we were trying to read from :)
Is guaranteed that every offset you can try to read is guaranteed to create a segfault?
cesarb · 2h ago
> Is guaranteed that every offset you can try to read is guaranteed to create a segfault?
The offset is fixed as part of the compiled code; the JVM can enforce that it's less than 4k (otherwise it can use an explicit NULL check), and that the first 4k page is always unmapped.
Mawr · 1d ago
Safety isn't binary, so your comment makes no sense.
kstrauser · 1d ago
I’d argue that unsafety is binary. If a normal eng doing normal things can break it without going out of their way to deliberately fool the compiler or runtime, I’d call it unsafe.
Calavar · 1d ago
By that definition Rust also counts as unsafe. Even managed languages like C# and Java would be unsafe.
gpm · 1d ago
There's a reason why rust devs qualify it as "memory safe" so frequently, we tend to agree that rust is, like virtually every current programming language, unsafe in other ways.
Memory safety is just the source of bugs that we've figured out how to eliminate. It's a significant source of really bad (hard to debug due to action at a distance, high impact, etc) bugs so that's worth a lot, but it's not perfect. And even then we have a more frequently used escape hatch to the memory-unsafe world than would be ideal from a safety perspective for practical reasons.
A more complete version of safety would be achieved with a language that proves code correct to arbitrary specifications. We aren't there yet for there being such a language that is practical for every day use. Personally I'm increasingly optimistic we'll get there sooner rather than later (say, within 20 years). Even then there will probably be specification level bugs that prevent a claim of complete safety...
kstrauser · 1d ago
My impression of the Rust devs is that they’d agree with you about any easy-to-trigger calamities. So would Java contributors. C# might not because MS is institutionally not good about admitting mistakes, but I bet the individual devs would agree over a beer.
ackfoobar · 1d ago
Do you have some examples? I think JDK developers make a lot of effort to make sure users bugs will not corrupt the runtime.
dcminter · 1d ago
What kinds of breakage do you have in mind though? The number of times I've segfaulted the JVM is tiny.
sapiogram · 21h ago
"Memory safety" has a specific, binary definition.
0xbadcafebee · 18h ago
It has two definitions?
chc4 · 1d ago
This is one of the things that I'm also looking on at Zig like a slow moving car crash about: they claim they are memory safe (or at least "good enough" memory safe if you use the safe optimization level, which is it's own discussion), but they don't have the equivalent to Rust's Send/Sync types. It just so happens that in practice no one was writing enough concurrent Zig code to get bitten by it a lot, I guess...except that now they're working on bringing back first-class async support to the language, which will run futures on other threads and presumably a lot of feet are going to be fired at once that lands.
ameliaquining · 1d ago
IIUC even single-threaded Zig programs built with ReleaseSafe are not guaranteed to be free of memory corruption vulnerabilities; for example, dereferencing a pointer to a local variable that's no longer alive is undefined behavior in all optimization modes.
Zig's claims of memory safety are a bad joke. Sure, it's easier to avoid memory safety bugs in Zig than it is in C, but that's also true of C++ (which nobody claims is a memory safe language).
jchw · 1d ago
This comes up now and again, somewhat akin to the Rust soundness hole issue. To be fair, it is a legitimate issue, and you could definitely cause it by accident, which is more than I can say about the Rust soundness hole(s?), which as far as I know are basically incomprehensible and about as likely to come across naturally as guessing someone's private key.
That said in many years of using Go in production I don't think I've ever come across a situation where the exact requirements to cause this bug have occurred.
Uber has talked a lot about bugs in Go code. This article is useful to understand some of the practical problems facing Go developers actually wind up being, particularly the table at the bottom summarizing how common each issue is.
They don't have a specific category that would cover this issue, because most of the time concurrent map or slice accesses are on the same slice and this needs you to exhibit a torn read.
So why doesn't it come up more in practice? I dunno. Honestly beats me. I guess people are paranoid enough to avoid this particular pitfall most of the time, kind of like the Technology Connections theory on Americans and extension cords/powerstrips[1]. Re-assigning variables that are known to be used concurrently is obvious enough to be a problem and the language has atomics, channels, mutex locks so I think most people just don't wind up doing that in a concurrent context (or at least certainly not on purpose.) The race detector will definitely find it.
For some performance hit, though, the torn reads problem could just be fixed. I think they should probably do it, but I'm not losing sweat over all of the Go code in production. It hasn't really been a big issue.
It took months to finally solve a data race in Go. No race detector would see anything. Nobody understood what was happening.
It ultimately resulted in a loop counter overflowing, which recomputed the same thing a billion of time (but always the same!). So the visible effect was a request would randomly take 3 min instead of 100ms.
I ended up using perf in production, which indirectly lead me to understand the data race.
I was called in to help the team because of my experience debugging the weirdest things as a platform dev.
Because of this I was exposed to so many races in Go, from my biased point of view, I want Rust everywhere instead.
But I guess I am putting myself out of a job? ;)
norir · 1d ago
It is very unfortunate that we use fixed width numbers by default in most programming languages and that common ops will silently overflow. Smarter compilers can work with richer numeric primitives and either automatically promote machine words to big numbers or throw an error on overflow.
People talk a lot about the productivity gains of ai, but fixing problems like this at the language level could have an even bigger impact on productivity, but are far less sensational. Think about how much productivity is lost due to obscure but detectable bugs like this one. I don't think rust is a good answer (it doesn't check overflow by default), but at least it points a little bit in the vaguely correct direction.
recursivecaveat · 1d ago
The situation with numbers in basically every widely used programming language is kind of an indictment of our industry. Silent overflow for incorrect results, no convenient facilities for units, lossy casts everywhere. It's one of those things where standing in 1975 you'd think surely we'll spend some of the next 40 years of performance gains to give ourselves nice, correct numbers to work with, but we never did.
tomp · 1d ago
"nice, correct numbers" end somewhere between 1/3 and sqrt(2)
so in reality, it's just "pick your own poison" to various degrees...
tialaramex · 21h ago
The square root of two is still a computable Real. We choose not to cope with that, but it's not actually impossible it was merely inconvenient. I've mentioned elsewhere that my Rust care realistic is quite happy to work with these numbers e.g. take the square root of ten, and the square root of forty, multiply them together and get the quite ordinary integer twenty.
The non computable reals are a huge problem because, as their name suggests, we can't compute them - and in the strict sense that's Almost All reals, but none of the ones you're thinking of are non-computable so you'll likely be fine.
For the merely rational numbers like a third, or sixteen hundred and five sevenths, it's even more so a matter of choosing not to address it rather than it being out of reach.
GolDDranks · 17h ago
The problem with computables is that equivalence between them is only semi-decidable. (If the two numbers are different, it is decidable, but if they are not, it isn't. The problem is that you don't know if they are different a priori, so you might get lucky and find difference, but you might as well not.)
We know for sure that algebraic numbers behave nicely in terms of equivalence, and there are other, bigger number systems that are conjectured to behave nicely ( https://en.wikipedia.org/wiki/Period_(algebraic_geometry) ), but the problem with these and computers is that they are hard to represent.
tialaramex · 13h ago
Yeah, all these types have problems, we've decided to put up with the IEEE floating point numbers, we could have chosen to have the big rationals, or drawn any other line. I don't disagree that there's no satisfying "correct" answer but it's a little disappointing that programmers so easily accept the status quo as though nothing else could be in its place.
Maybe Python having automatic big numbers like Lisps often did will help introduce new programmers to the idea that the 32-bit two's complement integer provided on all modern computers isn't somehow "really" how numbers work.
int_19h · 15h ago
Even so, we don't need to pick the more potent poison. And most certainly not when we had decades of awareness about better alternatives.
There's really no excuse for a modern PL to not have, at the very least, overflow detection by default.
astrange · 15h ago
Swift traps on overflow, which I think is the correct solution. You shouldn't make all your numbers infinitely-ranged, that turns all O(1) operations into O(N) in time and memory, and introduces a lot of possibilities for remote DoS.
devnullbrain · 1d ago
Rust checks overflow by default in debug builds
jeffparsons · 19h ago
I've often thought that I'd prefer it to check by default in release builds, too, but I understand that comes with a performance penalty that a lot of folks aren't happy with.
I assume this implies that common processor architectures (x86_64, aarch64) lack trap-on-overflow variants of their integer arithmetic instructions? If the explanation really is that simple, it's pretty disappointing.
tialaramex · 13h ago
You can tell the compiler that you want your crate to perform the checks regardless.
You can also either (in nightly Rust) use the strict APIs which make it explicit that you want the overflow panics, or, (stably) use the checked APIs and then do whatever makes sense, which could include explicitly panic when overflow would happen unexpectedly.
This would let you have e.g. code where most arithmetic is checked, but a tight inner loop you're pretty sure won't overflow only has checks in debug (in release it will wrap, but you should not rely on that for correctness, unintended overflow is a bug)
kbolino · 7h ago
> I assume this implies that common processor architectures (x86_64, aarch64) lack trap-on-overflow variants of their integer arithmetic instructions?
Yes*. But all modern instruction sets have condition flags and conditional instructions, so it's still very much possible to implement the checks robustly in machine code. However, doing so would generally require injecting at least one additional conditional-branch instruction, and in some cases, switching from non-flag-setting instructions to flag-setting instructions (which can be slower).
* = true "trap on overflow" existed in 32-bit x86 but was tricky to use and got removed when going to 64-bit
jchw · 1d ago
I think the true answer is that the moment you have to do tricky concurrency in Go, it becomes less desirable. I think that Go is still better at tricky concurrency than C, though there are some downsides too (I think it's a bit easier to sneak in a torn read issue in Go due to the presence of fat pointers and slice headers everywhere.)
Go is really good at easy concurrency tasks, like things that have almost no shared memory at all, "shared-nothing" architectures, like a typical web server. Share some resources like database handles with a sync.Pool and call it a day. Go lets you write "async" code as if it were sync with no function coloring, making it decidedly nicer than basically anything in its performance class for this use case.
Rust, on the other hand, has to contend with function coloring and a myriad of seriously hard engineering tasks to deal with async issues. Async Rust gets better every year, but personally I still (as of last month at least) think it's quite a mess. Rust is absolutely excellent for traditional concurrency, though. Anything where you would've used a mutex lock, Rust is just way better than everything else. It's beautiful.
But I struggle to be as productive in Rust as I am in Go, because Rust, the standard library, and its ecosystem gives the programmer so much to worry about. It sometimes reminds me of C++ in that regard, though it's nowhere near as extremely bad (because at least there's a coherent build system and package manager.) And frankly, a lot of software I write is just boring, and Go does fine for a lot of that. I try Rust periodically for things, and romantically it feels like it's the closest language to "the future", but I think the future might still have a place for languages like Go.
dev_l1x_be · 13h ago
> But I struggle to be as productive
You should calculate TCO in productivity. Can you write Python/Go etc. faster? Sure! Can you operate these in production with the same TCO as Rust? Absolutely not. Most of the time the person debugging production issues and data races is different than the one who wrote the code. This gives the illusion of productivity being better with Python/Go.
After spending 20+ years around production systems both as a systems and a software engineer I think that Rust is here for reducing the TCO by moving the mental burden to write data race free software from production to development.
jchw · 3h ago
With the notable inclusion of Google where the SRE team is usually separate from the SWE team (but it wasn't for my particular case) I actually was always doing operations and code at all of my jobs at least at points and usually during most of the job. This is in part my own election. I do mean all of them though I don't really love listing my work history publicly everywhere just to keep some separation.
So, my first job actually started as a pure Python gig. Operations for Python/Django absolutely sucked ass. Deploying Django code reliably was a serious challenge. We got better over time by using tools like Vagrant and Docker and eventually Kubernetes, so the differences between production and dev/testing eventually faded and become less notable. But frankly no matter what we did, not causing production issues with Django/Python was a true-to-life nightmare. Causing accidental type errors not caught by tests was easy and MyPy couldn't really cover all that much of the code easily, and the Django ORM was very easy to accidentally cause horrible production behavior with (that, of course, would look okay locally with tiny amounts of data.) This is actually the original reason why I switched to Go in the first place, at my first job in around 2016. The people who I worked with are still around to attest to this fact, if you want I can probably get them to chime in on this thread, I still talk to some of them.
Go was a totally different story. Yes, we did indeed have some concurrency pains, which really didn't exist in Python for obvious reasons, but holy shit, we could really eek a lot of performance out of Go code compared to Python. We were previously afraid we might have to move data heavy workloads from Twisted (not related to the Django stuff) to something like C++ or maybe even optimized Java, but Go handily took it and allowed us to saturate the network interface on our EC2 boxes. (A lot of communications were going over Websockets, and the standards for compression in websockets took a long time to settle and become universally supported, so we actually played with implementing the lz4 compression scheme in JS. I wound up writing my own lz4 implementation based on the algorithms, I believe, from the C version. It wound up being too much compute, though. But, we had to try, anyway.)
So how much reliability problems did we wind up having doing all this? Honestly not a whole lot on the Go side of things. The biggest production issue I ever ran into was one where the Kubernetes AWS integration blew up because we wound up having too many security groups. I wound up needing to make an emergency patch to kubelet in the early hours to solve that one :) We did run into at least one serious Go related issue over time, which was indeed concurrency related: when Go 1.6 came out, it started detecting concurrent misuses of maps. And guess what? We had one! It wasn't actually triggering very often, but in some cases we could run into a fairly trivial concurrent map access. It didn't seem to crash before but it could at least cause some weird behaviors in the event that it actually triggered before Go 1.6; now it was a crash that we could debug. It was a dumb mistake and it definitely underscores the value of borrow checking; "just don't mess up" will never prevent all mistakes, obviously. I will never tell you that I think borrow checking is useless, and really, I would love to just always write 100% correct software all the time.
That said though, that really is most of the extent of the production issues we had with Go. Go was a serious workhorse and we were doing reasonably non-trivial things in Go. (I had essentially built out a message queue system for unreliable delivery of very small events. We had a firehose of data coming in with many channels of information and needed to route those to the clients that needed them and handle throttling/etc. Go was just fantastic at this task.) Over time things got easier too, as Go kept updating and improving, helping us catch more bugs.
I can only come to one conclusion: people who treat Go and Python in the same class are just ignorant to the realities of the situation. There are cases where Rust will be immensely valuable because you really can't tolerate a correctness problem, but here's the thing about that Go concurrent map access issue: while it could cause some buggy behavior and eventually caused some crashing, it never really caused any serious downtime or customer issues. The event delivery system was inherently dealing with unreliable data streams, and we had multiple instances. If there was a blip, clients would just reconnect and people would barely notice anything even if they were actively logged in. (In fact, we really didn't do anything special for rolling deployments to this service, because the frontend component was built to just handle a disconnection gracefully. If it reconnected quickly enough, there was no visual disturbance.)
That's where the cost/benefit analysis gets tricky though. Python and Django and even Twisted are actually pretty nice and I'm sure it's even better than when we originally left it (to be clear we did still have some minor things in Django after that, too, but they were mostly internal-only services.) Python and Django had great things like the built-in admin panel which, while it couldn't solve everyone's needs, was pretty extensible and usable on its own. It took us a while to outgrow it for various use cases. Go has no equivalent to many Django conveniences, so if you haven't fully outgrown e.g. the Django admin panel and ORM, it's hard to fully give up on those features.
Throughout all of this, we had a lot more issues with our JS frontend code than we ever did with either Python/Django or Go, though. We went through trying so many things to fix that, including Elm and Flow, and eventually the thing that really did fix it, TypeScript. But that is another story. (Boy, I sure learned a lot on my first real career job.)
At later jobs, Go continued to not be at the center of most of the production issues I faced running Go software. That's probably partly because Go was not doing a lot of the most complicated work, often times the most complicated bits were message queues, databases and even to some degree memory caches, and the Go bits were mostly acting like glue (albeit definitely glue with application logic, to be sure.)
So is the TCO of Go higher than Rust? I dunno. You can't really easily measure it since you don't get to explore parallel universes where you made different choices.
What I can say is that Go has been a choice I never regretted making all the way from the very first time and I would choose it again tomorrow.
bombela · 22h ago
It wasn't really tricky concurrency. Somebody just made the mistake of sharing a pointer across goroutines. It was quite indirect. Boils down to a function takeing a param and holds onto it. `go` is used at some point closing over this pointer. And now we have a data race in the waiting.
jchw · 19h ago
Aside from the type system bypass described in the article though, this is basically no different from the status quo for virtually all languages with free threading that aren't Rust. I argue that while everyone is fallible, experienced programmers usually don't make this sort of mistake directly, because they are usually well aware that sharing pointers over a closure on another thread is a recipe for disaster. Instead, I think that most of these issues are actually involving tricky circumstances that result in this happening by accident, like accidentally re-using an err variable from the enclosing function of a closure. These sorts of bugs are really bad, because they happen in spite of everyone knowing not to inappropriately share data across goroutines, and they are easy to sneak by in code reviews unnoticed. They don't intuitively look like concurrency bugs, and sometimes they're as little as one single : away from being correct. (Which I agree is a bad place for a language design to be.)
Thankfully though, people don't just throw their hands up there; a good amount of work has gone into figuring out the kinds of mistakes that often lead to Go concurrency bugs in the real world and writing static analysis tools that can help prevent them. That work, combined with Go's builtin tools and standard library, and the memory safety of individual isolated goroutines, makes most production Go concurrency bugs fairly boring even compared to C concurrency bugs, even though they theoretically have the same basic problem where you can freely share mutable data unsafely across concurrent threads.
So yes, it is still possible to write trivial, obvious concurrency bugs. The language won't stop you. However I've used Go across almost every job I've had since like 2016 and it has been rare to come across a concurrency bug this trivial. I hope I would catch flagrantly shared mutable state across threads during code review.
bombela · 3h ago
Yes experienced programmers won't make the obvious mistake... until they do because the distance from the source of the bug to it's manifestation is too great to notice until it fails in production.
jchw · 3h ago
Look, this is pointless. I'm not learning anything new when you tell me that it can and will happen. How will it happen and how much will it happen?
Hence linking to Uber's case study on the issue. The answer? Not that much.
Uber started performing race detection in production over a 6 month period and found 2,000 different race conditions. Ouch, that sounds horrible!
But wait, we're talking about 50 million lines of Go code and 2,100 services at the time of that writing. That means they were seeing approximately 1 race condition per 25,000 lines of code and about 1 race condition per service. That actually lines up pretty well with my experiences. Although I haven't had a production outage or serious correctness issue caused by a race condition in Go, I have seen probably about one or two race conditions that made it to production per service. I reckon those codebases were likely somewhere between 10,000 and 25,000 lines of code most likely, so not so far off of the scale.
But again it doesn't always lead to a serious production outage, it's just that simple. It could be worse too (could corrupt some data and pollute your production database or something, in the worst case) but usually it's better (wonky behavior but no long-term effects, maybe the service periodically crashes but restarts, leading to some dropped requests but no long term downtime.) Uber has no doubt seen at least some Go data races that have caused actual production outages, but they've seen at least 2,000 Go data races that haven't, otherwise they would've probably been caught before the race detector caught them, Go dumps stacktraces on crash. That has to tell you something about the actual probability of causing a production outage due to a data race.
Again, you do you, but I will not be losing sleep over this. It is something to be weary of when working on Go services, but it is manageable.
zozbot234 · 2h ago
Identifiable "wonky" behavior and periodic crashes seem like a very real issue to me. This wouldn't fly for any mission-critical service, it's something that demands a root cause analysis. Especially since it's hard to be sure after the fact that no data has been corrupted somehow or that security invariants have not been violated due to the "wonky" behavior.
jchw · 2h ago
I am saying in no uncertain terms that most people here, and by most I am not talking simple majority stuff, have literally not once worked on software that is mission critical by any meaningful definition of "mission critical". Even Rust is questionable on truly mission critical software, since it does not actually prevent all runtime crashes and certainly not all correctness issues; you'd have to go further, towards something like Ada/SPARK for that. I kind of wish I could get into Ada/SPARK, too, to be honest, but it's a pretty big rabbithole it seems.
zozbot234 · 2h ago
A meaningful definition of "mission critical" is just "serious money can be lost if this software crashes or misbehaves in problematic ways". That would seem to cover a whole lot of software that is not written in Ada/SPARK or anything comparable. I'm not talking about the "safety critical" kind where actual human lives may be at stake, only about the well known run-of-the-mill stuff.
jchw · 1h ago
In that case, when we're just talking about money, it's pretty easy to reason about this then, no? You can literally determine how much risk you're willing to take on by estimating what you might have to lose from such a bug and how much it might cost you versus how often they are likely to happen. The answer for how often is "not very often", and depending on the nature of the bug the monetary cost of it may be "pretty much $0" in the easy cases. Let's be conservative and say that you might see a moderate severity Go concurrency bug every 10,000 lines of Go code or so. That's still really not much. It means that a moderate sized 50,000 line code program might see like five of said kinds of bugs, and they might wind up being benign. Computers and networks are unreliable. Dropping some requests occasionally or having a weird bug for a small fraction of requests or database records is usually not going to cause you serious financial distress as a business.
When working on Go services it is nearly the last thing I am concerned about.
zozbot234 · 21m ago
> Computers and networks are unreliable. Dropping some requests occasionally or having a weird bug for a small fraction of requests
This seems to come with the obvious implication that Golang should only ever be used to implement "services" that are essentially a part of the network infrastructure, passing requests along to other parts of the backend but not implementing any "logic" themselves (since that's where the correctness issues we're discussing might have severe consequences for the business). Isn't this a rather sobering take, all things considered?
zozbot234 · 1d ago
> And frankly, a lot of software I write is just boring, and Go does fine for a lot of that. I try Rust periodically for things, and romantically it feels like it's the closest language to "the future", but I think the future might still have a place for languages like Go.
It's not so much about being "boring" or not; Rust does just fine at writing boring code once you get familiar with the boilerplate patterns (Real-world experience has shown that Rust is not really at a disadvantage wrt. productivity or iteration speed).
There is a case for Golang and similar languages, but it has to do with software domains where there literally is no viable alternative to GC, such as when dealing with arbitrary, "spaghetti" reference graphs. Most programs aren't going to look like that though, and starting with Rust will yield a higher quality solution overall.
Mawr · 1d ago
> (Real-world experience has shown that Rust is not really at a disadvantage wrt. productivity or iteration speed).
I don't believe that for a second. Even just going from Python to Go drops my productivity by maybe about 50%. Rust? Forget it.
Sure, if you have a project that demands correctness and high performance that requires tricky concurrency to achieve, something like Rust may make sense. Not for your run-of-the-mill programs though.
Yoric · 1d ago
Hey, going from Rust to Go drops my productivity by maybe about 50% :)
But more seriously, yeah, Rust doesn't make sense for trivial programs. But these days, I write Python for a living, and it doesn't take long to stumble upon bugs that Rust would have trivially detected from within the comfort from my IDE.
ralfj · 3h ago
See https://www.youtube.com/watch?v=QrrH2lcl9ew for a a presentation of Google's study, which found no measurable difference in productivity between teams using Rust vs Go.
Ar-Curunir · 2h ago
I am much more productive with Rust than any other programming language, except maybe python for programs shorter than 100 lines. Does that mean every other language has terrible productivity? No, it just means that I am more experienced with Rust. In general, experienced rust devs tend to be as efficient with Rust as other devs with other languages. There’s even Google data corroborating that internally Rust teams are as productive as Go teams
sophacles · 18h ago
I believe your productivity drops as you say. I don't think it's inherent to the language though, at least not most of it. Rather it think it's a matter of familiarity and experience in each. When you're less practiced in a language, you're slower at it. I can write python fast, but im pretty slow at ruby. I've written a lot of python rust and go, and am about equally productive in them (although how that productivity is distributed through the dev cycle is different). It wasn't always this way, I was slow in each of them at first.
brabel · 13h ago
Rust is objectively harder to write than Go (or any other GC-language) because it exposes more concerns for the programmer to take care of. With Rust, you must always ensure your program complies with the borrow checker's rules (which is what makes Rust memory-safe) which includes having to add lifetime annotations to your variables in many cases. Go just doesn't have any of that. You could argue Rust still has an advantage in that it prevents bugs that in Go you're free to write, but then what you're claiming is that this compensates for the extra work you have to do upfront in Rust. That's a difficult position to defend, though, because an experienced Go developer probably has internalized how to avoid those bugs and the cost of preventing them can be nearly negligible. I do agree that they will still make mistakes, and those can have a cost, but they may also be quite rare, or may not matter much most of the time depending on your domain. I think that's why most people seem to agree Rust is probably only advantageous where the cost of data races in production is higher than the cognitive cost (which translates into increased effort) on the programmer.
zozbot234 · 12h ago
> an experienced Go developer probably has internalized how to avoid those bugs and the cost of preventing them can be nearly negligible.
And an experienced Rust developer has internalized the patterns (such as cloning or ARC) that are needed to cope with the borrow checker while writing prototype-quality, quick-iteration code. What's easier, fixing hard-to-spot bugs in the code or getting that code to compile in the first place?
sophacles · 5h ago
>Rust is objectively harder to write than Go (or any other GC-language) because it exposes more concerns for the programmer to take care of.
comparing apples to apples: Once you get a tiny bit of experience, almost all of that goes away. The common patterns and idioms in the language allow you to write whole programs without ever thinking about lifetimes or memory allocation or anything else different from the gc language case.
comparing apples to oranges: you do need to worry about those things when writing tricky memory management code that you couldn't even get from most gc lanuages... yeah then you have to worry about the things since it's a case where those things are the point.
> You could argue Rust still has an advantage in that it prevents bugs that in Go you're free to write, but then what you're claiming is that this compensates for the extra work you have to do upfront in Rust.
I have evidence in the form of multiple services and programs running in prod under heavy use for years without having to revist the code to deal with bugs. Meanwhile the stuff written in go has to be touched a lot to deal with bugs. The extra couple of weeks upfront to do it in rust is mitigated after the first incident with the go code. The effort proves worthwhile after the second incident.
Also tangentially related: the cost of an incident in the form of lost business, refunds, etc is usually far higher than the cost of a couple developer weeks.
>because an experienced Go developer probably has internalized how to avoid those bugs and the cost of preventing them can be nearly negligible
Some of them yes. But this is literally the same argument I'm making about rust experience meaning that you don't spend all that much extra effort up-front. Like I said, I'm about equally productive in go, python or rust.
> I think that's why most people seem to agree Rust is probably only advantageous where the cost of data races in production is higher than the cognitive cost (which translates into increased effort) on the programmer.
I think people who say this haven't gotten much experience in rust. In my experience they spent a week trying to learn rust and decided to stop and compare it to their years of other languages and paradigms.
brabel · 3h ago
> I think people who say this haven't gotten much experience in rust. In my experience they spent a week trying to learn rust and decided to stop and compare it to their years of other languages and paradigms.
I have written Rust for around 6 years now.
sophacles · 2h ago
That doesn't say much about experience. Ive written ruby at a rate of a few dozen lines/year, for the 20 years.
I guess I could say I've written ruby for 20 years... But someone full-time in ruby for only a year would likely be significantly better at the language than I am (i am bad at it).
jchw · 1d ago
Rust can yield a higher quality solution, but we can't make a perfect solution, we can only approach perfection. If we want to go further, we could introduce formally-proven code, too. Personally I'm interested in the intersection of proof assistants and Rust, like creusot-rs, and have been investigating it.
But as much as I love LARPing about correctness (believe me I do,) it's just simply the case that we won't right perfect software and it's totally OK. It's totally OK that our software will have artificial limitations, like with Go, only accepting filenames that are valid UTF-8, or taking some unnecessary performance/latency hits, or perhaps even crashing in some weird ass edge case. There are very few domains in which correctness issues can't be tolerated.
I don't deal with domains that are truly mission critical, where people could die if the code is incorrect. At worst, people could lose some money if my code is incorrect. I still would prefer not to cause that to happen, but those people are generally OK with taking that risk if it means getting features faster.
That's why Go has a future really. It's because for most software, some correctness issues are not the end of the world, and so you can rely on not fully sound approaches to finding bugs, like automated testing, race detection, and so on.
Rust can also make some types of software more productive to write, but it is unlikely to beat Go in terms of productivity when it comes to a lot of the stuff SaaS shops deal with. And boy, the software industry sure is swamped in fucking SaaS.
josephg · 20h ago
> Rust can also make some types of software more productive to write, but it is unlikely to beat Go in terms of productivity when it comes to a lot of the stuff SaaS shops deal with. And boy, the software industry sure is swamped in fucking SaaS.
I just wish Go supported parametric enums (sum types) and Option, rather than copying Hoare’s billion dollar mistake.
I ported some code to Go and rust a few years ago to try both languages out. The rust code ended up being 30% smaller because I could use an enum and a match expression. In Go I needed to make a set of types and interface{} to achieve the same thing - which was both slower and way more verbose. My rust implementation was as fast as my C implementation in 2/3rds as much code. And it was trivial to debug. My Go implementation took way more code to write - about the same amount of code as C, but it was harder to read than C and ran much slower.
For cookie cutter SAAS and prototypes, I prefer typescript. It’s fast enough for most things, and the type system is much more expressive without getting in your way. Not as convenient to deploy as go - especially on mobile. And the standard library is more like an attic. But in my opinion it’s a much better designed language.
Sadly, that project seems to be dead, but I hope someone picks up its mantle some day. A marginally better Go could, well, go far.
sophacles · 1d ago
A lot of sass people i know are more and more choosing rust for boring code. This includes several people who said things like "go is good enough, i don't want to deal with all the rust completely".
Once your sass products get enough users, and you're dealing with millions or billions of requests per day, those rare bugs start showing up quite often... And it turns out programming towards correctness is desirable, if for no other reason than to keep pagerduty quiet. Tolerating correctness issues isn't cost-free... People having to respond during off hours costs money and stress. I think most people would rather pay the costs at dev time, when they aren't under the pressure of an incident, than during an outage.
jchw · 1d ago
But correctness is not binary, it's more like a multidimensional spectrum. Your choice of programming language has some influence, as does standards and conventions, the ecosystem of your programming language, use of automated tooling like linting and testing, or even just ol' unreliable, discipline. Being a relatively greenfield language, Go is not in a terrible place when it comes to most of those things. Tons of automated tooling, including tools like the Checklocks analyzer or the many tools bundled with golangci-lint. Uber has done a pretty good job enumerating the challenges that remain, and even working at improving those issues too, such as with NilAway.
The question isn't "wouldn't you prefer more correctness?" it's "how much would you pay for how much of an improvement in correctness?".
Rust is still growing rapidly though, whereas Go is probably not growing rapidly anymore, I think Go has at least saturated it's own niche more than 50% and is on the other end of the curve by now. Last I checked Rust is the trendiest language by far, the one that people most wish they were writing, and the one that you want to be able to say your project is written in. So it would be extremely surprising to hear if there wasn't a growing Rust presence basically everywhere, SaaS's included.
zozbot234 · 1d ago
This is also in line with everything we know about good software engineering. Putting out fires in production is extremely costly, hence potential issues should be addressed at the earliest feasible stage.
lossolo · 4h ago
> A lot of sass people i know are more and more choosing rust for boring code
It seems like you're in some kind of bubble, especially when looking at Rust usage in the industry.
> Once your sass products get enough users, and you're dealing with millions or billions of requests per day, those rare bugs start showing up quite often...
This is a blanket statement that's simply not true and I'm speaking as someone who uses Go in the exact scenario you described.
What kind of bugs are actually happening to these people? Do you have any real-world examples of the issues you're referring to, ones that suddenly start occurring only at the scale of millions or billions of requests per day to them?
wavemode · 1d ago
> It ultimately resulted in a loop counter overflowing, which recomputed the same thing a billion of time (but always the same!). So the visible effect was a request would randomly take 3 min instead of 100ms.
This means that multiple goroutines were writing to the same local variable. I've never worked on a Go team where code that is structured in such a way would be considered normal or pass code review without good justification.
bombela · 21h ago
It happens all the time sadly.
It's not because people intentionally write this way. A function takes a parameter (a Go slice for example) and calls another function and so one. Deep down a function copies the pointer to the slice (via closure for example). And then a goroutine is spawned with this closure.
The most obvious mistakes are caught quickly. Buu sharing a memory address between two threads can happen very indirectly.
And somehow in Go, everybody feels incredibly comfortable spawning millions of coroutines/threads.
Thaxll · 1d ago
Rust does have loop counter overflow.
jason_oster · 1d ago
This is irrelevant on 64-bit platforms [^1] [^2]. For platforms with smaller `usize`, enable overflow-checks in your release builds.
Theoretically you can construct a loop counter that overflows, but I don't that there is any reasonable way to do it accidentally?
Within safe rust you would likely need to be using an explicit .wrapping_add() on your counter, and explicitly constructing a for loop that wasn't range-based...
tekne · 1d ago
Well, in debug mode yes, but in release mode overflow is wrapping by default unless you explicitly set a flag to make it panic.
ameliaquining · 1d ago
I think it's also worth noting that Rust's maintainers acknowledge its various soundness holes as bugs that need to be fixed. It's just that some of them, like https://github.com/rust-lang/rust/issues/25860 (which I assume you're referring to), need major refactors of certain parts of the compiler in order to fix, so it's taking a while.
ralfj · 8h ago
Yeah, I can totally believe that this is not a big issue in practice.
But I think terms like "memory safety" should have a reasonably strict meaning, and languages that go the extra mile of actually preventing memory corruption even in concurrent programs (which is basically everything typically considered "memory safe" except Go) should not be put into the same bucket as languages that decide not to go through this hassle.
sethammons · 8h ago
That Uber article is fantastic. I believe Go fixed the first example recently.
We had a rule at my last gig: avoid anonymous functions and always recover from them.
qcnguy · 1d ago
What do Uber mean in that article when they say that Go programs "expose 8x more concurrency compared to Java microservices"? They're using the word concurrency as if it were a countable noun.
wmf · 1d ago
If the Java version creates 4 concurrent tasks (could be threads, fibers, futures, etc.) but the Go version creates 32 goroutines, that's 8x the concurrency.
camgunz · 5h ago
I feel like I'm defending Go constantly these days. I don't even like Go!
Go can already ensure "consistency of multi-word values": use whatever synchronization you want. If you don't, and you put a race into your code, weird shit will happen because torn reads/writes are fuckin weird. You might say "Go shouldn't let you do that", but I appreciate that Go lets me make the tradeoff myself, with a factoring of my choosing. You might not, and that's fine.
But like, this effort to blow data races up to the level of C/C++ memory safety issues (this is what is intended by invoking "memory safety") is polemic. They're nowhere near the same problem or danger level. You can't walk 5 feet through a C/C++ codebase w/o seeing a memory safety issue. There are... zero Go CVEs resulting from this? QED.
"To sum up: most of the time, ensuring Well-Defined Behavior is the responsibility of the type system, but as language designers we should not rule out the idea of sharing that responsibility with the programmer."
dcsommer · 5h ago
Unsafety in a language is fine as long as it is clearly demarcated. The problem with Go's approach is there no clear demarcation of the unsafety, making reasoning about it much more difficult.
camgunz · 5h ago
The "go" keyword is that demarcation
bobbylarrybobby · 4h ago
“go” being a necessary keyword even for benign operations makes its use an unsafety marker pointless; you end up needing to audit your entire codebase anyway. The whole point of demarcation is that you have a small surface area to go over with a fine-toothed comb.
camgunz · 1h ago
You're free to structure code in and around goroutines however you like. No one's forcing you to spray this all over your codebase. Feel free to implement work queues, or the actor pattern, or whatever. No one's forcing you to use whatever frameworks or libraries or patterns; that's all you.
So much of all of this is weirdly entitled. Languages that do things differently exist: Erlang, Pony, Rust. They all make wildly different tradeoffs than Go does, not better, not worse, different. If you think they're better, use 'em. Let your better software win in the market. These weirdo polemics just fan language flamewars.
tptacek · 1d ago
This is a canard.
What's happening here, as happens so often in other situations, is that a term of art was created to describe something complicated; in this case, "memory safety", to describe the property of programming languages that don't admit to memory corruption vulnerabilities, such as stack and heap overflows, use-after-frees, and type confusions. Later, people uninvolved with the popularization of the term took the term and tried to define it from first principles, arriving at a place different than the term of art. We saw the same thing happen with "zero trust networking".
The fact is that Go doesn't admit memory corruption vulnerabilities, and the way you know that is the fact that there are practically zero exploits for memory corruption vulnerabilities targeting pure Go programs, despite the popularity of the language.
Another way to reach the same conclusion is to note that this post's argument proves far too much; by the definition used by this author, most other higher-level languages (the author exempts Java, but really only Java) also fail to be memory safe.
Is Rust "safer" in some senses than Go? Almost certainly. Pure functional languages are safer still. "Safety" as a general concept in programming languages is a spectrum. But "memory safety" isn't; it's a threshold test. If you want to claim that a language is memory-unsafe, POC || GTFO.
kllrnohj · 1d ago
> in this case, "memory safety", to describe the property of programming languages that don't admit to memory corruption vulnerabilities, such as [..] type confusions
> The fact is that Go doesn't admit memory corruption vulnerabilities
Except it does. This is exactly the example in the article. Type confusion causes it to treat an integer as a pointer & deference it. This then trivially can result in memory corruption depending on the value of the integer. In the example the value "42" is used so that it crashes with a nice segfault thanks to lower-page guarding, but that's just for ease of demonstration. There's nothing magical about the choice of 42 - it could just as easily have been any number in the valid address space.
dboreham · 1d ago
Everyone knows that there's something very magical about the choice of 42.
Sharlin · 1d ago
> to describe the property of programming languages that don't admit to memory corruption vulnerabilities, such as stack and heap overflows, use-after-frees, and type confusions.
And data races allow all of that. There cannot be memory-safe languages supporting multi-threading that admit data races that lead to UB. If Go does admit data races it is not memory-safe. If a program can end up in a state that the language specification does not recognize (such as termination by SIGSEGV), it’s not memory safe. This is the only reasonable definition of memory safety.
tptacek · 1d ago
If that were the case, you'd be able to support the argument with evidence.
chowells · 1d ago
You mean like the program in the article where code that never dereferences a non-pointer causes the runtime to dereference a non-pointer? That seems like evidence to me.
tptacek · 1d ago
An exploit against a real Go program that relies on memory corruption.
afdbcreid · 1d ago
It should be possible to construct an exploit for such programs. But even for truly unsafe languages, vulnerabilities just from data races are very rare, because they are much harder to exploit.
You could argue Go is safe from memory vulnerabilities, and that'll be 99% correct (we can't know what will happen if some very strong organization (e.g. a nation-state actor) will heavily invest in exploiting some Go program), but it still isn't memory safe, as per the definition in Wikipedia:
> Memory safety is the state of being protected from various software bugs and security vulnerabilities when dealing with memory access, such as buffer overflows and dangling pointers.
tptacek · 1d ago
There's enormous incentive to construct those exploits. Why don't they exist?
qcnguy · 1d ago
Go programs are normally running on the server side and are often proprietary, so you can't see the bugs exist to exploit them. It's not something like Chrome where someone can spend weeks finding a race and exploiting it to get a bug bounty, with full visibility of the source code and ability to develop an exploit in the lab.
qualeed · 1d ago
This seems to be operating on the premise that everyone knows of every exploit that exists. There's also an enormous incentive to hide working exploits.
tptacek · 1d ago
Any. Not every.
comex · 1d ago
You’re a cryptography person. So you know that most theoretically interesting cryptography vulnerabilities, even the ones that are exploitable in PoCs, are too obscure and/or difficult to get used by actual attackers. Same goes for hardware vulnerabilities. Rowhammer and speculative execution attacks are often shown to be able to corrupt and leak memory, respectively, but AFAIK there are no famous cases of them actually being used to attack someone. Partly because they’re fiddly; partly out of habit. Partly because if you’re in a position to satisfy the requirements for those attacks – you have copies of all the relevant binaries so you know the memory layout of code and data, you have some kind of sandboxed arbitrary code execution to launch the attack from – then you’re often able to find better vulnerabilities elsewhere. And the same is also true for certain types of software vulnerabilities…
Honestly, forget about Go: when was the last time you heard of a modern application backend being exploited through memory corruption, in any language? I know that Google and Meta and the like use a good amount of C++ on the server, as do many smaller companies. That C++ code may skew ‘modern’ and safer, but you could say the same about newly-developed client-side C++ code that’s constantly getting exploited. So where are the server-side attacks? Part of the answer is probably that they exist, but I don’t know about them because they haven’t been disclosed. Unlike client-side attacks, server-side attacks usually target a single entity who has little incentive to publish deep dives into how they were attacked. That especially applies to larger companies, which tend to use more C++. But we do sometimes see those deep dives written anyway, and the vulnerabilities described usually aren’t memory safety related. So I think there is also a gap in actual exploitation. Which probably has a number of causes, but I’d guess they include attackers (1) usually not having ready access to binaries, (2) not having an equivalent to the browser as a powerful launching point for exploits, and (3) not having access to as much memory-unsafe code as on the client side.
This is relevant to Go because of course Go is usually used on the server side. There is some use of Go on the client side, but I can’t think offhand of a single example of it being used in the type of consumer OS or client-side application that typically gets attacked.
Meanwhile, Go is of course much safer than C++. To make exploitation possible in Go, not only do you need a race condition (which are rarely targeted by exploits in any language), you also need a very specific code pattern. I’m not sure exactly how specific. I know how a stereotypical example of an interface pointer/viable mismatch works. But are there other options? I hear that maps are also thread-unsafe in general? I’d need to dig into the implementation to see how likely that is to be exploitable.
Regardless, the potential exists. If memory safety is a “threshold test” as you say, then Go is not memory-safe.
I agree though that the point would best be proven with a PoC of exploiting a real Go program. As someone with experience writing exploits, I think I could probably locate a vulnerability and create an exploit, if I had a few months to work on it. But for now I have employment and my free time is taken up by other things.
amluto · 1d ago
> when was the last time you heard of a modern application backend being exploited through memory corruption, in any language?
It happens all the time, but it’s a bit hard to find because “modern application backend[s]” are usually written in Go or Python or Rust. Even so, you’ll find plenty of exploits based on getting a C or C++ library on the backend to parse a malformed file.
comex · 1d ago
Are these exploits publicly documented?
amluto · 14m ago
Some exploit authors love writing up their work. For example:
Where? Within, as I said, “the type of consumer OS or client-side application that typically gets attacked”. It has to be a component of either a big application or a big OS, or something with comparable scope. Otherwise it would not likely be targeted by real-world memory corruption attacks (that we hear about) no matter the language. At least that’s my impression.
tptacek · 1d ago
I'm sure I could come up with a bunch of examples but the first thing that jumps into my head is the Docker ecosystem.
comex · 12h ago
Yeah, that’s not nearly the level of big I was thinking of. It’s not a browser or WhatsApp or Word.
Admittedly, Go is popular among developers. And there are some public examples of client-side attacks targeting developers and security researchers specifically. Such attacks could hypothetically go after something like Docker. But, searching now, every single example I can find seems to either exploit a non-developer-specific target (browser, iMessage, Acrobat), or else not exploit anything and just rely on convincing people to execute a Trojan (often by sending a codebase that executes the Trojan when you build it).
That bifurcation actually surprises me and I’m not sure what to conclude from it, other than “build systems are insecure by design”. But at any rate, the lack of Go exploits doesn’t say much if we don’t see exploits of developer tools written in C either.
tptacek · 8h ago
We routinely do see those exploits!
ameliaquining · 21h ago
I would say that Go is common in command-line developer tooling, which is sort of client-side albeit a noncentral example of same (since it includes tools for running servers and suchlike), and rare in all other client-side domains that I can think of.
afdbcreid · 1d ago
I'm not sure that's correct.
Yes, this is an enormous effort to construct exploits, but constructing exploits for C/C++ code is much much easier and gives not less, or even more, benefit. Therefore it makes sense the efforts are focused on that.
If/when most C/C++ code in the world will be gone, I assume we'll see more exploits of Go code.
lossolo · 1d ago
I can show you a trivial POC in C/C++ where someone opens a socket and ends up with a buffer overflow or UAF, both cases leading to memory corruption due to sloppy programming, and both easily exploitable for RCE.
Can you show me any reasonable proof of concept (without using unsafe etc.) in Go that leads to similar memory corruption and is exploitable for RCE?
This example hardcodes the payload, but (unless I've badly misunderstood how the exploit works) that's not necessary, it could instead be input from the network (and you wouldn't have to pass that input to any APIs that are marked unsafe). The payload is just hardcoded so that the example could be reproduced on the public Go Playground, which sandboxes the code it runs and so can't accept network input.
Note that what tptacek is asking for is more stringent than this; he wants a proof-of-concept exploitation of a memory safety vulnerability caused by the data-race loopholes in the Go memory model, in a real program that someone is running in production. I do think it's interesting that nobody has demonstrated that yet, but I'm not sure what it tells us about how sure we can be that those vulnerabilities don't exist.
lossolo · 1d ago
Yeah, it looks like CTF like POC, not what I would call reasonable code by any measure:
The tight goroutine loop that flips one variable between two different struct types just to win a race is not something a typical developer writes on purpose.
The trick to "defeat" compiler optimizations by assigning to a dummy variable inside an inline function.
Carefully computing the address difference between two slices to reach out of bounds, then using that to corrupt another slice’s header.
I mean calling mprotect and jumping to shellcode is outright exploit engineering, not business logic and it's not part of the attackers payload.
Chances of exact PoC pattern showing up in the wild by accident is basically zero.
I don’t see any evidence that anyone wrote an RCE exploit for this, but I also don’t see any evidence of anyone even trying to rule it out.
tptacek · 1d ago
What about this particular bug do you think makes it likely to be exploitable? I'm not asking you to write an RCE POC, just to tell a story of the sequence of events involving this bug that results in attacker-controlled code. What does the attacker control here, and how do they use that control to divert execution?
amluto · 1d ago
As a general heuristic, a corrupted data structure in a network server results in RCE. This is common in languages like C and C++.
On first glance, it looks like the bug can (at least) result in the server accessing a slice object where the various fields don’t all come from the same place. So the target server can end up accessing some object out of bounds (or as the wrong type or both), which can easily end up writing some data (possibly attacker controlled) to an inappropriate place. In standard attack, the attacker might try to modify the stack or a function pointer to set up a ROP chain or something similar, which is close enough to arbitrarily code to eventually either corrupt something to directly escalate privileges or to do appropriate syscalls to actually execute code.
tptacek · 1d ago
No, that doesn't work. Lots of (maybe even most) corrupted data structures aren't exploitable (past DOS). Where does the attacker-controlled data come from. What path does it take to get to where the attacker wants it to go. You have to be able to answer those two questions.
amluto · 21h ago
The Internet is full of nice articles of people bragging about their RCE exploits that start with single-byte overruns or seemingly-weak type confusions, etc.
> Where does the attacker-controlled data come from.
The example I gave was an HTTP server. Attackers can shove in as much attacker-controlled data as they want. They can likely do something like a heap by using many requests or many headers. Unless the runtime zeroes freed memory (and frees it immediately, which GC languages like Go often don’t do), then lots of attacker controlled data will stick around. And, for all I know, the slice that gets mixed up in this bug is fully attacker controlled!
In any event, I think this whole line of reasoning is backwards. Developers should assume that a memory safety error is game over unless there is a very strong reason to believe otherwise — assume full RCE, ability to read and write all in-process data, the ability to issue any syscall, and the ability to try to exploit side channels. Maybe very strong mitigations like hardware-assisted CFI will change this, and maybe not.
ameliaquining · 21h ago
I looked at the code, and unless I've misunderstood it, this bug can't corrupt the slice in the sense of allowing accesses outside the designated allocation or anything like that, because the slice variable is only written to once, when the writer is initialized, so there can't be racy accesses to it. The contents of the slice can potentially be corrupted, but that's just arbitrary bytes, so not a memory safety violation.
The line I'm not quite as sure about is https://go.googlesource.com/go/+/refs/tags/go1.13.1/src/bufi.... That assignment is to a variable of interface type, so in theory it could cause memory corruption if multiple goroutines executed it concurrently on the same receiver, which was possible until the bug was fixed. That said, I cannot immediately think of a way to exploit this; you can only write error values corresponding to errors that you can make occur while writing to the socket, and that's a much more constrained set of possible values than the arbitrary bytes that can occur in a buffer. And for that, you only get confusion among the types of those particular errors. It might be possible but it at least looks challenging.
Sharlin · 1d ago
That’s called "moving the goal posts".
A definition of memory safety that permits unsoundness as long as nobody has exploited said unsoundness is not a definition that anyone serious about security is going to accept. Unsoundness is unsoundness, undefined behavior is undefined behavior. The conservative stance is that once execution hits UB, anything can happen.
gf000 · 1d ago
Hide the same program into some dependency of a dependency and you have a nice little security vulnerability in your prod app. It's actually very easy to hide such a vulnerability as an innocent bug.
ameliaquining · 1d ago
If you're stipulating deliberately inserted vulnerabilities then there are much easier ways, e.g., with a plausibly-deniable logic bug in code that calls os/exec or reflect (both of which can execute arbitrary code by design).
gf000 · 22h ago
If you see `exec`, that's an obvious point where you want to pay extra attention.
Compare to an innocent looking map operation, and it's not even in the same league.
ameliaquining · 21h ago
What's the least suspicious-looking code that you think could facilitate remote code execution via data-race memory corruption?
nemothekid · 1d ago
This is no true Scotsman for programming languages.
I could also argue C is memory safe and all the exploits that have been made weren’t real C programs
sophacles · 1d ago
I think your security background is coloring your perception of the term memory safety. Specifically the requirement that the various issues lead to exploitation. These issues can lead to many other issues that are not vulnerability in the security sense, e.g. data corruption, incorrect (but not insecure) behavior, performance issues, and more. I don't think any of those were ever dismissed or excluded from memory safety discussion. Infosec circles tend to evaluate most ideas in the context of (anti)exploitation, and the rest of programming tends to focus on what the cool kids argue (that is they often weigh security concerns higher than other issues as well), so the other problems caused by double-free or buffer overruns (etc) just may not have been given as much weight in your mind.
tptacek · 1d ago
"Memory safety" is a security term, not a PLT term.
Sharlin · 7h ago
First off, in any and every engineering discipline it would be absurd to claim that "safety" only means security against intentional malice.
Second, the burden of proof goes the other way. It’s absurd to claim that UB is safe unless proven otherwise. Unsafety must obviously be the default assumption.
SkiFire13 · 14h ago
That's a statement without source and even if it was widely accepted as true it doesn't imply the fact that something needs to be exploitable to be considered a security issue. We are full of CVEs without a known way to be exploited.
tptacek · 8h ago
Non-falsifiable statement is non-falsifiable.
ralfj · 8h ago
> Another way to reach the same conclusion is to note that this post's argument proves far too much; by the definition used by this author, most other higher-level languages (the author exempts Java, but really only Java) also fail to be memory safe.
This is wrong.
I explicitly exempt Java, OCaml, C#, JavaScript, and WebAssembly. And I implicitly exempt everyone else when I say that Go is the only language I know of that has this problem.
> If you want to claim that a language is memory-unsafe, POC || GTFO.
There's a POC right in the post, demonstrating type confusion due to a torn read of a fat pointer. I think it could have just as easily been an out-of-bounds write via a torn read of a slice. I don't see how you can seriously call this memory safe, even by a conservative definition.
Did you mean POC against a real program? Is that your bar?
tptacek · 1d ago
You need a non-contrived example of a memory-corrupting data race that gives attackers the ability to control memory, through type confusion or a memory lifecycle bug or something like it. You don't have to write the exploit but you have to be able to tell the story of how the exploit would actually work --- "I ran this code and it segfaulted" is not enough. It isn't even enough for C code!
codys · 18h ago
The post is a demonstration that a class of problems: causing Go to treat a integer field as a pointer and access the memory behind that pointer without using any of Go's documented "unsafe.Pointer" (or other documented as unsafe operations).
We're talking about programming languages being memory safe (like fly.io does on it's security page [1]), not about other specific applications.
It may be helpful to think of this as talking about the security of the programming language implementation. We're talking about inputs to that implementation that are considered valid and not using "unsafe" marked bits (though I do note that the Go project itself isn't very clear on if they claim to be memory-safe). Then we want to evaluate whether the programming language implementation fulfills what people think it fulfills; ie: "being a memory safe programming language" by producing programs under some constraints (ie: no unsafe) that are themselves memory-safe.
The example we see in the OP is demonstrating a break in the expectations for the behavior of the programming language implementation if we expected the programming language implementation to produce programs that are memory safe (again under some conditions of not using "unsafe" bits).
In this thread I linked the fly.io security page because it helps us establish that one can talk about _languages_ as being memory safe specifically, which is something it seems you're rejecting as a concept in the parent and other comments.
(In a separate comment about "what do people claim about Go anyhow", I linked the memorysafety.org page, but I did not expect it to help in getting you to the understanding that we can evaluate programming languages as being memory safe or not, where something from the company where someone was a founder seemed more likely to get a person to reconsider the framing of what we're examining)
tptacek · 8h ago
Huh? No, I'm not. Go is a memory-safe programming language, like Java before it, like Python, Ruby, Javascript, and of course Rust.
zozbot234 · 8h ago
So you're saying nobody cares about actual memory safety in concurrent code? Then why did the Swift folks bother to finally make the language memory-safe (just as safe as Rust) for concurrent code? Heck why did the Java folks bother to define their safe concurrency/memory model to begin with? They could have done it the Golang way and not cared about the issue.
tptacek · 8h ago
I don't know why you're inventing things for me to have said.
weinzierl · 1d ago
"What's happening here, as happens so often in other situations, is that a term of art was created to describe something complicated; [..] Later, people uninvolved with the popularization of the term took the term and tried to define it from first principles, arriving at a place different than the term of art."
Happens all the time in math and physics but having centuries of experience with this issue we usually just slap the name of a person on the name of the concept. That is why we have Gaussian Curvature and Riemann Integrals. Maybe we should speak of Jung Memory Safety too.
Thinking about it, the opposite also happens. In the early 19th century "group" had a specific meaning, today it has a much broader meaning with the original meaning preserved under the term "Galois Group".
Or even simpler: For the longest time seconds were defined as fraction of a day and varied in length. Now we have a precise and constant definition and still call them seconds and not ISO seconds.
Mawr · 4h ago
> The fact is that Go doesn't admit memory corruption vulnerabilities, and the way you know that is the fact that there are practically zero exploits for memory corruption vulnerabilities targeting pure Go programs, despite the popularity of the language.
Another way to word it: If "Go is memory unsafe" is such a revelation after its been around for 13 years, it's more likely that such a statement is somehow wrong than that nobody's picked up on such a supposedly impactful safety issue in all this time.
As such, the burden of proof that addresses why nobody's ran into any serious safety issues in the last 13 years is on the OP. It's not enough to show some theoretical program that exhibits the issue, clearly that is not enough to cause real problems.
zozbot234 · 3h ago
There's no "revelation" here, it's always been well known among experts that Go is not fully memory safe for concurrent code, same for previous versions of Swift. OP has simply spelled out the argument clearly and made it easier to understand for average developers.
tptacek · 3h ago
It's made what would be a valid point using misleading terminology and framing that suggests these are security issues, which they simply are not.
"One could easily turn this example into a function that casts an integer to a pointer, and then cause arbitrary memory corruption."
No, one couldn't! One has contrived a program that hardcodes precisely the condition one wants to achieve. In doing so, one hasn't even demonstrated even one of the two predicates for a memory corruption vulnerability (attacker control of the data, and attacker ability to place controlled data somewhere advantageous to the attacker).
What the author is doing is demonstrating correctness advantages of Rust using inappropriate security framing.
zozbot234 · 3h ago
> misleading terminology and framing that suggests these are security issues
Could you quote where exactly OP has misleadingly "suggested" that these concerns lead to security issues in the typical case?
> attacker control of the data, and attacker ability to place controlled data somewhere advantageous to the attacker
Under this definition the Rowhammer problem with hardware DRAM does not qualify as a genuine security concern since it inherently relies on fiddly non-determinism that cannot possibly be "controlled" by any attacker. (The problem with possible torn writes in concurrent Go code is quite similar in spirit; it's understood that an actually observed torn write might only occur rarely.) Needless to say there is a fairly strong case for addressing these problems anyway, as a matter of defence in depth.
> correctness advantages of Rust
Memory safety in OP's sense is not exclusive to Rust. Swift has it. Even Java/C# cannot access arbitrary memory as a result of torn writes. It would be more accurate to say that OP has identified a correctness issue that's apparently exclusive to Go.
tptacek · 2h ago
I quoted directly from the article.
zozbot234 · 2h ago
To use your definition, that quote is clearly making a point about correctness, not necessarily about real-world security.
tptacek · 1h ago
As long as we agree that there isn't a meaningful security implication, we don't need to keep litigating.
lenkite · 1d ago
How does Java "fail" to be memory safe by the definition used by the author ? Please give an example.
elktown · 10h ago
The older I get the more I just see these kinds of threads like I see politics: Exaggerate your "opponents" weaknesses, underplay/ignore its strengths and so on. So if something no matter how disproportionate can be construed to be, or be associate with, a current zeitgeist with a negative sentiment, it's an opportunity to gain ground.
I really don't understand why people get so obsessed with their tools that it turns into a political battleground. It's a means to an end. Not the end itself.
johnnyjeans · 1d ago
This is a good post and I agree with it in full, but I just wanted to point out that (safe) Rust is safer from data races than, say, Haskell due to the properties of an affine type system.
Haskell in general is a much safer than Rust thanks to its more robust type system (which also forms the basis of its metaprogramming facilities), monads being much louder than unsafe blocks, etc. But data races and deadlocks are one of the few things Rust has over it. There are some pure functional languages that are dependently typed like Idris, and thus far safer than Rust, but they're in the minority and I've yet to find anybody using them industrially. Also Fortnite's Verse thing? I don't know how pure that language is though.
chowells · 1d ago
I don't think it's true that Rust is safer, using the terminology from the article. Both languages prevent you from doing things that will result in safety violations unless you start mucking with unsafe internals.
Rust absolutely does make it easier to write high-performance threaded code correctly, though. If your system depends on high amounts of concurrent mutation, Rust definitely makes it easier to write correct code.
On the other hand, a system like STM in Haskell can make it easier to write complex concurrency logic correctly in Haskell than Rust, but it can have very bad performance overhead and needs to be treated with extreme suspicion in performance-sensitive code. It's a huge win for simple expression of complex concurrency, but you have to pay for it somewhere. It can be used in ways where that overhead is acceptable, but you absolutely need to be suspicious in a way that's never a concern in Rust.
empath75 · 1d ago
> Another way to reach the same conclusion is to note that this post's argument proves far too much; by the definition used by this author, most other higher-level languages (the author exempts Java, but really only Java) also fail to be memory safe.
Yes I mean that was the whole reason they invented rust. If there were a bunch of performant memory safe languages already they wouldn't have needed to.
FiloSottile · 1d ago
I have never seen real Go code (i.e. not code written purposefully to be exploitable) that was exploitable due to a data race.
This doesn’t prove a negative, but is probably a good hint that this risk is not something worth prioritizing for Go applications from a security point of view.
Compare this with C/C++ where 60-75% of real world vulnerabilities are memory safety vulnerabilities. Memory safety is definitely a spectrum, and I’d argue there are diminishing returns.
stouset · 1d ago
Maintenance in general is a burden much greater than CVEs. Exploits are bad, certainly, but a bug not being exploitable is still a bug that needs to be fixed.
With maintenance being a "large" integer multiple of initial development, anything that brings that factor down is probably worth it, even if it comes at an incremental cost in getting your thing out the door.
9rx · 1d ago
> but a bug not being exploitable is still a bug that needs to be fixed.
Do you? Not every bug needs to be fixed. I've never see a data race bug in documented behaviour make it past initial development.
I have seen data races in undocumented behaviour in production, but as it isn't documented, your program doesn't have to do that! It doesn't matter if it fails. It wasn't a concern of your program in the first place.
That is still a problem if an attacker uses undocumented behaviour to find an exploit, but when it is benign... Oh well. Who cares?
LtWorf · 1d ago
I have! What do i win?
EE84M3i · 21h ago
Was it open source? Would be interested to know more.
LtWorf · 20h ago
Yeah, reading binary files in go with an mmap library and the whole file is based on offsets to point to other sections of the file. Damaged file or programming error and segfault.
ncruces · 12h ago
How is that a data race? Also, you're using unsafe.
The post is about data races in safe Go leading to crashes or exploits, because things like "eface" (the tuple used to implement interfaces) and slices (again a tuple) are multi-word and thus impossible to update atomically.
This is not an issue in Java because such things always box behind a pointer.
debugnik · 9h ago
Note that C#'s Memory<T> (a slice) isn't boxed either, but it stays memory-safe under tearing: the resulting slice might have unintended bounds, but it will either be in-bounds or will throw an exception.
This of course has some overhead, which is why you usually turn it into the cheaper, data race free Span<T>. Go could have the same safety and fix some of the overhead with compiler optimizations, they just don't want to take the trade-off.
crawshaw · 1d ago
Memory safety is a big deal because many of the CVEs against C programs are memory safety bugs. Thread safety is not a major source of CVEs against Go programs.
It’s a nice theoretical argument but doesn’t hold up in practice.
nine_k · 1d ago
A typical memory safety issue in a C program is likely to generate an RCE. A thread-safety issue that leads to a segfault can likely only lead to a DoS attack, unpleasant but much less dangerous. A race condition can theoretically lead to more powerful attacks, but triggering it should be much harder.
SkiFire13 · 15h ago
A thread-safety issue does not always lead to a segfault. Here it did because the address written was 42, but if you somehow manage to obtain the address of some valid value then you could read from that instead, and not cause an immediate segfault.
I agree with the sentiment that data races are generally harder to exploit, but it _is possible_ to do.
okanat · 19h ago
It depends on what threads can do. Threads share memory with other threads and you can corrupt the data structure to force the other thread to do an unsafe / invalid operation.
It can be as simple as changing the size of a vector from one thread while the other one accesses it. When executed sequentiality, the operations are safe. With concurrency all bets are off. Even with Go. Hence the argument in TFA.
crawshaw · 17h ago
All bets aren’t off, we empirically measure the safety of software based on exploits. C memory handling is most of its exploits.
Show me the exploits based on Go parallelism. This issue has been discussed publicly for 10 years yet the exploits have not appeared. That’s why it's a nice theoretical argument but does not hold up in practice.
stouset · 1d ago
A CVE is worse, but a threading bug resulting in corrupted data or a crash is still a bug that needs someone to triage, understand, and fix.
crawshaw · 1d ago
But it's not why I stopped writing C programs. It's just a bug and I create and fix a dozen bugs every day. Security is the only argument for memory safety that moves mountains.
kllrnohj · 7h ago
This isn't arguing about exploit risks of the language but simply whether or not it meets the definition of memory safe. Go doesn't satisfy the definition, so it's not memory safe. It's quite black & white here.
Nice strawman though
qcnguy · 1d ago
The point being made is sound, but I can never escape the feeling that most concurrency discussion in programming language theory is ignoring the elephant in the room. The concurrency bugs that matter in most apps are all happening inside the database due to lack of proper locking, transactions or transactional isolation. PL theory ignores this and so things like Rust's approach to race freedom ends up not mattering much outside of places like kernels. A Rust app can avoid use of unsafe entirely and still be riddled with race conditions because all the data that matters is in an RDBMS and someone forgot a FOR UPDATE in their SELECT clause.
layer8 · 10h ago
What’s worse, even if you use proper transactions for everything, it’s hard to reason about visibility and data races when performing SQL across tables, or multiple dependent SQL statements within a transaction.
norir · 1d ago
The sad thing is that most languages with threads have a default of global variables and unrestricted shared memory access. This is the source of the vast majority of data corruption and races. Processes are generally a better concurrency model than threads, but they are unfortunately too heavyweight for many use cases. If we defaulted to message passing all required data to each thread (either by always copying or tracking ownership to elide unnecessary copying), most of these kinds of problems would go away.
In the meantime, we thankfully have agency and are free to choose not to use global variables and shared memory even if the platform offers them to us.
kibwen · 1d ago
> The sad thing is that most languages with threads have a default of global variables and unrestricted shared memory access. This is the source of the vast majority of data corruption and races. Processes are generally a better concurrency model than threads
Modern languages have the option of representing thread-safety in the type system, e.g. what Rust does, where working with threads is a dream (especially when you get to use structured concurrency via thread::scope).
People tend to forget that Rust's original goal was not "let's make a memory-safe systems language", it was "let's make a thread-safe systems language", and memory safety just came along for the ride.
tialaramex · 1d ago
Originally Rust is something altogether different. Graydon has written about that extensively. Graydon wanted tail calls, reflection, more "natural" arithmetic with Python style automatic big numbers, decimal for financial work and so on.
The Rust we have from 1.0 onwards is not what Graydon wanted at all. Would Graydon's language have been broadly popular? Probably not, we'll never know.
kibwen · 1d ago
Even in pre-1.0 Rust, concurrency was a primary goal; there's a reason that Graydon listed Newsqueak, Alef, Limbo, and Erlang in the long list of influences for proto-Rust.
fmajid · 22h ago
And yet it ignored the primary lesson of Erlang: no shared memory access whatsoever, which is what makes it so robust.
littlestymaar · 22h ago
Because the other teams members (IIRC brson and pcwalton) wanted Rust to be as performant as C++, which means you must have a way to have shared memory.
rurban · 11h ago
Which was the ultimate failure of Rust, because as Pony benchmarks have shown, you get safety and speed by proper security and architecture. Rust just survived by lying about the its safeties.
What kills performance are not memory copies, but locks. Parallel nonblocking IO and a non POSIX stdlib will bring you far away from C++ or Rust performance.
kllrnohj · 7h ago
> What kills performance are not memory copies, but locks.
I'm pretty sure if every thread executing an LLM model had to have its own copy that that would murder performance more than any lock does, and it won't even be close.
It's cheaper to copy than to lock when the data is small, but that does not scale and it also ignores things like reader/writer locks where the data is primarily read-only, at least during the concurrent stage. Or where the work can be safely chunked up such that writes don't ever overlap which is very common in graphics
littlestymaar · 5h ago
> It's cheaper to copy than to lock when the data is small
Exactly this.
kobebrookskC3 · 4h ago
> Rust just survived by lying about the its safeties.
strong claim. care to back it up?
littlestymaar · 5h ago
Oh yeah, the “ultimate failure of Rust”, and tell me how industrially successful Pony has been compared to Rust?
(Don't get me wrong, I liked the idea behind Pony for backend code, it's much saner than Go for the same target space. But it failed to capture that market, because Go was already there. And it was never a competitor to Rust because this model is only viable for web back end tasks, not for general computing).
rurban · 5h ago
It isn't it a shame that the industry always falls for the liars?
The pony model was also better for compute tasks, not just IO. Because it provided safe concurrency, 10x faster than go.
littlestymaar · 4h ago
No, the pony model isn't better for compute tasks…
Think for instance about how you'd do efficient matrix multiplication of two matrices with a million row and column, in Pony, versus how it works in languages with shared memory. You'd spend a gigantic amount of time copying data for no good reason…
rurban · 3h ago
Of course pony can do shared memory. How about clevering up?
nine_k · 1d ago
While at it, I suppose it's straightforward to implement arbitrary-precision integers and decimals in today's Rust; there are several crates for that. There's also a `tailcall` crate that apparently implements TCO [1].
Oh, I do know you can have arbitrary precision. I'm the author of realistic, which isn't "just" arbitrary precision it's an approximation of the computable reals as well, which is sometimes just enough more power than you'd hardly notice you have arbitrary precision too.
Message passing can easily lead to more logical errors (such as race conditions and/or deadlocks) than sharing memory directly with properly synchronized access. It's not a silver bullet.
umpalumpaaa · 1d ago
100%.
Some more modern languages - eg. Swift – have "sendable" value types that are inherently thread safe. In my experience some developers tend to equate "sendable" / thread safe data structures with a silver bullet. But you still have to think about what you do in a broader sense… You still have to assemble your thread safe data structures in a way that makes sense, you have to identify what "transactions" you have in your mental model and you still have to think about data consistency.
advisedwang · 1d ago
Wow that's a really big gotcha in go!
To be fair though, go has a big emphasis on using its communication primitives instead of directly sharing memory between goroutines [1].
Even if you use channels to send things between goroutines, go makes it very hard to do so safely because it doesn't have the idea of sendable types, ownership, read-only references, and so on.
For example, is the following program safe, or does it race?
func processData(lines <-chan []byte) {
for line := range lines {
fmt.Printf("processing line: %v\n", line)
}
}
func main() {
lines := make(chan []byte)
go processData(lines)
var buf bytes.Buffer
for range 3 {
buf.WriteString("mock data, assume this got read into the buffer from a file or something")
lines <- buf.Bytes()
buf.Reset()
}
}
The answer is of course that it's a data race. Why?
Because `buf.Bytes()` returns the underlying memory, and then `Reset` lets you re-use the same backing memory, and so "processData" and "main" are both writing to the same data at the same time.
In rust, this would not compile because it is two mutable references to the same data, you'd either have to send ownership across the channel, or send a copy.
In go, it's confusing. If you use `bytes.Buffer.ReadBytes("\n")` you get a copy back, so you can send it. Same for `bytes.Buffer.String()`.
But if you use `bytes.Buffer.Bytes()` you get something you can't pass across a channel safely, unless you also never use that bytes.Buffer again.
Channels in rust solve this problem because rust understands "sending" and ownership. Go does not have those things, and so they just give you a new tool to shoot yourself in the foot that is slower than mutexes, and based on my experience with new gophers, also more difficult to use correctly.
Mawr · 3h ago
> In go, it's confusing. If you use `bytes.Buffer.ReadBytes("\n")` you get a copy back, so you can send it. Same for `bytes.Buffer.String()`.
>
> But if you use `bytes.Buffer.Bytes()`
If you're experienced, it's pretty obvious that a `bytes.Buffer` will simply return its underlying storage if you call `.Bytes()` on it, but will have to allocate and return a new object if you call say `.String()` on it.
> unless you also never use that bytes.Buffer again.
I'm afraid that's concurrency 101. It's exactly the same in Go as in any language before it, you must make sure to define object lifetimes once you start passing them around in concurrent fashion.
Channels are nice in that they model certain common concurrency patterns really well - pipelines of processing. You don't have to annotate everything with mutexes and you get backpressure for free.
But they are not supposed to be the final solution to all things concurrency and they certainly aren't supposed to make data races impossible.
> Even if you use channels to send things between goroutines, go makes it very hard to do so safely
Really? Because it seems really easy to me. The consumer of the channel needs some data to operate on? Ok, is it only for reading? Then send a copy. For writing too? No problem, send a reference and never touch that reference on our side of the fence again until the consumer is done executing.
Seems about as hard to understand to me as the reason why my friend is upset when I ate the cake I gave to him as a gift. I gave it to him and subsequently treated it as my own!
Such issues only arise if you try to apply concurrency to a problem willy-nilly, without rethinking your data model to fit into a concurrent context.
Now, would the Rust approach be better here? Sure, but not if that means using Rust ;) Rust's fancy concurrency guarantees come with the whole package that is Rust, which as a language is usually wildly inappropriate for the problem at hand. But if I could opt into Rust-like protections for specific Go data structures, that'd be great.
"2. Shared buffer causes race/data reuse
You're writing to buf, getting buf.Bytes(), and sending it to the channel. But buf.Bytes() returns a slice backed by the same memory, which you then Reset(). This causes line in processData to read the reset or reused buffer."
I mean, you're basically passing a pointer to another thread to processData() and then promptly trying to do stuff with the same pointer.
nemothekid · 19h ago
If you are familiar with the internals of bytes/buffer you would catch this. But it would be great for the compiler to catch this instead of a human reviewer. In Rust, this code wouldn't even compile. And I'd argue even in C++, this mistake would be clearer to see in just the code.
TheDong · 17h ago
> I mean, you're basically passing a pointer to another thread to processData()
And yet, "bytes.Buffer.ReadBytes(delim)" returns a copy of the underlying data which would be safe in this context.
The type system does not make it obvious when this is safe or not, and passing pointers you own across channels is fine and common.
> That code would never pass a human pull request review
Yes, that was a simplified example that a human or AI could spot.
When you actually see this in the wild, it's not a minimal example, it's a small bug in hundreds of lines of code.
I've seen this often enough that it obviously does actually happen, and does pass human code review.
Mawr · 4h ago
Wow who knew concurrency is hard!
This isn't anything special, if you want to start dealing with concurrency you're going to have to know about race conditions and such. There is no language that can ever address that because your program will always be interacting with the outside world.
zozbot234 · 1d ago
Real-world golang programs share memory all the time, because the "share by communicating" pattern leads to pervasive logical problems, i.e. "safe" race conditions and "safe" deadlocks.
jrockway · 1d ago
I am not sure sync.Mutex fixes either of these problems. Press C-\ on a random Go server that's been up for a while and you'll probably find 3000 goroutines stuck on a Lock() call that's never going to return. At least you can time out channel operations:
select {
case <-ctx.Done():
return context.Cause(ctx)
case msg := <-ch:
...
}
aatd86 · 5h ago
Why does it segfault? Because you have not used a sufficiently clever value for the integer that wouldn't when used as an address?
Just wondering.
Realistically that would be quite rare since it is obvious that this is unprotected shared mutable access.
But interesting that such a conversion without unsafe may happen.
If it segfaults all the time though then we still have memory safety I guess.
The article is interesting but I wish it would try to provide ideas for solutions then.
codys · 18h ago
Curiously, Go itself is unclear about its memory safety on go.dev. It has a few references to memory safety in the FAQ (https://go.dev/doc/faq#Do_Go_programs_link_with_Cpp_programs, https://go.dev/doc/faq#unions) implying that Go is memory safe, but never defines what those FAQ questions mean with their statements about "memory safety". There is a 2012 presentation by Rob Pike (https://go.dev/talks/2012/splash.slide#49) where it is stated that go is "Not purely memory safe", seeming to disagree with the more recent FAQ. What is meant by "purely memory safe" is also not defined. The Go documentation for the race detector talks about whether operations are "safe" when mutexes aren't added, but doesn't clarify what "safe" actually means (https://go.dev/doc/articles/race_detector#Unprotected_global...). The git record is similarly unclear.
In contrast to the go project itself, external users of Go frequently make strong claims about Go's memory safety. fly.io calls Go a "memory-safe programming language" in their security documentation (https://fly.io/docs/security/security-at-fly-io/#application...). They don't indicate what a "memory-safe programming language" is. The owners of "memorysafety.org" also list Go as a memory safe language (https://www.memorysafety.org/docs/memory-safety/). This later link doesn't have a concrete definition of the meaning of memory safety, but is kind enough to provide a non-exaustive list of example issues one of which ("Out of Bounds Reads and Writes") is shown by the article from this post to be something not given to us by Go, indicating memorysafety.org may wish to update their list.
It seems like at the very least Go and others could make it more clear what they mean by memory safety, and the existence of this kind of error in Go indicates that they likely should avoid calling Go memory safe without qualification.
ralfj · 7h ago
> Curiously, Go itself is unclear about its memory safety on go.dev.
Yeah... I was actually surprised by that when I did the research for the article. I had to go to Wikipedia to find a reference for "Go is considered memory-safe".
Maybe they didn't think much about it, or maybe they enjoy the ambiguity. IMO it'd be more honest to just clearly state this. I don't mind Go making different trade-offs than my favorite language, but I do mind them not being upfront about the consequences of their choices.
phire · 18h ago
The definition kind of changed.
At the time Go was created, it met one common definition of "memory safety", which was essentially "have a garbage collector". And compared to c/c++, it is much safer.
ralfj · 7h ago
> it met one common definition of "memory safety", which was essentially "have a garbage collector"
This is the first time I hear that being suggested as ever having been the definition of memory safety. Do you have a source for this?
Given that except for Go every single language gets this right (to my knowledge), I am kind of doubtful that this is a consequence of the term changing its meaning.
codys · 18h ago
That seems contrasted by Rob Pike's statement in 2012 in the linked presentation being one of the places where it's called "not purely memory safe". That would have been early, and Go is not called memory safe then. It seems like calling Go memory safe is a more recent thing rather than a historical thing.
phire · 17h ago
Keep in mind that the 2012 presentations dates to 10 months after Rust's first release, and its version of "Memory Safety" was collecting quite a bit of attention. I'd argue the definition was already changing by this point. It's also possible that Go was already discovering their version of "Memory Safety" just wasn't safe enough.
If you go back to the original 2009 announcement talk, "Memory Safety" is listed as an explicit goal, with no carveouts:
"Safety is critical. It's critical that the language be type-safe and that it be memory-safe."
"It is important that a program not be able to derive a bad address and just use it; That a program that compiles is type-safe and memory-safe. That is a critical part of making robust software, and that's just fundamental."
> Rust's first release, and its version of "Memory Safety" was collecting quite a bit of attention
Note that this was not Rust's first stable release, but it's first public release. At the time it was still changing a lot and still had "garbage collected" types.
phire · 13h ago
Yeah, it was the 0.1 release. I can't remember exactly when Rust entered the general "programming language discourse" on hackernews and /r/programming, but it was somewhere around here. I'm sure the people behind Go would have known about it by this point in time.
And while rust did have optional "garbage collected pointers", it's important to point out that it is not a garbage collected language. The ownership system and borrow checker were very much front-and-centre for the 0.1 release, it was what everyone was talking about.
Actually, my memory is that while the language had syntax to declare garbage collected pointers, it wasn't actually hooked up to a proper garbage collector. It was always more of a "we are reserving the syntax and we will hook it up when needed", and it turns out the ownership system was powerful enough that it was never needed.
SkiFire13 · 9h ago
> Actually, my memory is that while the language had syntax to declare garbage collected pointers, it wasn't actually hooked up to a proper garbage collector. It was always more of a "we are reserving the syntax and we will hook it up when needed", and it turns out the ownership system was powerful enough that it was never needed.
AFAIK it was just an `Rc`/`Arc` with the possibility of upgrading it to an actual GC in the future.
codys · 16h ago
That's a very good point on the timing. Thanks for adding that extra info.
alkonaut · 5h ago
And here I thought the type system and error handling were the two biggest Go warts. You’re now telling me their memory model is basically ”YOLO”?
swiftcoder · 14h ago
I wish we had picked a better name than "thread safety". This is really more like "concurrency safety", since it applies even in the absence of hardware threads.
layer8 · 10h ago
Threads aren’t hardware, they are OS. Multihreading != multiprocessing.
zozbot234 · 10h ago
Hardware threads are a thing.
layer8 · 6h ago
Other than in the sense of SMT (Hyper-Threading)? I don't think so. Threads are a software concept.
One can distinguish between native (OS) threads and green (language-runtime) threads which may use a different context-switching mechanism. But that's more of a spectrum in terms of thread-safety; similar to how running multiple threads on a single CPU core without SMT, single CPU core with SMT, multiple CPU cores, with different possible CPU cache coherency guarantees, create a spectrum of possible thread-safety issues.
zozbot234 · 3h ago
Runtime-switched tasks cannot lead to memory unsafety unless multiple OS threads are involved, because that's the only case where torn writes are possible. And a typical configuration of Go will not be running multiple OS threads unless multiple hardware threads (aka "logical cores", "virtual processors" etc.) are available.
dataflow · 16h ago
Am I missing something or is that bold claim obviously wrong on its face? This seems like a Go deficiency (lack of atomicity for it pointers), not some sort of law about programming languages.
Can you violate memory safety in C# without unsafe{} blocks (or GCHandle/Marshal/etc.)? (No.)
Can you write thread-unsafe code in C# without using unsafe{} blocks etc.? (Yes, just make your integers race.)
Doesn't that contradict the claim that you can't have memory safety without thread safety?
corysama · 20h ago
This is why I’m excited about https://www.hylo-lang.org/ as a new, statically-compiled language with all the safeties!
Thaxll · 1d ago
Go is memory safe by the most common definition, does not matter if you have segfault in some scenario.
How many exploits or security issues have there been related to data race on dual word values? I work with Go for the last 10 years and I never heard of such issues. Not a single time.
zozbot234 · 1d ago
The most common definition of memory safe is literally "cannot segfault" (unless invoking some explicitly unsafe operation - which is not the case here unless you think the "go" keyword should be unsafe).
SkiFire13 · 1d ago
TBH segfaults are not necessarily a sign of memory unsafety, but _unexpected_ segfaults are.
For some examples, Rust (although this is not specific to it) uses stack guard pages to detect stack overflows by _forcing_ a segfault (as opposed to reading/writing arbitrary memory after the usual stack). Some JVMs also expect and handle segfaults when dereferencing null pointers, to avoid always paying the cost for checking them.
jlouis · 1d ago
The definition has to do with certain classes of spatial and temporal memory errors. Ie., the ability to access memory outside the bounds of an array would be an example of a spatial memory error. Use-after-free would be an example of a temporal one.
The violation occurs if the program keeps running after having violated a memory safety property. If the program terminates, then it can still be memory safe in the definition.
Segfaults has nothing to do with the properties. There's some languages or some contexts in which segfaults is part of the discussion, but in general, the theory doesn't care about segfaults.
sapiogram · 21h ago
> The violation occurs if the program keeps running after having violated a memory safety property. If the program terminates, then it can still be memory safe in the definition.
I don't know what you're trying to say here. C would also be memory-safe if the program just simply stopped after violating memory safety, but it doesn't necessarily do that, so it's not memory safe. And neither is Go.
uecker · 15h ago
The point is that a segfault is not an indication for memory unsafety. It is the opposite: The OS stops some unsafe access. The problem with C implementations is that it often comes to late and the segfault does not stop a prior unsafe read or write. But this is also an implementation property, you can implement C in a memory safe way as many have shown. Rust has, unfortunately, changed the narrative so that people now believe memory safety is a property of the language, when it is one of the implementation. (there are, of course, language properties that make it harder to implement C in a memory safe way without sacrificing performance and/or breaking ABI).
ralfj · 3h ago
(EDIT: removed the first part since I realized you were replying to some comment further up, not my example.)
> Rust has, unfortunately, changed the narrative so that people now believe memory safety is a property of the language, when it is one of the implementation.
I am not sure I agree with that (the concept of memory-safe languages looong predates Rust), but you can just define a memory-safe language as one where all conforming implementations are memory-safe -- making it a feature of the language itself, not just a feature of a particular implementation.
uecker · 2h ago
I am not really saying the term is not useful, the idea that it is the only legitimate way of memory safety is what I am questioning.
SkiFire13 · 14h ago
The segfault seen here is not a property of the language implementation, it's just a consequence of the address chosen by the attacker: 42. If you replicated this code in C you would get the same result, and if you used an address pointing to mapped memory in Go then the program would continue executing like in similar exploits in C.
The only reason this isn't a more critical issue is because data races are hard to exploit and there aren't lot of concurrent Go programs/system libraries that accept lot of attacker controlled inputs.
uecker · 9h ago
Whether you can a segfault if you access an out-of-bounds address or not is part of the language implementation. An implementation that guarantees a segfault for out-of-bounds accesses is memory safe.
zozbot234 · 9h ago
You can't really guarantee that all out-of-bounds accesses will segfault, because memory protection mechanisms are not that granular. (And actual memory segmentation, that did have the required granularity, has fallen out of use - though CHERI is an attempt to revive it.) That's why a segfault is treated as something to be avoided altogether, not as a reliable error mechanism.
What you can say though (and the point I made upthread) is that if a language manages to provably never segfault, then it must have some sort of true language-enforced safety because the difference between segfaulting or not is really just a matter of granularity.
uecker · 9h ago
How granular the memory protection mechanism is is part of the implementation.
zozbot234 · 9h ago
It's part of the broader system, not the language implementation. And in practice, systems that achieve this are not in common use.
uecker · 7h ago
You are using a narrower definition than me. The language implementation builds on the functionality of the a larger system. An implementation can utilize the functionality of the overall system and close the loopholes. For example, using sanitizer you can turn out-of-bounds accesses to arrays into traps. This is not a segmentation fault but SIGILL, but it also builds on the trapping mechanism to achieve bounds safety (if you limit yourself to arrays).
zozbot234 · 1d ago
Both spatial and temporal memory unsafety can lead to segfaults, because that's how memory protection is intended to work in the first place. I don't believe it's feasible to write a language that manages to provably never trip a memory protection fault in your typical real-world system, yet still fails to be memory safe, at least in some loose sense. For example, such a language could never be made to execute arbitrary code, because arbitrary code can just trip a segfault. You'd be left with the sort of type confusion logical error that happens all the time anyway in all sorts of "weakly typed" languages - that's not what "memory safety" is about.
dylnuge · 1d ago
I've never heard anyone define memory safety that way. You can segfault by overflowing stack space and hitting the guard page or dereferencing a null pointer. Those are possible in languages that don't even expose their underlying pointers like Java. You can make Python segfault if you set the recursion limit too high. Meanwhile a memory access bug or exploit that does not result in a segfault would still be a memory safety issue.
Memory safe languages make it harder to segfault but that's a consequence, not the primary goal. Segfaults are just another memory protection. If memory bugs only ever resulted in segfaults the instant constraints are violated, the hardware protections would be "good enough" and we wouldn't care the same way about language design.
Thaxll · 1d ago
I don't know the NSA with their white house paper about memory safe language mentioned Go, maybe you should tell that there are wrong.
You did not even read the link the parent comment provided and are continuing with the same flawed argument.
Yoric · 1d ago
Segfaults are just the simplest way of exposing a memory issue. It's quite easy to use a race condition to reproduce a state that isn't supposed to be reachable, and that's much worse than a segfault, because it means memory corruption.
Now the big question, as you mention, is "can it be exploited?" My assumption is that it can, but that there are much lower-hanging fruits. But it's just an assumption, and I don't even know how to check it.
munificent · 1d ago
I agree with the author's claim that you need thread safety for memory safety.
But I don't agree with:
> I will argue that this distinction isn’t all that useful, and that the actual property we want our programs to have is absence of Undefined Behavior.
There is plenty of undefined behavior that can't lead to violating memory safety. For example, in many languages, argument evaluation order is undefined. If you have some code like:
foo(print(1), print(2));
In some languages, it's undefined as to whether "1" is printed before "2" or vice versa. But there's no way to violate memory safety with this.
I think the only term the author needs here is "memory safety", and they correctly observe that if the language has threading, then you need a memory model that ensures that threads can't break your memory safety.
Go lacks that. It seems to be a rare problem in practice, but if you want guarantees, Go doesn't give you them. In return, I guess it gives you slightly faster execution speed for writes that it allows to potentially be torn.
ralfj · 7h ago
> There is plenty of undefined behavior that can't lead to violating memory safety. For example, in many languages, argument evaluation order is undefined. If you have some code like:
You are mixing up non-determinism and UB. Sadly that's a common misunderstanding.
The evaluation order is _unspecified_, not undefined behaviour.
gpderetta · 1d ago
Interestingly, at least in C++, this was changed in the recent past. It used to be that evaluation of arguments was not sequenced at all and if any evaluation touched the same variable, and at least one was a write, it was UB.
It was changed as part of the C++11 memory model and now, as you said, there is a sequenced-before order, it is just unspecified which one it is.
I don't know much about C, but I believe it was similarly changed in C11.
tialaramex · 1d ago
Sure, prior to the C++ 11 memory model there just isn't a memory ordering model in C++ and all programs in either C or C++ which would need ordering for correctness did not have any defined behaviour in the language standard.
This is very amusing because that means in terms of the language standard Windows and Linux, which both significantly pre-date C++ 11 and thus its memory model, were technically relying on Undefined Behaviour. Of course, as operating systems they're already off piste because they're full of raw assembly and so on.
Linux has its own ordering model as a result, pre-dating the C++ 11 model. Linus is writing software for multi-processor computers more than a decade before the C++ 11 model so obviously he can't wait around for that.
[Edit: Corrected Linux -> Linux when talking about the man]
gpderetta · 1d ago
It is not so much that windows and linux were relying on UB, but that these platforms, with their compilers, provided guarantees beyond the standard. e.g. GCC not only aims for C/C++ standard compliance, but also POSIX.
Of course these guarantees were often not fully written down nor necessarily self consistent (but then again, neither is the current standard).
gliptic · 1d ago
Yes, but that's just a subset of expressions where unspecified sequencing applied. For instance, the example with two `print()` as parameters would have a sequence point (in pre-C++11 terminology) separating any reads/writes inside the `print` due to the function calls. It would never be UB even though the order in which the prints are called is still unspecified.
gpderetta · 1d ago
IIRC the point was that there was no sequence point between argument evaluation, so for example f(++i, ++i) was UB. Or maybe it was only for builtin operators?
Cppreference is not authoritative[1], but seems to support my recollection. In fact it states that the f(++i, ++i) was UB till C++17.
`f(++i, ++i)` is/was indeed UB, but the example in munificent's comment was `foo(print(1), print(2))` which as far as I know is not even if both `print` calls read/write the same memory.
gpderetta · 1d ago
(5) in the paragraph I mentioned earlier seems to prevent interleaving of function calls, which admittedly would make the language hard to use. So I think you are right.
zozbot234 · 1d ago
That's "unspecified" not "undefined". "Undefined behavior" literally means "anything goes", so any program that invokes it is broken by definition.
bigstrat2003 · 1d ago
That is not true, that is a very specific definition of UB which C developers (among others) favor. That doesn't mean that another language can't say "this is undefined behavior" without all the baggage that accompanies the term in C.
zozbot234 · 1d ago
It's literally how the term "UB" is defined, and understood by experts. Why would anyone want to say "undefined" when they really mean "unspecified"? That's just confusing.
bigstrat2003 · 1d ago
No, it's how one very specific community of experts understands it. It is not some kind of universal law of definition that it must mean that always and everywhere. As far as what is confusing, that is a matter of perspective. I think it is confusing (to put it mildly) that the C community has chosen to use "undefined behavior" to mean "it must never happen, and anything goes if it does". That is extremely counterintuitive, and only makes sense to those who live and breathe that world. So if the standard is to be "avoiding confusion", then we better change the definition used by the C community ASAP.
ameliaquining · 1d ago
I agree that the term "undefined behavior", when used as in C/C++/Rust/Swift/.NET, isn't very good at communicating to non-experts what's at stake, not least because it doesn't sound scary enough (the security community remains indebted to whoever coined the term "nasal demons"). That said, is there a specific other community of practice where there's a shared understanding that the term "undefined behavior" means something different?
uecker · 1d ago
It is also not what the C community has chosen. It is what was imposed on us by certain optimizing compilers that used the interpretation that gave them maximum freedom to excel in benchmarks, and it was then endorsed by C++. The C definition is that "undefined behavior" can have arbitrary concrete behavior, not that a compiler can assume it does not happen. (that form semantic people prefer the former because it makes their life easier did not help)
gliptic · 1d ago
The author is using the term in the way that everyone else understands it. They are not aware of your unusual definition.
bakugo · 1d ago
"Undefined behavior" is not a meaningless made up term that you can redefine at will.
The word "undefined" has a clear meaning: there is no behavior defined at all for what a given piece of code will do, meaning it can literally do anything. If the language spec defines the possible behaviors you can expect (even if the behavior can vary between implementations), then by definition it's not undefined.
bigstrat2003 · 1d ago
> "Undefined behavior" is not a meaningless made up term that you can redefine at will.
Sure, I agree with that.
> The word "undefined" has a clear meaning: there is no behavior defined at all for what a given piece of code will do...
That is true, but...
> ...meaning it can literally do anything.
This is not at all true! That is a different (but closely related) matter, which is "what is to be done about undefined behavior". Which is certainly something one has to take a stance on when working to a language spec that has undefined behavior, but that does not mean that "undefined" automatically means your preferred interpretation of how to handle undefined behavior.
zozbot234 · 23h ago
The original question is how UB is defined, not about the preferred way of dealing with it in a practical sense. And the definition of UB is behavior for which the language definition imposes no requirements, and explicitly leaves open the possibility of ignoring the situation altogether with unpredictable results.
joaohaas · 1d ago
Your example does not classify as 'undefined behavior'. Something is 'undefined behavior' if it is specified in the language spec, and in such case yes, the language is capable of doing anything including violating memory safety.
nromiun · 1d ago
I bet not even 5% of all programs are multi-threaded, or even concurrent.
Memory safety is a much bigger problem.
pizlonator · 1d ago
False.
Java got this right. Fil-C gets it right, too. So, there is memory safety without thread safety. And it’s really not that hard.
Memory safety is a separate property unless your language chooses to gate it on thread safety. Go (and some other languages) have such a gate. Not all memory safe languages have such a gate.
glowcoil · 1d ago
I would recommend reading beyond the title of a post before leaving replies like this, as your comment is thoroughly addressed in the text of the article:
> At this point you might be wondering, isn’t this a problem in many languages? Doesn’t Java also allow data races? And yes, Java does allow data races, but the Java developers spent a lot of effort to ensure that even programs with data races remain entirely well-defined. They even developed the first industrially deployed concurrency memory model for this purpose, many years before the C++11 memory model. The result of all of this work is that in a concurrent Java program, you might see unexpected outdated values for certain variables, such as a null pointer where you expected the reference to be properly initialized, but you will never be able to actually break the language and dereference an invalid dangling pointer and segfault at address 0x2a. In that sense, all Java programs are thread-safe.
And:
> Java programmers will sometimes use the terms “thread safe” and “memory safe” differently than C++ or Rust programmers would. From a Rust perspective, Java programs are memory- and thread-safe by construction. Java programmers take that so much for granted that they use the same term to refer to stronger properties, such as not having “unintended” data races or not having null pointer exceptions. However, such bugs cannot cause segfaults from invalid pointer uses, so these kinds of issues are qualitatively very different from the memory safety violation in my Go example. For the purpose of this blog post, I am using the low-level Rust and C++ meaning of these terms.
Java is in fact thread-safe in the sense of the term used in the article, unlike Go, so it is not a counterexample to the article's point at all.
pizlonator · 23h ago
> I would recommend reading beyond the title of a post before leaving replies like this, as your comment is thoroughly addressed in the text of the article:
The title is wrong. That's important.
> Java is in fact thread-safe in the sense of the term used in the article
The article's notion of thread safety is wrong. Java is not thread safe by construction, but it is memory safe.
ralfj · 7h ago
Java also sometimes uses "memory safe" to refer to programs that don't have null pointer exceptions. So in that sense, Java isn't memory safe by construction either.
These terms are used slightly differently by different communities, which is why I discuss this point in the article. But you seem adamant that you have the sole authority for defining these terms so :shrug:
pizlonator · 7h ago
When those US government articles about how we should switch to memory safe languages come out, they refer to Java as a “memory safe language”.
They also count data race freedom as part of memory safety, which I think is wrong (and contradicts their inclusion of Java and even Go in the list of memory safe languages).
So no, I’m not an authority. I’m just following the general trend of how the term is used.
And ive never heard “memory safe” used in relation to not having null pointer exceptions. That’s a new one and sounds nonsensical, frankly
dwattttt · 22h ago
If a language is "memory safe", by some definition we expect safety from memory faults (for example, not accessing memory incorrectly).
If a language is "memory safe" but not "thread safe", is the result "the language is free from 'memory faults', unless threads are involved"?
Or to put it another way; when used however the term of art is intended, "memory safety" is meant to provide some guarantees about not triggering certain erroneous conditions. "not thread safe" seems to mean that those same erroneous conditions can be triggered by threads, which seems to amount to '"memory safety" does not guarantee the absence of erroneous memory conditions'.
pizlonator · 22h ago
> If a language is "memory safe" but not "thread safe", is the result "the language is free from 'memory faults', unless threads are involved"?
Yes.
If a language is memory safe but not thread safe, then you can race, but the outcome of those races won't be memory corruption or the violation of the language's type system. It will lead to weird stuff, however - just a different kind of weirdness than breaking out of the language's sandbox
dwattttt · 19h ago
> If a language is memory safe but not thread safe, then you can race, but the outcome of those races won't be memory corruption or the violation of the language's type system.
By these definitions, doesn't that mean go is neither memory or thread safe? It looks like concurrent modification can result in memory corruption, e.g. the attempted access 0x42 example in the article
pizlonator · 17h ago
> By these definitions, doesn't that mean go is neither memory or thread safe?
Yes, with the caveat that you can't treat "memory safe" as a binary condition.
The strictest notion of memory safety is what I call GIMSO: "Garbage In, Memory Safety Out". I.e. there does not exist any sequence of bytes you could feed to the compiler that would result in a memory-unsafe outcome at runtime. Java aims for this. Fil-C does too. JavaScript also does.
But there are languages that I think it's fair to consider to be memory safe that offer escape hatches that violate GIMSO. Rust with `unsafe` is an example. C# with `unsafe` is another. Java if you include `sun.misc.Unsafe` (arguably it's not part of the language).
So I think if a language is memory safe, not thread safe, and the memory safety is gated on thread safety, then it's kinda fair to make statements like, "it's memory safe", if you have fine print somewhere that says "but the memory safety does not hold under the following kinds of races".
All of that said, I'd rather we just said that "memory safety" means what I call "GIMSO". But the ship has sailed. Lots of languages are called "memory safe" to mean something like, "you can get memory safety in this language if you obey certain idioms" - and in Rust that means "don't use unsafe" while in Go that means "don't race in certain ways".
SkiFire13 · 10h ago
In my opinion this is missing a very important different between the two approaches: using `unsafe`/`sun.misc.Unsafe` in Rust/C#/Java is a very deliberate choice which presence can easily be checked syntactically, meanwhile data races in Go are most often unintended and you can't easily check for their _guaranteed_ absence. Otherwise C/C++ are also "GIMSO" with the caveat "don't UB"!
pizlonator · 6h ago
GIMSO is defined as memory safety without caveats. The only way to get it (currently) in C/C++ is to compile with Fil-C.
You have a good point otherwise, but Go is considered memory safe anyway. And it probably makes sense that it is, since the chances of exploitation due to memory safety issues caused by races in Go are infinitesimal. It’s not at all fair to compare to the exploited-all-the-time issues of C/C++ (when you make the mistake of compiling with something other than Fil-C)
dwattttt · 22h ago
I guess to also elaborate the point; it's also entirely correct to say "Rust is guaranteed to be memory safe unless 'unsafe' is involved".
pizlonator · 22h ago
Yeah and Rust is guaranteed to be thread safe unless 'unsafe' is involved, I think
jillesvangurp · 9h ago
It's not that black and white and the solution isn't necessarily pick language X and you'll be fine. It never is that simple.
Basically, functional languages make it easier to write code that is safe. But they aren't necessarily the fastest or the easiest to deal with. Erlang and related languages are a good example. And they are popular for good reasons.
Java got quite a few things right but it took a while for it to mature. Modern day Java is quite a different beast than the first versions of Java. The Thread class, API, and the language have quite a few things in there that aren't necessarily that great of an idea. E.g. the synchronized keyword might bite you if you are trying to use the new green threads implementation (you'll get some nice deadlocks if you block the one thread you have that does everything). The modern java.concurrent package is implemented mostly without it.
Of course people that know their history might remember that green threads are actually not that new. Java did not actually support real threads until v1.1. Version 1.0 only had green threads. Those went out of fashion for about two decades and then came back with recent versions. And now it does both. Which is dangerous if you are a bit fuzzy on the difference. It's like putting spoilers on your fiesta. Using green threads because they are "faster" is a good sign that you might need to educate yourself and shut up.
On the JVM, if you want to do concurrent and parallel stuff, Scala and Kotlin might be better options. All the right primitives are there in the JVM of course. And Java definitely gives you access to all it. But it also has three decades of API cruft and a conservative attitude about keeping backwards compatible with all of that. And not all of it was necessarily that all that great. I'm a big fan of Kotlin's co-routine support that is rooted in a lot of experience with that. But that's subjective of course. And Scala-ists will probably insist that Scala has even better things. And that's before we bring up things like Clojure.
Go provides a good balance between ease of use / simplicity and safety. But it has quite a few well documented blind spots as well. I'm not that big of a fan but I appreciate it for what it is. It's actually a nice choice for people that aren't well versed in this topic and it naturally nudges people in a direction where things probably will be fine. Rust is a lot less forgiving and using it will make you a great engineer because your code won't even compile until you properly get it and do it right. But it won't necessarily be easy (humbled by experience here).
With languages the popular "if you have a hammer everything looks like a nail" thing is very real. And stepping out of your comfort zone and realizing that other tools are available and might be better suited to what you are trying to do is a good skill to have.
IMHO python is actually undervalued. It was kind of shit at all of this for a long time. But they are making a lot of progress modernizing the language and platform and are addressing its traditional weaknesses. Better interpreting and jit performance, removing the GIL, async support that isn't half bad, etc. We might wake up one day and find it doing a lot of stuff that we'd traditionally use JVM/GO/Rust for a few years down the line. Acknowledging weaknesses and addressing those is what I'm calling out here as a very positive thing. Oddly, I think there are a lot of python people that are a bit conflicted about progress like this. I see the same with a lot of old school Java people. You get that with any language that survives that long.
Note how I did not mention C/C++ here so far. There's a lot of it out there. But if you care about safety, you should probably not go near it. I don't care how disciplined you are. Your C/C++ code has bugs. Any insistence that it doesn't just means you haven't found them yet. Possibly because you are being sloppy looking for them. Does it even have tests? There are whole classes of bugs that we can prevent with modern languages and practices. It's kind of negligent and irresponsible not to. There are attempts to make C++ better of course.
zozbot234 · 9h ago
> IMHO python is actually undervalued. It was kind of shit at all of this for a long time. But they are making a lot of progress modernizing the language and platform and are addressing its traditional weaknesses. Better interpreting and jit performance, removing the GIL, async support that isn't half bad, etc.
The issue with Python isn't just the GIL and lack of support for concurrency. It uses dynamic types (i.e. variant types) for everything. That's way too slow, it means every single variable access must go through a dispatch step. About the only thing Python has going for it is the easy FFI with C-like languages.
ngrilly · 7h ago
That’s why I’m quite excited about Cinder, Meta’s CPython fork, that lets the programmer opt in “strict modules” and “static Python”, enabling many optimizations.
pizlonator · 5h ago
What does any of this have to do with memory safety?
danbruc · 1d ago
Nope. You can have programs without undefined behavior and still not have thread safety. In .NET, for example, writes to variables that are wider then the machine width or not aligned properly, are not guaranteed to be atomic. So if you assign some value to an Int128 variable, it will not be updated atomically - how could it, that is just beyond the capabilities of the processor - and therefore a different thread can observe a state where only half of the variable has been updated. No undefined behavior here but also sharing this variable between threads is not thread safe. And having the language synchronize all such writes - just in case some other thread might want tot look at it - is a performance disaster. And disallowing anything that might be a potential thread safety issue will give you a pretty limited language.
tialaramex · 1d ago
> disallowing anything that might be a potential thread safety issue will give you a pretty limited language.
Safe Rust doesn't seem that limited to me.
I don't think any of the C# work I do wouldn't be possible in Rust, if we disregard the fact that the rest of the team don't know Rust.
Most of the programs you eliminate when you have these "onerous" requirements like memory safety are nonsense, they either sometimes didn't work or had weird bugs that would be difficult to understand and fix - sometimes they also had scary security implications like remote code execution. We're better off without them IMNSHO.
kllrnohj · 1d ago
Critically to the authors point that type of data race does not result in UB and does not break the language and thus does not create any memory safety issues. Ergo, it's a memory safe language.
Go (and previously Swift) fails at this. There data races can result in UB and thus break memory safety
ameliaquining · 1d ago
See the article's comments on Java, which is "thread safe" in the sense of preventing undefined behavior but not in the sense of preventing data-race-related logic bugs. .NET is precisely analogous in this respect.
tialaramex · 1d ago
I can buy that claim for the .NET CLR but I've never seen it nailed down properly the way Java did which gives me pause.
I worry about the Win95-era "Microsoft Pragmatism" at work and a concrete example which comes to mind is nullability. In the nice modern software I often work on I can say some function takes a string and in that program C# will tell me that's not allowed to be null, it has to be an actual string - a significant engineering benefit. But, the CLR does not enforce such rules, so that function may still receive a null instead e.g. if called by some ten year old VB.NET code which has no idea about "nullability" and so just fills out a null for that parameter anyway.
Of course the CLR memory model might really be set in stone and 100% proof against such problems, but I haven't seen anything to reassure me as I did for Java and I fear that if it were convenient for Windows to not quite do that work they would say eh, good enough.
So, the answer is that I've read that and I wasn't as reassured as I'd like. ECMA definitely isn't enough as it acknowledges. The platforms which exist today are fine. And future platforms? Well we're told it will be difficult to change these assumptions. Yeah, it would be difficult.
For a comparison, the x86 has what that document calls TSO, a very strict "free" ordering (in fact you pay all the time, but, you can't opt out so in that sense it's free to get this ordering on Intel) so 1990s C++ written for Windows just assumes volatile means you get memory ordering -- even though that's not what that means. If you compile brand new code for x86 on Microsoft's compilers today you get the exact same promise, but if you target their ARM platforms you don't get that because it would be expensive so, too bad.
actionfromafar · 1d ago
It depends on what it says?
kibwen · 1d ago
The statement "there is no memory safety without thread safety" does not suggest that memory safety is sufficient to provide thread safety. Instead, it's just saying that if you want thread safety, then memory safety is a requirement.
minitech · 1d ago
> Instead, it's just saying that if you want thread safety, then memory safety is a requirement.
It's saying the opposite – that if you want memory safety, thread safety is a requirement – and Java and C# refute it.
zozbot234 · 1d ago
> Java and C# refute it.
No, they don't. They're using a different meaning for "thread safety" that's more useful in context since they do ensure data race safety - which is the only kind of thread safety OP is talking about. By guaranteeing data race safety as a language property, Java and C# are proving OP's point, not refuting it.
kibwen · 1d ago
> It's saying the opposite
Indeed, you're correct, I interpreted the implications in reverse.
goodpoint · 10h ago
> safety is not binary, it is a spectrum, and on that spectrum Go is much closer to a typical safe language than to C
That's a too low bar to clear to call it safe.
singpolyma3 · 19h ago
Honestly what I mostly want is to not have memory leaks. Which somehow stopped being a focus at some point
tialaramex · 9h ago
The "good" news is that Bjarne Stroustrup is right there with you, Bjarne sees eliminating all memory leaks as a high priority for C++ and one of his main goals.
The bad news ought to be obvious, this "goal" is not achievable, it's a fantasy that somehow we should be able to see the future, divine that some value stored won't be needed in the future and thus we don't need to store it. Goals like "We shouldn't store things we can't even refer to" are already solved in languages used today, so a goal to "not have memory leaks" refers only to that unachievable fantasy.
astrange · 15h ago
This is harder than it looks as soon as you start counting abandoned memory (stuff that's still referenced but not actually used.)
singpolyma3 · 9h ago
Well if it's still referenced it's still used from my PoV
astrange · 1h ago
If it's never read again then it's not used.
Wowfunhappy · 19h ago
Because we have so much memory no one cares if it leaks. <_<
loeg · 1d ago
Are we still have semantic fights about what exactly memory safety means? Why?
sapiogram · 20h ago
Because people think Golang is immune to bugs that it's not immune from.
shadowgovt · 1d ago
This is, in my mind, the trickiest issue with Rust right now as a language project, to wit:
- The above is true
- If I'm writing something using a systems language, it's because I care about performance details that would include things like "I want to spawn and curate threads."
- Relative to the borrow-checker, the Rust thread lifecycle static typing is much more complicated. I think it is because it's reflecting some real complexity in the underlying problem domain, but the problem stands that the description of resource allocation across threads can get very hairy very fast.
pornel · 18h ago
I don't know what you're referring to. Rust's threads are OS threads. There's no magic runtime there.
The same memory corruption gotchas caused by threads exist, regardless of whether there is a borrow checker or not.
Rust makes it easier to work with non-trivial multi-threaded code thanks to giving robust guarantees at compile time, even across 3rd party dependencies, even if dynamic callbacks are used.
Appeasing the borrow checker is much easier than dealing with heisenbugs. Type system compile-time errors are a thing you can immediately see and fix before problems happen.
OTOH some racing use-after-free or memory corruption can be a massive pain to debug, especially when it may not be possible to produce in a debugger due to timing, or hard to catch when it happens when the corruption "only" mangles the data instead of crashing the program.
shadowgovt · 18h ago
It's not the runtime; it's how the borrow-checker interoperates with threads.
This is an aesthetics argument more than anything else, but I don't think the type theory around threads and memory safety in Rust is as "cooked" as single-thread borrow checking. The type assertions necessary around threads just get verbose and weird. I expect with more time (and maybe a new paradigm after we've all had more time to use Rust) this is a solvable problem, but I personally shy away from Rust for multi-threaded applications because I don't want to please the type-checker.
pornel · 7h ago
You know that Rust supports scoped threads? For the borrow checker, they behave like same-thread closures.
Borrow checking is orthogonal to threads.
You may be referring to the difficulty satisfying the 'static liftime (i.e. temporary references are not allowed when spawning a thread that may live for an arbitrarily long time).
If you just spawn an independent thread, there's no guarantee that your code will reach join(), so there's no guarantee that references won't be dangling. The scoped threads API catches panics and ensures the thread will always finish before references given to it expire.
shadowgovt · 5h ago
I'll have to look more closely at scoped threads. What I'm referring to is that compared to the relatively simple syntax of declaring scopes for arguments to functions and return values to functions, the syntax when threads get involved is (to take an example from the Rust Book, Chapter 21):
But really, that first type signature is not very complex. It can get far, far, far worse. That’s just what happens when you encode things in types.
(It reads as “spawn is a function that accepts a closure that returns a type T. It returns a JoinHandle that also wraps a T. Both the closure and the T must be able to be sent to another thread and have a static lifetime.”)
kazinator · 1d ago
This is false as a generality.
A memory safe, managed language doesn't become unsafe just because you have a race condition in a program.
Like, say, reading and writing several related shared variables without a mutex.
Say that the language ensures that the reads and writes themselves of these word-sized variables are safe without any lock, and that memory operations and reclamation of memory are thread safe: there are no low-level pointers (or else only as an escape hatch that the program isn't using).
The rest is your bug; the variable values coming out of sync with each other, not maintaining the invariant among their values.
It could be the case that a thread-unsafe program breaks a managed run-time, but not an unvarnished truth.
A managed run-time could be built on the assumption that the program will not create two or more threads such that those threads will invoke concurrent operations on the same objects. E.g. a managed run time that needs a global interpreter lock, but which is missing.
munificent · 1d ago
> A memory safe, managed language doesn't become unsafe just because you have a race condition in a program.
The author's point is that Go is not a memory safe language according to that distinction.
There are values that are a single "atomic" write in the language semantics (interface references, slices) that are implemented with multiple non-atomic writes in the compiler/runtime. The result is that you can observe a torn write and break the language's semantics.
ralfj · 3h ago
> The rest is your bug; the variable values coming out of sync with each other, not maintaining the invariant among their values.
If the language and its runtime let me break their invariant, then that's their bug, not mine. This is the fundamental promise of type-safe languages: you can't accidentally break the language abstraction.
> It could be the case that a thread-unsafe program breaks a managed run-time, but not an unvarnished truth.
I demonstrated that the Go runtime is such a case, and I think that should be considered a memory safety violation. Not sure which part of that you disagree with...
gpderetta · 1d ago
race condition != data race. Specifically, in go, a race condition can cause application level bugs but won't affect, directly, the runtime consistency; on the other hand a data race on a slice can cause torn writes and segfaults in the best case, and fandango on core in the worst case.
dodobirdlord · 1d ago
If the variables are word-sized, sure. But what if they are larger? Now a race condition between one thread writing and another thread reading or writing a variable is a memory safety issue.
zozbot234 · 1d ago
> Now a race condition between one thread writing and another thread reading or writing a variable is a memory safety issue.
No it isn't, because the torn write cannot have arbitrary effects that potentially break the program. It only becomes such if you rely on such a variable to establish an invariant about memory that's broken if a torn write occurs (such as by encoding a ptr+len in it), which is just silly. Don't do that!
gpderetta · 1d ago
> which is just silly. Don't do that!
tell that to the Go runtime, which relies on slices always being valid and not being able to create invalid ones.
kazinator · 1d ago
Don't have such things, if you know what's good for you, or else don't have threads.
qcnguy · 1d ago
The author knows that. His point is that Go doesn't work that way because it uses greater-than-word-sized values that can suffer torn writes leading to segfaults in some cases.
pharrington · 17h ago
Your fantasy language doesn't have a race condition.
Mawr · 1d ago
There is no house safety without nuclear warhead detonation safety.
There is no pedestrian safety without mandatory helmet laws.
There is no car safety without driving a tank.
kiitos · 23h ago
> To see what I mean by this, consider this program written in Go, which according to Wikipedia is memory-safe:
The Wikipedia definition of memory safety is not the Go definition of memory safety, and in Go programs it is the Go definition of memory safety that matters.
The program in the article is obviously racy according to the Go language spec and memory model. So this is all very much tilting at windmills.
ralfj · 7h ago
Can you point me to the Go definition of memory safety? I searched all over their website, and couldn't find any.
(But also, it'd be kind of silly for every language to make up their own definition of memory safety. Then even C is memory safe, they just have to define it the right way. ;)
Swift has (had?) the same issue and I had to write a program to illustrate that Swift is (was?) perfectly happy to segfault under shared access to data structures.
Go has never been memory-safe (in the Rust and Java sense) and it's wild to me that it got branded as such.
This is just two groups of people talking past each other.
It's not as if Go programmers are unaware of the distinction you're talking about. It's literally the premise of the language; it's the basis for "share by communicating, don't communicate by sharing". Obviously, that didn't work out, and modern Go does a lot of sharing and needs a lot of synchronization. But: everybody understands that.
> the issue here is that the "Rust and Java sense" of memory safety is not the actual meaning of the term
So what is the actual meaning? Is it simply "there are no cases of actual exploited bugs in the wild"?
Because in another comment you wrote:
> a term of art was created to describe something complicated; in this case, "memory safety", to describe the property of programming languages that don't admit to memory corruption vulnerabilities, such as stack and heap overflows, use-after-frees, and type confusions. Later, people uninvolved with the popularization of the term took the term and tried to define it from first principles, arriving at a place different than the term of art.
But type confusion is exactly what has been demonstrated in the post's example. So what kind of memory safety does Go actually provide, in the term of art sense?
If you were engaged to do a software security assessment for an established firm that used Go (or Python, or any of the other mainstream languages that do shared-memory concurrency and don't have Rust's type system), and you said "this code is memory-unsafe", showing them this example, you would not be taken seriously.
If people want to make PLT arguments about Rust's correctness advantages, I will step out of the way and let them do that. But this article makes a security claim, and that claim is in the practical sense false.
It is trivial to change this example into an arbitrary int2ptr cast.
> Go (or Python, or any of the other mainstream languages that do shared-memory concurrency and don't have Rust's type system),
As the article discusses, only Go has this issue. Python and Java and JavaScript and so on are all memory-safe. Maybe you are mixing up "language has data races" and "data races can cause the language itself to be broken"?
> If people want to make PLT arguments about Rust's correctness advantages, I will step out of the way and let them do that. But this article makes a security claim, and that claim is in the practical sense false.
This article makes a claim about the term "memory safety". You are making the claim that that's a security term. I admit I am not familiar with the full history of the term "memory safety", but I do know that "type safety" has been used in PLT for many decades, so it's not like all "safety" terms are somehow in the security domain.
I am curious what your definition of "memory safety" is such that Go satisfies the definition. Wikipedia defines it as
> Memory safety is the state of being protected from various software bugs and security vulnerabilities when dealing with memory access, such as buffer overflows and dangling pointers.
My example shows that Go does not enforce memory safety according to that definition -- and not through some sort of oversight or accident, but by design. Out-of-bounds reads and writes are possible in Go. The example might be contrived, but the entire point of memory safety guarantees is that it doesn't matter how contrived the code is.
I'm completely fine with Go making that choice, but I am not fine with Go then claiming to be memory safe in the same sense that Java or Rust are, when it is demonstrably not the case.
While you're wondering why I keep claiming Go is a memory-safe language, you can also go ask the ISRG, which says the same thing I am at (checks notes) https://www.memorysafety.org/.
And yet Go violates the definition they give -- it doesn't prevent out-of-bounds accesses. (And just to be sure we're talking about the same thing, I'm specifically talking about Go here. All the other languages on their list are actually memory safe, as far as I know.)
> you have to demonstrate a plausible scenario in realistic code where an attacker controls both the value and the address it's written to.
So your definition of memory safety includes some notion of "plausible" and "realistic"? Neither https://www.memorysafety.org/docs/memory-safety/ nor Wikipedia have such a qualification in their definition. It would help if you could just spell out your definition in full, rather than having us guess.
This is a strawman argument, you're arguing semantics here. You're a smart person, so you know exactly what he means. The perception created by your article is that people shouldn't use Go because it's not memory-safe. But the average developer hearing "not memory-safe" thinks of C/C++ level issues, with RCEs everywhere.
Unless you can show a realistic way this could be exploited for RCE in actual programs, you're just making noise. Further down the thread, you admit yourself that you're in a PLT research bubble and it shows.
Seriously, why are we bashing a researcher for being academic? This makes no fucking sense. Nobody claimed anywhere that people should stop using Go.
Yes, semantics — what do things mean, exactly? — is the subject of the discussion here and is actually quite important in general.
Uh, where exactly am I saying or implying that? I am, in fact, saying that Go is much closer to memory-safe languages than to C, safety-wise.
But I am arguing that the term "memory safe" should only be used for languages that actually went through the effort of thinking this problem through to the end and plugging all the holes through which memory safety violates can sneak in. Go is 99% there, but it's falling slightly short of the goal. I think that's a useful distinction, and I am disappointed that it is regularly swept under the rug, which is why I wrote this blog post. You are free to disagree, I never expected to convince everyone. But I think I gave some people some new food for thought, and that's all I can hope for.
I am honestly curious here. I am a PLT researcher so I am in a bubble where people use the term consistently with how I use it. You are the first person I meet (for some notion of "meet" ;) that uses the term differently. But without external sources it's hard to judge how wide-spread your definition (that you still haven't spelled out...) is.
U.S. and International Partners Issue Recommendations to Secure Software Products Through Memory Safety
They recommand Go among other language in their paper.
https://media.defense.gov/2023/Dec/06/2003352724/-1/-1/0/THE...
I think we actually agree on all of the factual points here, we just don't agree on how languages should be categorized/labeled according to their guarantees in both a theoretical and a practical sense, and that's largely a subjective matter anyway. So, happy to agree to disagree here.
> Memory safety is a property of some programming languages that prevents programmers from introducing certain types of bugs related to how memory is used. Since memory safety bugs are often security issues, memory safe languages are more secure than languages that are not memory safe.
That is the definition they give. Since Go does not "prevent programmers from introducing certain types of bugs related to how memory is used." it does not fall under this definition. They can list go as memory safe, but then either they disagree with their own definition or made the mistake of adding Go to that list. Memory safety is not a spectrum. You are either memory safe or unsafe. The spectrum is in the unsafety. Go is obviously less unsafe than C for example.
Denial of service can absolutely be a security issue, as can any correctness bug if it leads to unintended behavior or corrupted data.
I’ll take tptacek’s word over most FAANG type on such topics if we’re doing appeals to authority. The guy is very practical, unlike the Rust community which is incredibly focused on theoretical correctness instead of real-world experiences.
Sure, those mainframes from the 80's weren't bullet proof either. But you first had to get to them. And even if the data traveled in plain text on leased lines (point-to-point but not actually point-to-point (that would require a lot of digging), no multiplexing) you had to physically move to the country where they were located to eavesdrop on them, and injecting data into the stream was a much harder problem.
It's not even always the case that corrupted data structures (or even pointers) in C code are exploitable. You need attacker control of data and where it goes in memory. It's far less often the case in Python or Go --- in fact, it's basically never the case. As evidence for that claim: the zero memory corruption RCEs in all of shipping Go code, of which there is a lot.
Everybody does not understand that otherwise there would be zero of these issues in shipping code.
This is the problem with the C++ crowd hoping to save their language. Maybe they'll finally figure out some --disallow-all-ub-and-be-memory-safe-and-thread-safe flag but at the moment it's still insanely trivial to make a mistake and return a reference to some value on the stack or any number of other issues.
The answer can not be "just write flawless code and you'll never have these issues" but at the moment that's all C++, and Go, from this article has.
- in other languages, it’s understood that perhaps the language is vulnerable to certain errors and one should attempt to mitigate them. But more importantly, those errors are one class of bug and bugs can happen. Set up infra to detect and recover.
- in Rust the code must be safe, must be written in a certain way, must be proven correct to the largest extent possible at compile time.
This leads to the very serious, solemn attitude typical of Rust developers. But the reality is that most people just don’t care that much about a particular type of error as opposed to other errors.
Opposite really. I like rust because I can be care free and have fun.
> in Rust the code must be safe, must be written in a certain way, must be proven correct to the largest extent possible at compile time.
Only for the Safe Rust subset. Rust has the 'unsafe' keyword that shows exactly where the former case does apply. (And even then, only for possible memory unsoundness. Rust does not attempt to fix all possible errors.)
I had to convince Go people that you can segfault with Go. Or you mean the language designers with using everybody?
It's been in usage for PLT for at least twenty years[1]. You are at least two decades late to the party.
[1]https://llvm.org/pubs/2003-05-05-LCTES03-CodeSafety.pdfDoesn’t NASA have an incredibly strict, specific set of standards for writing safety critical C that helps with writing programs that can be formalized?
C and C++ always defaults to minimum amount of safety for maximum allowance of the compiler interpretation. The priority of the language designers of them is keeping existing terrible code running as long as possible first, letting compilers interpret the source code as freely as possible second.
That's why many military and aerospace code actually uses much safer and significantly more formally verifiable Ada.
If you assume the entire lang, yes. If you use a large subset, no. Furthermore, compiler interpretation might actually be sane! There are more compilers out there than GCC, Clang or MSVC. I suspect many assumptions are being made on this claim.
But a memory-safe program != memory safe language. Memory safe language helps you maintain memory-safety by reducing the chances to cause memory unsafety.
Hence the focus on fearless concurrency or other small-scale idioms like match in an attempt to present Rust as an overall better language compared to other safe languages like Go, which is proving to be a solid competitor and is much easier to learn and understand.
~130k LoC Swift app was converted from 5 -> 6 for us in about 3 days.
Swift is strating to look more like old java beans. (if you are old enough to remember this, most swift developers are too young). Doing some of the same mistakes.
Anways https://forums.swift.org/t/has-swifts-concurrency-model-gone... Common problems all devs face: https://www.massicotte.org/problematic-patterns
Anyways, they are trying to reinvent 'safe concurrency' while almost throwing the baby with the bathwater, and making swift even more complex and harder to get into.
There is ways to go. For simple apps, the new concurrency is easy to adopt. But for anything that is less than trivial, it becomes a lot of work, to the point that it might not make it worth it.
A much bigger problem I think are the way concurrency settings are provided via flags. It's no longer possible to know what a piece of code does without knowing the exact build settings. For example, depending on Xcode project flags, a snippet may always run on the main loop, or not at all or on a dedicated actor all together.
A piece of code in a library (SPM) can build just fine in one project but fail to build in another project due to concurrency settings. The amount of overhead makes this very much unusable in a production / high pressure environment.
Rust on the other hand solves that. There is code you can't write easily in Rust, but just yesterday I took a rust iteration, changed 'iter()' to 'par_iter()', and given it compiled I had high confidence it was going to work (which it did).
Inconsistent data is pretty bad, but it's not as bad as memory corruption.
In C# For example, if a structure is over CPU arch Word size (i.e. 32 or 64 bits) then you could have a torn read if it's being written. However object refs themselves are always word size so you'll never have a torn pointer read on those.
However, in either case there is still a need in multithreaded environments to remember the CPU's memory ordering rules and put proper fences (or, to be safe, locks, since memory barrier rules are different between ARM and x86 for example).
But that second bit is a fairly hard problem to solve for without having the right type of modelling around your compiler.
In practice, Java programs tend to pick up on data races very quickly because they mutate some collection and the collections framework has safety checks for this.
Is it? It will depend on the code, but my gut feeling is that you typically would get a few (if not lot of) unnoticed non-segfaulting issues before you get the segfaulting one that tells you straight in your face that you have a problem.
Rust avoids all this entirely, by using its type system.
https://pkg.go.dev/sync#Map
Not synchronizing writes on most data structure does not create a SEGFAULT, you have to be in a very specific condition to create one, those conditions are extremely rares and un-usual ( from the programmer perspective).
In OP blog to triggers one he's doing one of those condition in an infinite loop.
https://research.swtch.com/gorace
Or put another way what is the likelihood that a go program is memory unsafe?
Not going down the same road is the only reason it didn't end up on the pile of obscure languages nobody uses.
Case in point, Limbo and Oberon-2, the languages that influenced its design, and authors were involved with.
This is a killer combination for any team looking to write code for auto-scalable microservices, to run for example on Kubernetes. Java is not great in this niche because of its slow startup time, relatively large memory overhead, and the need for warm-up before code actually starts executing fast (so scaling up and down has a very large cost for Java services). .NET has similar problems, and also a huge container size. Python is far too slow, and not typed. TypeScript is single threaded, and still has a pretty hefty runtime. OCaml doesn't have any large org behind it, is quite obscure syntax, and was still single-threaded at the time Kubernetes started. Haskell has similar issues, and is also large and slow starting. Rust, C++, C all require manual memory management.
So, it's no surprise that Go was used for Kubernetes services themselves, and it's no surprise that people designing for Kubernetes mostly chose to write their new stuff in Go. Go the language, with its antiquated design, is actually quite secondary to all of that. But Go's runtime is completely unmatched in this space.
> .NET has similar problems
s/has/had/
https://blog.washi.dev/posts/tinysharp/
The issue is that some people still fighting against the concepts ML family languages (primarily SML) introduced. Go implemented go routines and channels from CSP (https://en.wikipedia.org/wiki/Communicating_sequential_proce...) but dragged a lot on influence from C (understandable) into the language.
I think Rust opted for the best combinations (some CSP, a lot of ML and a bit of C++).
The article you quote is a toy example - if you write a C# or F# web API server, you'll see that it takes up way more space than a Go one with similar functionality (and has way higher memory overhead as well). A Go API web server is maybe 10MB on disk, with no dependencies (that is, you can run it perfectly in a container that is defined as `FROM scratch; COPY my-go-exec /my-go-exec `). The equivalent Java or .NET container is somewhere around 2-400MB at the minimum.
As for the syntax and constructs, I don't care so much. If OCaml or SML had comparable support and a comparable ecosystem to Go, I'd bet plenty of people would have chosen them instead.
Go is not particularly fast. People often see that Java gets faster as it runs and thinks, oh, it must be slow at the start then. But when you compare like with like, Go ends up being stuck at the bottom of the curve that Java then sharply climbs. The difference in GC quality is big, or at least, used to be? The only place where you really notice the difference is command line tools, and Java has GraalVM for that.
There are some kubernetes services that scale up and down. And even for those that don't normally, if they have some kind of failure, the difference between taking a millisecond to get back up and taking a second can actually matter for a web host.
> Go is not particularly fast. People often see that Java gets faster as it runs and thinks, oh, it must be slow at the start then. But when you compare like with like, Go ends up being stuck at the bottom of the curve that Java then sharply climbs.
Go starts up much faster than Java. And Go code runs measurably faster than interpreted Java code, even though it's slower than the JITed code you'll eventually have if your JVM runs long enpigh. But un-JITed Java code is very slow, more comparable to Python than JITed Java or with Go . This has nothing to do with the GC - where I do agree Go is mediocre at best.
At one time, Go maps were not thread-safe. Was that fixed?
No comments yet
sync.Map was added, but isn't intended to be a general purpose map.
——
The Map type is specialized. Most code should use a plain Go map instead, with separate locking or coordination, for better type safety and to make it easier to maintain other invariants along with the map content.
The Map type is optimized for two common use cases: (1) when the entry for a given key is only ever written once but read many times, as in caches that only grow, or (2) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys. In these two cases, use of a Map may significantly reduce lock contention compared to a Go map paired with a separate Mutex or RWMutex.
Dart ended up on the pile of languages nobody uses. And Carbon? What's Carbon? Exactly!
> Case in point, Limbo and Oberon-2, the languages that influenced its design
Agreed. Limbo and Oberon-2, as primitive as they may look now, had the kitchen sinks of their time. Why wouldn't they have ended up on the pile of languages nobody uses?
Dart was a victim of internal politics between the Chrome team, Dart team, AdWords moving away from GWT wanting AngularDart (see Angular documentary), and the Web in general.
Had Chrome team kept pushing DartVM, it might have been quite different story.
Carbon, good example of failure to actually know what the team purposes are. It is officially a research project for Google themselves, where the team is the first to advise using Rust or another MSL.
One just needs to actually spend like a couple of minutes on their wiki, but I guess that is asking too much on modern times.
Limbo and Oberon-2 were definitely not kitchen sinks of their time, their failure was that neither Bell Labs in 1996, nor ETHZ in 1992, were that relevant for the programming language community in the industry.
Trouble with that line of thinking is that Google never pushed Go either. It didn't even bother to use it internally (outside from the occasional side project here and there). Google paid some salaries. I'll give you that. But it has paid salaries for a lot of different languages. That is not some kind of secret sauce.
> It is officially a research project for Google themselves
It's not just a research project. It is officially "not ready for use", but its roadmap has a clear "ready for use" plan in the coming months. Rust was also "not ready for use" when it hit the streets, it officially being a Mozilla research project, but every second discussion on HN was about it and what is to come. And that was without Google backing. If what you say is true, why isn't Carbon being shouted from every rooftop right now?
I know you're struggling to grasp at straws here, but let's just be honest for a moment: If it hasn't caught attention already, it isn't going to. Just another language to add to the pile.
It is officially "not ready to use", it isn't a strawman as people keep complaining about nothing.
I will grant you that Carbon is still in its infancy, but when Rust was in the same youthful stage we never heard an end to all the people playing with it. You, even if not tried it yourself, definitely knew about it.
You've made up a fun idea, but reality doesn't support it. Google has not shown its weight carries anything. They have really struggled to get any for-profit business units off the ground since they gained the weight, never mind their hobbies! If anything, Google is detrimental to a project.
Lets also not forget Rob Pike famous quote regarding simple minds, as target audience.
As for Go, Kubernetes made it unavoidable, it is like UNIX for C, Web for JavaScript, and so forth.
While you fairly point out that many fall into Python because they learned about it in school and never bother to look beyond, Go has had no such equivalent. For you to choose it, you have to actively seek it out, where you are going to also see all the other programming languages you could also choose.
> As for Go, Kubernetes made it unavoidable, it is like UNIX for C, Web for JavaScript, and so forth.
UNIX's programming interface is a set of C functions. You are right that C is the path of least resistance to use it.
The web's programming interface is Javascript. You are right that Javascript is the path of least resistance to use it.
Kubernetes' programming interface is a "REST API" – or your program running inside a container, if you want to look at it from the other direction. In what way is Go the path of least resistance to use it?
If only Google put their weight into a watch, maybe you'd have one?
Oh wait. They did! Google can't successfully turn their weight into much of anything. Go's success, if we can call it that, clearly happened in spite of Google.
Like who? Outside of Go itself, which is really more of a community project — albeit with the chief maintainers still on Google's payroll, almost nothing at Google is written in Go. In fact, Pike once gave a talk reflecting on why it didn't succeed in that space, and noted that it was the "Python and Ruby programmers" who actually ended up adopting it.
Google makes money selling services (i.e. Google Cloud) that run Kubernetes, Docker, etc. If it weren't for that, it is unlikely that Google would even be continuing to maintain it at this point. It was an interesting experiment, perhaps, but ultimately a failure within Google. As before, it was the Python and (probably most especially) Ruby communities that ended up leaning into it.
Which isn't surprising in hindsight. Go offered those who were using Python and Ruby a language that was in the same kind of vein, while solving many of the pain points they were experiencing with Python and Ruby (awful deployment strategies, terrible concurrency stories, trouble with performance, etc.) These developers were never going to use Haskell. They wanted Ruby with less problems.
And that's what Go gave them — at least to the extent of being better than any other attempt to do the same. Since it solved real problems people had, without forcing them into new programming paradigms, it was adopted. Choosing a technology based on starry-eyed fandom and arbitrary feelings might be how you go about navigating this world, but that doesn't extrapolate.
This got to be a joke right. The only thing I hear is at Google no one likes Go. Most software is in C++, Rust, Java or Kotlin.
I personally appreciate Go as a research experiment. Plenty of very interesting ideas, just as, for instance, Haskell. I don't particularly like it as a development language, but I can understand why some people do.
Is there? When you get down to it, it is really just a faster Python. Which is exactly what it was said to be when it was released. Their goal was to create a "dynamically-typed" language that was more performant. It is likely that it wouldn't have had a static type system at all if they figured out how to achieve on the performance end without needing types.
You can tell who is clueless when you hear someone say its type system is lacking. I mean, technically it is, but it is supposed to be. Like saying Javascript or Ruby's type system is lacking.
- using zero values as an optimization mechanism;
- (non-)pointers and passing self by copy.
I mean, I hate both mechanisms, but intellectually, I find them quite interesting.
Also, I'd not classify it as a faster Python. It's more of a cousin of Obj-C if the authors of Obj-C had fallen in love of Erlang instead of Smalltalk.
Do you have any good examples? Not trying to argue, just genuinely curious as someone who hasn't been in this field for decades.
It was designed with contempt for developers, for example disallowing developers to create generic data structures, or lacking a decent way of error checking that is not extremely error prone and verbose.
TFA's point is that (safe) Rust is also like that, but achieves it by restricting all cases where a torn write could be observed through its type system instead of VM's memory model.
Usually, but not always! https://jcdav.is/2015/10/06/SIGSEGV-as-control-flow/
Is guaranteed that every offset you can try to read is guaranteed to create a segfault?
The offset is fixed as part of the compiled code; the JVM can enforce that it's less than 4k (otherwise it can use an explicit NULL check), and that the first 4k page is always unmapped.
Memory safety is just the source of bugs that we've figured out how to eliminate. It's a significant source of really bad (hard to debug due to action at a distance, high impact, etc) bugs so that's worth a lot, but it's not perfect. And even then we have a more frequently used escape hatch to the memory-unsafe world than would be ideal from a safety perspective for practical reasons.
A more complete version of safety would be achieved with a language that proves code correct to arbitrary specifications. We aren't there yet for there being such a language that is practical for every day use. Personally I'm increasingly optimistic we'll get there sooner rather than later (say, within 20 years). Even then there will probably be specification level bugs that prevent a claim of complete safety...
That said in many years of using Go in production I don't think I've ever come across a situation where the exact requirements to cause this bug have occurred.
Uber has talked a lot about bugs in Go code. This article is useful to understand some of the practical problems facing Go developers actually wind up being, particularly the table at the bottom summarizing how common each issue is.
https://www.uber.com/en-US/blog/data-race-patterns-in-go/
They don't have a specific category that would cover this issue, because most of the time concurrent map or slice accesses are on the same slice and this needs you to exhibit a torn read.
So why doesn't it come up more in practice? I dunno. Honestly beats me. I guess people are paranoid enough to avoid this particular pitfall most of the time, kind of like the Technology Connections theory on Americans and extension cords/powerstrips[1]. Re-assigning variables that are known to be used concurrently is obvious enough to be a problem and the language has atomics, channels, mutex locks so I think most people just don't wind up doing that in a concurrent context (or at least certainly not on purpose.) The race detector will definitely find it.
For some performance hit, though, the torn reads problem could just be fixed. I think they should probably do it, but I'm not losing sweat over all of the Go code in production. It hasn't really been a big issue.
[1]: https://www.youtube.com/watch?v=K_q-xnYRugQ
It ultimately resulted in a loop counter overflowing, which recomputed the same thing a billion of time (but always the same!). So the visible effect was a request would randomly take 3 min instead of 100ms.
I ended up using perf in production, which indirectly lead me to understand the data race.
I was called in to help the team because of my experience debugging the weirdest things as a platform dev.
Because of this I was exposed to so many races in Go, from my biased point of view, I want Rust everywhere instead.
But I guess I am putting myself out of a job? ;)
People talk a lot about the productivity gains of ai, but fixing problems like this at the language level could have an even bigger impact on productivity, but are far less sensational. Think about how much productivity is lost due to obscure but detectable bugs like this one. I don't think rust is a good answer (it doesn't check overflow by default), but at least it points a little bit in the vaguely correct direction.
so in reality, it's just "pick your own poison" to various degrees...
The non computable reals are a huge problem because, as their name suggests, we can't compute them - and in the strict sense that's Almost All reals, but none of the ones you're thinking of are non-computable so you'll likely be fine.
For the merely rational numbers like a third, or sixteen hundred and five sevenths, it's even more so a matter of choosing not to address it rather than it being out of reach.
We know for sure that algebraic numbers behave nicely in terms of equivalence, and there are other, bigger number systems that are conjectured to behave nicely ( https://en.wikipedia.org/wiki/Period_(algebraic_geometry) ), but the problem with these and computers is that they are hard to represent.
Maybe Python having automatic big numbers like Lisps often did will help introduce new programmers to the idea that the 32-bit two's complement integer provided on all modern computers isn't somehow "really" how numbers work.
There's really no excuse for a modern PL to not have, at the very least, overflow detection by default.
I assume this implies that common processor architectures (x86_64, aarch64) lack trap-on-overflow variants of their integer arithmetic instructions? If the explanation really is that simple, it's pretty disappointing.
https://doc.rust-lang.org/cargo/reference/profiles.html#over...
You can also either (in nightly Rust) use the strict APIs which make it explicit that you want the overflow panics, or, (stably) use the checked APIs and then do whatever makes sense, which could include explicitly panic when overflow would happen unexpectedly.
This would let you have e.g. code where most arithmetic is checked, but a tight inner loop you're pretty sure won't overflow only has checks in debug (in release it will wrap, but you should not rely on that for correctness, unintended overflow is a bug)
Yes*. But all modern instruction sets have condition flags and conditional instructions, so it's still very much possible to implement the checks robustly in machine code. However, doing so would generally require injecting at least one additional conditional-branch instruction, and in some cases, switching from non-flag-setting instructions to flag-setting instructions (which can be slower).
* = true "trap on overflow" existed in 32-bit x86 but was tricky to use and got removed when going to 64-bit
Go is really good at easy concurrency tasks, like things that have almost no shared memory at all, "shared-nothing" architectures, like a typical web server. Share some resources like database handles with a sync.Pool and call it a day. Go lets you write "async" code as if it were sync with no function coloring, making it decidedly nicer than basically anything in its performance class for this use case.
Rust, on the other hand, has to contend with function coloring and a myriad of seriously hard engineering tasks to deal with async issues. Async Rust gets better every year, but personally I still (as of last month at least) think it's quite a mess. Rust is absolutely excellent for traditional concurrency, though. Anything where you would've used a mutex lock, Rust is just way better than everything else. It's beautiful.
But I struggle to be as productive in Rust as I am in Go, because Rust, the standard library, and its ecosystem gives the programmer so much to worry about. It sometimes reminds me of C++ in that regard, though it's nowhere near as extremely bad (because at least there's a coherent build system and package manager.) And frankly, a lot of software I write is just boring, and Go does fine for a lot of that. I try Rust periodically for things, and romantically it feels like it's the closest language to "the future", but I think the future might still have a place for languages like Go.
You should calculate TCO in productivity. Can you write Python/Go etc. faster? Sure! Can you operate these in production with the same TCO as Rust? Absolutely not. Most of the time the person debugging production issues and data races is different than the one who wrote the code. This gives the illusion of productivity being better with Python/Go.
After spending 20+ years around production systems both as a systems and a software engineer I think that Rust is here for reducing the TCO by moving the mental burden to write data race free software from production to development.
So, my first job actually started as a pure Python gig. Operations for Python/Django absolutely sucked ass. Deploying Django code reliably was a serious challenge. We got better over time by using tools like Vagrant and Docker and eventually Kubernetes, so the differences between production and dev/testing eventually faded and become less notable. But frankly no matter what we did, not causing production issues with Django/Python was a true-to-life nightmare. Causing accidental type errors not caught by tests was easy and MyPy couldn't really cover all that much of the code easily, and the Django ORM was very easy to accidentally cause horrible production behavior with (that, of course, would look okay locally with tiny amounts of data.) This is actually the original reason why I switched to Go in the first place, at my first job in around 2016. The people who I worked with are still around to attest to this fact, if you want I can probably get them to chime in on this thread, I still talk to some of them.
Go was a totally different story. Yes, we did indeed have some concurrency pains, which really didn't exist in Python for obvious reasons, but holy shit, we could really eek a lot of performance out of Go code compared to Python. We were previously afraid we might have to move data heavy workloads from Twisted (not related to the Django stuff) to something like C++ or maybe even optimized Java, but Go handily took it and allowed us to saturate the network interface on our EC2 boxes. (A lot of communications were going over Websockets, and the standards for compression in websockets took a long time to settle and become universally supported, so we actually played with implementing the lz4 compression scheme in JS. I wound up writing my own lz4 implementation based on the algorithms, I believe, from the C version. It wound up being too much compute, though. But, we had to try, anyway.)
So how much reliability problems did we wind up having doing all this? Honestly not a whole lot on the Go side of things. The biggest production issue I ever ran into was one where the Kubernetes AWS integration blew up because we wound up having too many security groups. I wound up needing to make an emergency patch to kubelet in the early hours to solve that one :) We did run into at least one serious Go related issue over time, which was indeed concurrency related: when Go 1.6 came out, it started detecting concurrent misuses of maps. And guess what? We had one! It wasn't actually triggering very often, but in some cases we could run into a fairly trivial concurrent map access. It didn't seem to crash before but it could at least cause some weird behaviors in the event that it actually triggered before Go 1.6; now it was a crash that we could debug. It was a dumb mistake and it definitely underscores the value of borrow checking; "just don't mess up" will never prevent all mistakes, obviously. I will never tell you that I think borrow checking is useless, and really, I would love to just always write 100% correct software all the time.
That said though, that really is most of the extent of the production issues we had with Go. Go was a serious workhorse and we were doing reasonably non-trivial things in Go. (I had essentially built out a message queue system for unreliable delivery of very small events. We had a firehose of data coming in with many channels of information and needed to route those to the clients that needed them and handle throttling/etc. Go was just fantastic at this task.) Over time things got easier too, as Go kept updating and improving, helping us catch more bugs.
I can only come to one conclusion: people who treat Go and Python in the same class are just ignorant to the realities of the situation. There are cases where Rust will be immensely valuable because you really can't tolerate a correctness problem, but here's the thing about that Go concurrent map access issue: while it could cause some buggy behavior and eventually caused some crashing, it never really caused any serious downtime or customer issues. The event delivery system was inherently dealing with unreliable data streams, and we had multiple instances. If there was a blip, clients would just reconnect and people would barely notice anything even if they were actively logged in. (In fact, we really didn't do anything special for rolling deployments to this service, because the frontend component was built to just handle a disconnection gracefully. If it reconnected quickly enough, there was no visual disturbance.)
That's where the cost/benefit analysis gets tricky though. Python and Django and even Twisted are actually pretty nice and I'm sure it's even better than when we originally left it (to be clear we did still have some minor things in Django after that, too, but they were mostly internal-only services.) Python and Django had great things like the built-in admin panel which, while it couldn't solve everyone's needs, was pretty extensible and usable on its own. It took us a while to outgrow it for various use cases. Go has no equivalent to many Django conveniences, so if you haven't fully outgrown e.g. the Django admin panel and ORM, it's hard to fully give up on those features.
Throughout all of this, we had a lot more issues with our JS frontend code than we ever did with either Python/Django or Go, though. We went through trying so many things to fix that, including Elm and Flow, and eventually the thing that really did fix it, TypeScript. But that is another story. (Boy, I sure learned a lot on my first real career job.)
At later jobs, Go continued to not be at the center of most of the production issues I faced running Go software. That's probably partly because Go was not doing a lot of the most complicated work, often times the most complicated bits were message queues, databases and even to some degree memory caches, and the Go bits were mostly acting like glue (albeit definitely glue with application logic, to be sure.)
So is the TCO of Go higher than Rust? I dunno. You can't really easily measure it since you don't get to explore parallel universes where you made different choices.
What I can say is that Go has been a choice I never regretted making all the way from the very first time and I would choose it again tomorrow.
Thankfully though, people don't just throw their hands up there; a good amount of work has gone into figuring out the kinds of mistakes that often lead to Go concurrency bugs in the real world and writing static analysis tools that can help prevent them. That work, combined with Go's builtin tools and standard library, and the memory safety of individual isolated goroutines, makes most production Go concurrency bugs fairly boring even compared to C concurrency bugs, even though they theoretically have the same basic problem where you can freely share mutable data unsafely across concurrent threads.
So yes, it is still possible to write trivial, obvious concurrency bugs. The language won't stop you. However I've used Go across almost every job I've had since like 2016 and it has been rare to come across a concurrency bug this trivial. I hope I would catch flagrantly shared mutable state across threads during code review.
Hence linking to Uber's case study on the issue. The answer? Not that much.
Uber started performing race detection in production over a 6 month period and found 2,000 different race conditions. Ouch, that sounds horrible!
But wait, we're talking about 50 million lines of Go code and 2,100 services at the time of that writing. That means they were seeing approximately 1 race condition per 25,000 lines of code and about 1 race condition per service. That actually lines up pretty well with my experiences. Although I haven't had a production outage or serious correctness issue caused by a race condition in Go, I have seen probably about one or two race conditions that made it to production per service. I reckon those codebases were likely somewhere between 10,000 and 25,000 lines of code most likely, so not so far off of the scale.
But again it doesn't always lead to a serious production outage, it's just that simple. It could be worse too (could corrupt some data and pollute your production database or something, in the worst case) but usually it's better (wonky behavior but no long-term effects, maybe the service periodically crashes but restarts, leading to some dropped requests but no long term downtime.) Uber has no doubt seen at least some Go data races that have caused actual production outages, but they've seen at least 2,000 Go data races that haven't, otherwise they would've probably been caught before the race detector caught them, Go dumps stacktraces on crash. That has to tell you something about the actual probability of causing a production outage due to a data race.
Again, you do you, but I will not be losing sleep over this. It is something to be weary of when working on Go services, but it is manageable.
When working on Go services it is nearly the last thing I am concerned about.
This seems to come with the obvious implication that Golang should only ever be used to implement "services" that are essentially a part of the network infrastructure, passing requests along to other parts of the backend but not implementing any "logic" themselves (since that's where the correctness issues we're discussing might have severe consequences for the business). Isn't this a rather sobering take, all things considered?
It's not so much about being "boring" or not; Rust does just fine at writing boring code once you get familiar with the boilerplate patterns (Real-world experience has shown that Rust is not really at a disadvantage wrt. productivity or iteration speed).
There is a case for Golang and similar languages, but it has to do with software domains where there literally is no viable alternative to GC, such as when dealing with arbitrary, "spaghetti" reference graphs. Most programs aren't going to look like that though, and starting with Rust will yield a higher quality solution overall.
I don't believe that for a second. Even just going from Python to Go drops my productivity by maybe about 50%. Rust? Forget it.
Sure, if you have a project that demands correctness and high performance that requires tricky concurrency to achieve, something like Rust may make sense. Not for your run-of-the-mill programs though.
But more seriously, yeah, Rust doesn't make sense for trivial programs. But these days, I write Python for a living, and it doesn't take long to stumble upon bugs that Rust would have trivially detected from within the comfort from my IDE.
And an experienced Rust developer has internalized the patterns (such as cloning or ARC) that are needed to cope with the borrow checker while writing prototype-quality, quick-iteration code. What's easier, fixing hard-to-spot bugs in the code or getting that code to compile in the first place?
comparing apples to apples: Once you get a tiny bit of experience, almost all of that goes away. The common patterns and idioms in the language allow you to write whole programs without ever thinking about lifetimes or memory allocation or anything else different from the gc language case.
comparing apples to oranges: you do need to worry about those things when writing tricky memory management code that you couldn't even get from most gc lanuages... yeah then you have to worry about the things since it's a case where those things are the point.
> You could argue Rust still has an advantage in that it prevents bugs that in Go you're free to write, but then what you're claiming is that this compensates for the extra work you have to do upfront in Rust.
I have evidence in the form of multiple services and programs running in prod under heavy use for years without having to revist the code to deal with bugs. Meanwhile the stuff written in go has to be touched a lot to deal with bugs. The extra couple of weeks upfront to do it in rust is mitigated after the first incident with the go code. The effort proves worthwhile after the second incident.
Also tangentially related: the cost of an incident in the form of lost business, refunds, etc is usually far higher than the cost of a couple developer weeks.
>because an experienced Go developer probably has internalized how to avoid those bugs and the cost of preventing them can be nearly negligible
Some of them yes. But this is literally the same argument I'm making about rust experience meaning that you don't spend all that much extra effort up-front. Like I said, I'm about equally productive in go, python or rust.
> I think that's why most people seem to agree Rust is probably only advantageous where the cost of data races in production is higher than the cognitive cost (which translates into increased effort) on the programmer.
I think people who say this haven't gotten much experience in rust. In my experience they spent a week trying to learn rust and decided to stop and compare it to their years of other languages and paradigms.
I have written Rust for around 6 years now.
I guess I could say I've written ruby for 20 years... But someone full-time in ruby for only a year would likely be significantly better at the language than I am (i am bad at it).
But as much as I love LARPing about correctness (believe me I do,) it's just simply the case that we won't right perfect software and it's totally OK. It's totally OK that our software will have artificial limitations, like with Go, only accepting filenames that are valid UTF-8, or taking some unnecessary performance/latency hits, or perhaps even crashing in some weird ass edge case. There are very few domains in which correctness issues can't be tolerated.
I don't deal with domains that are truly mission critical, where people could die if the code is incorrect. At worst, people could lose some money if my code is incorrect. I still would prefer not to cause that to happen, but those people are generally OK with taking that risk if it means getting features faster.
That's why Go has a future really. It's because for most software, some correctness issues are not the end of the world, and so you can rely on not fully sound approaches to finding bugs, like automated testing, race detection, and so on.
Rust can also make some types of software more productive to write, but it is unlikely to beat Go in terms of productivity when it comes to a lot of the stuff SaaS shops deal with. And boy, the software industry sure is swamped in fucking SaaS.
I just wish Go supported parametric enums (sum types) and Option, rather than copying Hoare’s billion dollar mistake.
I ported some code to Go and rust a few years ago to try both languages out. The rust code ended up being 30% smaller because I could use an enum and a match expression. In Go I needed to make a set of types and interface{} to achieve the same thing - which was both slower and way more verbose. My rust implementation was as fast as my C implementation in 2/3rds as much code. And it was trivial to debug. My Go implementation took way more code to write - about the same amount of code as C, but it was harder to read than C and ran much slower.
For cookie cutter SAAS and prototypes, I prefer typescript. It’s fast enough for most things, and the type system is much more expressive without getting in your way. Not as convenient to deploy as go - especially on mobile. And the standard library is more like an attic. But in my opinion it’s a much better designed language.
Sadly, that project seems to be dead, but I hope someone picks up its mantle some day. A marginally better Go could, well, go far.
Once your sass products get enough users, and you're dealing with millions or billions of requests per day, those rare bugs start showing up quite often... And it turns out programming towards correctness is desirable, if for no other reason than to keep pagerduty quiet. Tolerating correctness issues isn't cost-free... People having to respond during off hours costs money and stress. I think most people would rather pay the costs at dev time, when they aren't under the pressure of an incident, than during an outage.
The question isn't "wouldn't you prefer more correctness?" it's "how much would you pay for how much of an improvement in correctness?".
Rust is still growing rapidly though, whereas Go is probably not growing rapidly anymore, I think Go has at least saturated it's own niche more than 50% and is on the other end of the curve by now. Last I checked Rust is the trendiest language by far, the one that people most wish they were writing, and the one that you want to be able to say your project is written in. So it would be extremely surprising to hear if there wasn't a growing Rust presence basically everywhere, SaaS's included.
It seems like you're in some kind of bubble, especially when looking at Rust usage in the industry.
> Once your sass products get enough users, and you're dealing with millions or billions of requests per day, those rare bugs start showing up quite often...
This is a blanket statement that's simply not true and I'm speaking as someone who uses Go in the exact scenario you described.
What kind of bugs are actually happening to these people? Do you have any real-world examples of the issues you're referring to, ones that suddenly start occurring only at the scale of millions or billions of requests per day to them?
This means that multiple goroutines were writing to the same local variable. I've never worked on a Go team where code that is structured in such a way would be considered normal or pass code review without good justification.
It's not because people intentionally write this way. A function takes a parameter (a Go slice for example) and calls another function and so one. Deep down a function copies the pointer to the slice (via closure for example). And then a goroutine is spawned with this closure.
The most obvious mistakes are caught quickly. Buu sharing a memory address between two threads can happen very indirectly.
And somehow in Go, everybody feels incredibly comfortable spawning millions of coroutines/threads.
[^1]: https://www.reddit.com/r/ProgrammerTIL/comments/4tspsn/c_it_...
[^2]: https://stackoverflow.com/questions/69375375/is-it-safe-to-a...
Within safe rust you would likely need to be using an explicit .wrapping_add() on your counter, and explicitly constructing a for loop that wasn't range-based...
But I think terms like "memory safety" should have a reasonably strict meaning, and languages that go the extra mile of actually preventing memory corruption even in concurrent programs (which is basically everything typically considered "memory safe" except Go) should not be put into the same bucket as languages that decide not to go through this hassle.
We had a rule at my last gig: avoid anonymous functions and always recover from them.
Go can already ensure "consistency of multi-word values": use whatever synchronization you want. If you don't, and you put a race into your code, weird shit will happen because torn reads/writes are fuckin weird. You might say "Go shouldn't let you do that", but I appreciate that Go lets me make the tradeoff myself, with a factoring of my choosing. You might not, and that's fine.
But like, this effort to blow data races up to the level of C/C++ memory safety issues (this is what is intended by invoking "memory safety") is polemic. They're nowhere near the same problem or danger level. You can't walk 5 feet through a C/C++ codebase w/o seeing a memory safety issue. There are... zero Go CVEs resulting from this? QED.
EDIT:
I knew I remembered this blog. Here's a thing I read that I thought was perfectly reasonable: https://www.ralfj.de/blog/2021/11/18/ub-good-idea.html. Quote:
"To sum up: most of the time, ensuring Well-Defined Behavior is the responsibility of the type system, but as language designers we should not rule out the idea of sharing that responsibility with the programmer."
So much of all of this is weirdly entitled. Languages that do things differently exist: Erlang, Pony, Rust. They all make wildly different tradeoffs than Go does, not better, not worse, different. If you think they're better, use 'em. Let your better software win in the market. These weirdo polemics just fan language flamewars.
What's happening here, as happens so often in other situations, is that a term of art was created to describe something complicated; in this case, "memory safety", to describe the property of programming languages that don't admit to memory corruption vulnerabilities, such as stack and heap overflows, use-after-frees, and type confusions. Later, people uninvolved with the popularization of the term took the term and tried to define it from first principles, arriving at a place different than the term of art. We saw the same thing happen with "zero trust networking".
The fact is that Go doesn't admit memory corruption vulnerabilities, and the way you know that is the fact that there are practically zero exploits for memory corruption vulnerabilities targeting pure Go programs, despite the popularity of the language.
Another way to reach the same conclusion is to note that this post's argument proves far too much; by the definition used by this author, most other higher-level languages (the author exempts Java, but really only Java) also fail to be memory safe.
Is Rust "safer" in some senses than Go? Almost certainly. Pure functional languages are safer still. "Safety" as a general concept in programming languages is a spectrum. But "memory safety" isn't; it's a threshold test. If you want to claim that a language is memory-unsafe, POC || GTFO.
> The fact is that Go doesn't admit memory corruption vulnerabilities
Except it does. This is exactly the example in the article. Type confusion causes it to treat an integer as a pointer & deference it. This then trivially can result in memory corruption depending on the value of the integer. In the example the value "42" is used so that it crashes with a nice segfault thanks to lower-page guarding, but that's just for ease of demonstration. There's nothing magical about the choice of 42 - it could just as easily have been any number in the valid address space.
And data races allow all of that. There cannot be memory-safe languages supporting multi-threading that admit data races that lead to UB. If Go does admit data races it is not memory-safe. If a program can end up in a state that the language specification does not recognize (such as termination by SIGSEGV), it’s not memory safe. This is the only reasonable definition of memory safety.
You could argue Go is safe from memory vulnerabilities, and that'll be 99% correct (we can't know what will happen if some very strong organization (e.g. a nation-state actor) will heavily invest in exploiting some Go program), but it still isn't memory safe, as per the definition in Wikipedia:
> Memory safety is the state of being protected from various software bugs and security vulnerabilities when dealing with memory access, such as buffer overflows and dangling pointers.
Honestly, forget about Go: when was the last time you heard of a modern application backend being exploited through memory corruption, in any language? I know that Google and Meta and the like use a good amount of C++ on the server, as do many smaller companies. That C++ code may skew ‘modern’ and safer, but you could say the same about newly-developed client-side C++ code that’s constantly getting exploited. So where are the server-side attacks? Part of the answer is probably that they exist, but I don’t know about them because they haven’t been disclosed. Unlike client-side attacks, server-side attacks usually target a single entity who has little incentive to publish deep dives into how they were attacked. That especially applies to larger companies, which tend to use more C++. But we do sometimes see those deep dives written anyway, and the vulnerabilities described usually aren’t memory safety related. So I think there is also a gap in actual exploitation. Which probably has a number of causes, but I’d guess they include attackers (1) usually not having ready access to binaries, (2) not having an equivalent to the browser as a powerful launching point for exploits, and (3) not having access to as much memory-unsafe code as on the client side.
This is relevant to Go because of course Go is usually used on the server side. There is some use of Go on the client side, but I can’t think offhand of a single example of it being used in the type of consumer OS or client-side application that typically gets attacked.
Meanwhile, Go is of course much safer than C++. To make exploitation possible in Go, not only do you need a race condition (which are rarely targeted by exploits in any language), you also need a very specific code pattern. I’m not sure exactly how specific. I know how a stereotypical example of an interface pointer/viable mismatch works. But are there other options? I hear that maps are also thread-unsafe in general? I’d need to dig into the implementation to see how likely that is to be exploitable.
Regardless, the potential exists. If memory safety is a “threshold test” as you say, then Go is not memory-safe.
I agree though that the point would best be proven with a PoC of exploiting a real Go program. As someone with experience writing exploits, I think I could probably locate a vulnerability and create an exploit, if I had a few months to work on it. But for now I have employment and my free time is taken up by other things.
It happens all the time, but it’s a bit hard to find because “modern application backend[s]” are usually written in Go or Python or Rust. Even so, you’ll find plenty of exploits based on getting a C or C++ library on the backend to parse a malformed file.
https://googleprojectzero.blogspot.com/2016/12/chrome-os-exp...
Admittedly, Go is popular among developers. And there are some public examples of client-side attacks targeting developers and security researchers specifically. Such attacks could hypothetically go after something like Docker. But, searching now, every single example I can find seems to either exploit a non-developer-specific target (browser, iMessage, Acrobat), or else not exploit anything and just rely on convincing people to execute a Trojan (often by sending a codebase that executes the Trojan when you build it).
That bifurcation actually surprises me and I’m not sure what to conclude from it, other than “build systems are insecure by design”. But at any rate, the lack of Go exploits doesn’t say much if we don’t see exploits of developer tools written in C either.
Yes, this is an enormous effort to construct exploits, but constructing exploits for C/C++ code is much much easier and gives not less, or even more, benefit. Therefore it makes sense the efforts are focused on that.
If/when most C/C++ code in the world will be gone, I assume we'll see more exploits of Go code.
Can you show me any reasonable proof of concept (without using unsafe etc.) in Go that leads to similar memory corruption and is exploitable for RCE?
This example hardcodes the payload, but (unless I've badly misunderstood how the exploit works) that's not necessary, it could instead be input from the network (and you wouldn't have to pass that input to any APIs that are marked unsafe). The payload is just hardcoded so that the example could be reproduced on the public Go Playground, which sandboxes the code it runs and so can't accept network input.
Note that what tptacek is asking for is more stringent than this; he wants a proof-of-concept exploitation of a memory safety vulnerability caused by the data-race loopholes in the Go memory model, in a real program that someone is running in production. I do think it's interesting that nobody has demonstrated that yet, but I'm not sure what it tells us about how sure we can be that those vulnerabilities don't exist.
https://github.com/StalkR/misc/blob/master/go/gomium/exploit...
The tight goroutine loop that flips one variable between two different struct types just to win a race is not something a typical developer writes on purpose. The trick to "defeat" compiler optimizations by assigning to a dummy variable inside an inline function. Carefully computing the address difference between two slices to reach out of bounds, then using that to corrupt another slice’s header. I mean calling mprotect and jumping to shellcode is outright exploit engineering, not business logic and it's not part of the attackers payload.
Chances of exact PoC pattern showing up in the wild by accident is basically zero.
https://www.youtube.com/watch?v=1XIcS63jA3w
https://www.cloudfoundry.org/blog/cve-2020-15586/
I don’t see any evidence that anyone wrote an RCE exploit for this, but I also don’t see any evidence of anyone even trying to rule it out.
On first glance, it looks like the bug can (at least) result in the server accessing a slice object where the various fields don’t all come from the same place. So the target server can end up accessing some object out of bounds (or as the wrong type or both), which can easily end up writing some data (possibly attacker controlled) to an inappropriate place. In standard attack, the attacker might try to modify the stack or a function pointer to set up a ROP chain or something similar, which is close enough to arbitrarily code to eventually either corrupt something to directly escalate privileges or to do appropriate syscalls to actually execute code.
> Where does the attacker-controlled data come from.
The example I gave was an HTTP server. Attackers can shove in as much attacker-controlled data as they want. They can likely do something like a heap by using many requests or many headers. Unless the runtime zeroes freed memory (and frees it immediately, which GC languages like Go often don’t do), then lots of attacker controlled data will stick around. And, for all I know, the slice that gets mixed up in this bug is fully attacker controlled!
In any event, I think this whole line of reasoning is backwards. Developers should assume that a memory safety error is game over unless there is a very strong reason to believe otherwise — assume full RCE, ability to read and write all in-process data, the ability to issue any syscall, and the ability to try to exploit side channels. Maybe very strong mitigations like hardware-assisted CFI will change this, and maybe not.
The line I'm not quite as sure about is https://go.googlesource.com/go/+/refs/tags/go1.13.1/src/bufi.... That assignment is to a variable of interface type, so in theory it could cause memory corruption if multiple goroutines executed it concurrently on the same receiver, which was possible until the bug was fixed. That said, I cannot immediately think of a way to exploit this; you can only write error values corresponding to errors that you can make occur while writing to the socket, and that's a much more constrained set of possible values than the arbitrary bytes that can occur in a buffer. And for that, you only get confusion among the types of those particular errors. It might be possible but it at least looks challenging.
A definition of memory safety that permits unsoundness as long as nobody has exploited said unsoundness is not a definition that anyone serious about security is going to accept. Unsoundness is unsoundness, undefined behavior is undefined behavior. The conservative stance is that once execution hits UB, anything can happen.
Compare to an innocent looking map operation, and it's not even in the same league.
I could also argue C is memory safe and all the exploits that have been made weren’t real C programs
Second, the burden of proof goes the other way. It’s absurd to claim that UB is safe unless proven otherwise. Unsafety must obviously be the default assumption.
This is wrong.
I explicitly exempt Java, OCaml, C#, JavaScript, and WebAssembly. And I implicitly exempt everyone else when I say that Go is the only language I know of that has this problem.
(I won't reply to the rest since we're already discussing that at https://news.ycombinator.com/item?id=44678566 )
There's a POC right in the post, demonstrating type confusion due to a torn read of a fat pointer. I think it could have just as easily been an out-of-bounds write via a torn read of a slice. I don't see how you can seriously call this memory safe, even by a conservative definition.
Did you mean POC against a real program? Is that your bar?
We're talking about programming languages being memory safe (like fly.io does on it's security page [1]), not about other specific applications.
It may be helpful to think of this as talking about the security of the programming language implementation. We're talking about inputs to that implementation that are considered valid and not using "unsafe" marked bits (though I do note that the Go project itself isn't very clear on if they claim to be memory-safe). Then we want to evaluate whether the programming language implementation fulfills what people think it fulfills; ie: "being a memory safe programming language" by producing programs under some constraints (ie: no unsafe) that are themselves memory-safe.
The example we see in the OP is demonstrating a break in the expectations for the behavior of the programming language implementation if we expected the programming language implementation to produce programs that are memory safe (again under some conditions of not using "unsafe" bits).
[1]: https://fly.io/docs/security/security-at-fly-io/#application...
If you've got concerns about our security page, I think you should first take them to the ISRG Prossimo project.
https://www.memorysafety.org/docs/memory-safety/
(In a separate comment about "what do people claim about Go anyhow", I linked the memorysafety.org page, but I did not expect it to help in getting you to the understanding that we can evaluate programming languages as being memory safe or not, where something from the company where someone was a founder seemed more likely to get a person to reconsider the framing of what we're examining)
Happens all the time in math and physics but having centuries of experience with this issue we usually just slap the name of a person on the name of the concept. That is why we have Gaussian Curvature and Riemann Integrals. Maybe we should speak of Jung Memory Safety too.
Thinking about it, the opposite also happens. In the early 19th century "group" had a specific meaning, today it has a much broader meaning with the original meaning preserved under the term "Galois Group".
Or even simpler: For the longest time seconds were defined as fraction of a day and varied in length. Now we have a precise and constant definition and still call them seconds and not ISO seconds.
Another way to word it: If "Go is memory unsafe" is such a revelation after its been around for 13 years, it's more likely that such a statement is somehow wrong than that nobody's picked up on such a supposedly impactful safety issue in all this time.
As such, the burden of proof that addresses why nobody's ran into any serious safety issues in the last 13 years is on the OP. It's not enough to show some theoretical program that exhibits the issue, clearly that is not enough to cause real problems.
"One could easily turn this example into a function that casts an integer to a pointer, and then cause arbitrary memory corruption."
No, one couldn't! One has contrived a program that hardcodes precisely the condition one wants to achieve. In doing so, one hasn't even demonstrated even one of the two predicates for a memory corruption vulnerability (attacker control of the data, and attacker ability to place controlled data somewhere advantageous to the attacker).
What the author is doing is demonstrating correctness advantages of Rust using inappropriate security framing.
Could you quote where exactly OP has misleadingly "suggested" that these concerns lead to security issues in the typical case?
> attacker control of the data, and attacker ability to place controlled data somewhere advantageous to the attacker
Under this definition the Rowhammer problem with hardware DRAM does not qualify as a genuine security concern since it inherently relies on fiddly non-determinism that cannot possibly be "controlled" by any attacker. (The problem with possible torn writes in concurrent Go code is quite similar in spirit; it's understood that an actually observed torn write might only occur rarely.) Needless to say there is a fairly strong case for addressing these problems anyway, as a matter of defence in depth.
> correctness advantages of Rust
Memory safety in OP's sense is not exclusive to Rust. Swift has it. Even Java/C# cannot access arbitrary memory as a result of torn writes. It would be more accurate to say that OP has identified a correctness issue that's apparently exclusive to Go.
I really don't understand why people get so obsessed with their tools that it turns into a political battleground. It's a means to an end. Not the end itself.
Haskell in general is a much safer than Rust thanks to its more robust type system (which also forms the basis of its metaprogramming facilities), monads being much louder than unsafe blocks, etc. But data races and deadlocks are one of the few things Rust has over it. There are some pure functional languages that are dependently typed like Idris, and thus far safer than Rust, but they're in the minority and I've yet to find anybody using them industrially. Also Fortnite's Verse thing? I don't know how pure that language is though.
Rust absolutely does make it easier to write high-performance threaded code correctly, though. If your system depends on high amounts of concurrent mutation, Rust definitely makes it easier to write correct code.
On the other hand, a system like STM in Haskell can make it easier to write complex concurrency logic correctly in Haskell than Rust, but it can have very bad performance overhead and needs to be treated with extreme suspicion in performance-sensitive code. It's a huge win for simple expression of complex concurrency, but you have to pay for it somewhere. It can be used in ways where that overhead is acceptable, but you absolutely need to be suspicious in a way that's never a concern in Rust.
Yes I mean that was the whole reason they invented rust. If there were a bunch of performant memory safe languages already they wouldn't have needed to.
This doesn’t prove a negative, but is probably a good hint that this risk is not something worth prioritizing for Go applications from a security point of view.
Compare this with C/C++ where 60-75% of real world vulnerabilities are memory safety vulnerabilities. Memory safety is definitely a spectrum, and I’d argue there are diminishing returns.
With maintenance being a "large" integer multiple of initial development, anything that brings that factor down is probably worth it, even if it comes at an incremental cost in getting your thing out the door.
Do you? Not every bug needs to be fixed. I've never see a data race bug in documented behaviour make it past initial development.
I have seen data races in undocumented behaviour in production, but as it isn't documented, your program doesn't have to do that! It doesn't matter if it fails. It wasn't a concern of your program in the first place.
That is still a problem if an attacker uses undocumented behaviour to find an exploit, but when it is benign... Oh well. Who cares?
The post is about data races in safe Go leading to crashes or exploits, because things like "eface" (the tuple used to implement interfaces) and slices (again a tuple) are multi-word and thus impossible to update atomically.
This is not an issue in Java because such things always box behind a pointer.
This of course has some overhead, which is why you usually turn it into the cheaper, data race free Span<T>. Go could have the same safety and fix some of the overhead with compiler optimizations, they just don't want to take the trade-off.
It’s a nice theoretical argument but doesn’t hold up in practice.
I agree with the sentiment that data races are generally harder to exploit, but it _is possible_ to do.
It can be as simple as changing the size of a vector from one thread while the other one accesses it. When executed sequentiality, the operations are safe. With concurrency all bets are off. Even with Go. Hence the argument in TFA.
Show me the exploits based on Go parallelism. This issue has been discussed publicly for 10 years yet the exploits have not appeared. That’s why it's a nice theoretical argument but does not hold up in practice.
Nice strawman though
In the meantime, we thankfully have agency and are free to choose not to use global variables and shared memory even if the platform offers them to us.
Modern languages have the option of representing thread-safety in the type system, e.g. what Rust does, where working with threads is a dream (especially when you get to use structured concurrency via thread::scope).
People tend to forget that Rust's original goal was not "let's make a memory-safe systems language", it was "let's make a thread-safe systems language", and memory safety just came along for the ride.
The Rust we have from 1.0 onwards is not what Graydon wanted at all. Would Graydon's language have been broadly popular? Probably not, we'll never know.
What kills performance are not memory copies, but locks. Parallel nonblocking IO and a non POSIX stdlib will bring you far away from C++ or Rust performance.
I'm pretty sure if every thread executing an LLM model had to have its own copy that that would murder performance more than any lock does, and it won't even be close.
It's cheaper to copy than to lock when the data is small, but that does not scale and it also ignores things like reader/writer locks where the data is primarily read-only, at least during the concurrent stage. Or where the work can be safely chunked up such that writes don't ever overlap which is very common in graphics
Exactly this.
strong claim. care to back it up?
(Don't get me wrong, I liked the idea behind Pony for backend code, it's much saner than Go for the same target space. But it failed to capture that market, because Go was already there. And it was never a competitor to Rust because this model is only viable for web back end tasks, not for general computing).
The pony model was also better for compute tasks, not just IO. Because it provided safe concurrency, 10x faster than go.
Think for instance about how you'd do efficient matrix multiplication of two matrices with a million row and column, in Pony, versus how it works in languages with shared memory. You'd spend a gigantic amount of time copying data for no good reason…
[1]: https://docs.rs/tailcall/latest/tailcall/
https://crates.io/crates/realistic
Some more modern languages - eg. Swift – have "sendable" value types that are inherently thread safe. In my experience some developers tend to equate "sendable" / thread safe data structures with a silver bullet. But you still have to think about what you do in a broader sense… You still have to assemble your thread safe data structures in a way that makes sense, you have to identify what "transactions" you have in your mental model and you still have to think about data consistency.
To be fair though, go has a big emphasis on using its communication primitives instead of directly sharing memory between goroutines [1].
[1] https://go.dev/blog/codelab-share
For example, is the following program safe, or does it race?
The answer is of course that it's a data race. Why?Because `buf.Bytes()` returns the underlying memory, and then `Reset` lets you re-use the same backing memory, and so "processData" and "main" are both writing to the same data at the same time.
In rust, this would not compile because it is two mutable references to the same data, you'd either have to send ownership across the channel, or send a copy.
In go, it's confusing. If you use `bytes.Buffer.ReadBytes("\n")` you get a copy back, so you can send it. Same for `bytes.Buffer.String()`.
But if you use `bytes.Buffer.Bytes()` you get something you can't pass across a channel safely, unless you also never use that bytes.Buffer again.
Channels in rust solve this problem because rust understands "sending" and ownership. Go does not have those things, and so they just give you a new tool to shoot yourself in the foot that is slower than mutexes, and based on my experience with new gophers, also more difficult to use correctly.
>
> But if you use `bytes.Buffer.Bytes()`
If you're experienced, it's pretty obvious that a `bytes.Buffer` will simply return its underlying storage if you call `.Bytes()` on it, but will have to allocate and return a new object if you call say `.String()` on it.
> unless you also never use that bytes.Buffer again.
I'm afraid that's concurrency 101. It's exactly the same in Go as in any language before it, you must make sure to define object lifetimes once you start passing them around in concurrent fashion.
Channels are nice in that they model certain common concurrency patterns really well - pipelines of processing. You don't have to annotate everything with mutexes and you get backpressure for free. But they are not supposed to be the final solution to all things concurrency and they certainly aren't supposed to make data races impossible.
> Even if you use channels to send things between goroutines, go makes it very hard to do so safely
Really? Because it seems really easy to me. The consumer of the channel needs some data to operate on? Ok, is it only for reading? Then send a copy. For writing too? No problem, send a reference and never touch that reference on our side of the fence again until the consumer is done executing.
Seems about as hard to understand to me as the reason why my friend is upset when I ate the cake I gave to him as a gift. I gave it to him and subsequently treated it as my own!
Such issues only arise if you try to apply concurrency to a problem willy-nilly, without rethinking your data model to fit into a concurrent context.
Now, would the Rust approach be better here? Sure, but not if that means using Rust ;) Rust's fancy concurrency guarantees come with the whole package that is Rust, which as a language is usually wildly inappropriate for the problem at hand. But if I could opt into Rust-like protections for specific Go data structures, that'd be great.
"2. Shared buffer causes race/data reuse You're writing to buf, getting buf.Bytes(), and sending it to the channel. But buf.Bytes() returns a slice backed by the same memory, which you then Reset(). This causes line in processData to read the reset or reused buffer."
I mean, you're basically passing a pointer to another thread to processData() and then promptly trying to do stuff with the same pointer.
And yet, "bytes.Buffer.ReadBytes(delim)" returns a copy of the underlying data which would be safe in this context.
The type system does not make it obvious when this is safe or not, and passing pointers you own across channels is fine and common.
> That code would never pass a human pull request review
Yes, that was a simplified example that a human or AI could spot.
When you actually see this in the wild, it's not a minimal example, it's a small bug in hundreds of lines of code.
I've seen this often enough that it obviously does actually happen, and does pass human code review.
This isn't anything special, if you want to start dealing with concurrency you're going to have to know about race conditions and such. There is no language that can ever address that because your program will always be interacting with the outside world.
Just wondering.
Realistically that would be quite rare since it is obvious that this is unprotected shared mutable access. But interesting that such a conversion without unsafe may happen. If it segfaults all the time though then we still have memory safety I guess.
The article is interesting but I wish it would try to provide ideas for solutions then.
In contrast to the go project itself, external users of Go frequently make strong claims about Go's memory safety. fly.io calls Go a "memory-safe programming language" in their security documentation (https://fly.io/docs/security/security-at-fly-io/#application...). They don't indicate what a "memory-safe programming language" is. The owners of "memorysafety.org" also list Go as a memory safe language (https://www.memorysafety.org/docs/memory-safety/). This later link doesn't have a concrete definition of the meaning of memory safety, but is kind enough to provide a non-exaustive list of example issues one of which ("Out of Bounds Reads and Writes") is shown by the article from this post to be something not given to us by Go, indicating memorysafety.org may wish to update their list.
It seems like at the very least Go and others could make it more clear what they mean by memory safety, and the existence of this kind of error in Go indicates that they likely should avoid calling Go memory safe without qualification.
Yeah... I was actually surprised by that when I did the research for the article. I had to go to Wikipedia to find a reference for "Go is considered memory-safe".
Maybe they didn't think much about it, or maybe they enjoy the ambiguity. IMO it'd be more honest to just clearly state this. I don't mind Go making different trade-offs than my favorite language, but I do mind them not being upfront about the consequences of their choices.
At the time Go was created, it met one common definition of "memory safety", which was essentially "have a garbage collector". And compared to c/c++, it is much safer.
This is the first time I hear that being suggested as ever having been the definition of memory safety. Do you have a source for this?
Given that except for Go every single language gets this right (to my knowledge), I am kind of doubtful that this is a consequence of the term changing its meaning.
If you go back to the original 2009 announcement talk, "Memory Safety" is listed as an explicit goal, with no carveouts:
"Safety is critical. It's critical that the language be type-safe and that it be memory-safe."
"It is important that a program not be able to derive a bad address and just use it; That a program that compiles is type-safe and memory-safe. That is a critical part of making robust software, and that's just fundamental."
https://youtu.be/rKnDgT73v8s?t=463
Note that this was not Rust's first stable release, but it's first public release. At the time it was still changing a lot and still had "garbage collected" types.
And while rust did have optional "garbage collected pointers", it's important to point out that it is not a garbage collected language. The ownership system and borrow checker were very much front-and-centre for the 0.1 release, it was what everyone was talking about.
Actually, my memory is that while the language had syntax to declare garbage collected pointers, it wasn't actually hooked up to a proper garbage collector. It was always more of a "we are reserving the syntax and we will hook it up when needed", and it turns out the ownership system was powerful enough that it was never needed.
AFAIK it was just an `Rc`/`Arc` with the possibility of upgrading it to an actual GC in the future.
One can distinguish between native (OS) threads and green (language-runtime) threads which may use a different context-switching mechanism. But that's more of a spectrum in terms of thread-safety; similar to how running multiple threads on a single CPU core without SMT, single CPU core with SMT, multiple CPU cores, with different possible CPU cache coherency guarantees, create a spectrum of possible thread-safety issues.
Can you violate memory safety in C# without unsafe{} blocks (or GCHandle/Marshal/etc.)? (No.)
Can you write thread-unsafe code in C# without using unsafe{} blocks etc.? (Yes, just make your integers race.)
Doesn't that contradict the claim that you can't have memory safety without thread safety?
How many exploits or security issues have there been related to data race on dual word values? I work with Go for the last 10 years and I never heard of such issues. Not a single time.
For some examples, Rust (although this is not specific to it) uses stack guard pages to detect stack overflows by _forcing_ a segfault (as opposed to reading/writing arbitrary memory after the usual stack). Some JVMs also expect and handle segfaults when dereferencing null pointers, to avoid always paying the cost for checking them.
The violation occurs if the program keeps running after having violated a memory safety property. If the program terminates, then it can still be memory safe in the definition.
Segfaults has nothing to do with the properties. There's some languages or some contexts in which segfaults is part of the discussion, but in general, the theory doesn't care about segfaults.
I don't know what you're trying to say here. C would also be memory-safe if the program just simply stopped after violating memory safety, but it doesn't necessarily do that, so it's not memory safe. And neither is Go.
> Rust has, unfortunately, changed the narrative so that people now believe memory safety is a property of the language, when it is one of the implementation.
I am not sure I agree with that (the concept of memory-safe languages looong predates Rust), but you can just define a memory-safe language as one where all conforming implementations are memory-safe -- making it a feature of the language itself, not just a feature of a particular implementation.
The only reason this isn't a more critical issue is because data races are hard to exploit and there aren't lot of concurrent Go programs/system libraries that accept lot of attacker controlled inputs.
What you can say though (and the point I made upthread) is that if a language manages to provably never segfault, then it must have some sort of true language-enforced safety because the difference between segfaulting or not is really just a matter of granularity.
Memory safe languages make it harder to segfault but that's a consequence, not the primary goal. Segfaults are just another memory protection. If memory bugs only ever resulted in segfaults the instant constraints are violated, the hardware protections would be "good enough" and we wouldn't care the same way about language design.
The document is backed by foreign government as well.
https://media.defense.gov/2023/Dec/06/2003352724/-1/-1/0/THE...
You did not even read the link the parent comment provided and are continuing with the same flawed argument.
Now the big question, as you mention, is "can it be exploited?" My assumption is that it can, but that there are much lower-hanging fruits. But it's just an assumption, and I don't even know how to check it.
But I don't agree with:
> I will argue that this distinction isn’t all that useful, and that the actual property we want our programs to have is absence of Undefined Behavior.
There is plenty of undefined behavior that can't lead to violating memory safety. For example, in many languages, argument evaluation order is undefined. If you have some code like:
In some languages, it's undefined as to whether "1" is printed before "2" or vice versa. But there's no way to violate memory safety with this.I think the only term the author needs here is "memory safety", and they correctly observe that if the language has threading, then you need a memory model that ensures that threads can't break your memory safety.
Go lacks that. It seems to be a rare problem in practice, but if you want guarantees, Go doesn't give you them. In return, I guess it gives you slightly faster execution speed for writes that it allows to potentially be torn.
You are mixing up non-determinism and UB. Sadly that's a common misunderstanding.
See https://www.ralfj.de/blog/2021/11/18/ub-good-idea.html for an explanation of what UB is, though I don't go into the distinction to non-determinism there.
It was changed as part of the C++11 memory model and now, as you said, there is a sequenced-before order, it is just unspecified which one it is.
I don't know much about C, but I believe it was similarly changed in C11.
This is very amusing because that means in terms of the language standard Windows and Linux, which both significantly pre-date C++ 11 and thus its memory model, were technically relying on Undefined Behaviour. Of course, as operating systems they're already off piste because they're full of raw assembly and so on.
Linux has its own ordering model as a result, pre-dating the C++ 11 model. Linus is writing software for multi-processor computers more than a decade before the C++ 11 model so obviously he can't wait around for that.
[Edit: Corrected Linux -> Linux when talking about the man]
Of course these guarantees were often not fully written down nor necessarily self consistent (but then again, neither is the current standard).
Cppreference is not authoritative[1], but seems to support my recollection. In fact it states that the f(++i, ++i) was UB till C++17.
[1] https://en.cppreference.com/w/cpp/language/eval_order.html, Pre C++11 Ordering Rules, point (2).
The word "undefined" has a clear meaning: there is no behavior defined at all for what a given piece of code will do, meaning it can literally do anything. If the language spec defines the possible behaviors you can expect (even if the behavior can vary between implementations), then by definition it's not undefined.
Sure, I agree with that.
> The word "undefined" has a clear meaning: there is no behavior defined at all for what a given piece of code will do...
That is true, but...
> ...meaning it can literally do anything.
This is not at all true! That is a different (but closely related) matter, which is "what is to be done about undefined behavior". Which is certainly something one has to take a stance on when working to a language spec that has undefined behavior, but that does not mean that "undefined" automatically means your preferred interpretation of how to handle undefined behavior.
Memory safety is a much bigger problem.
Java got this right. Fil-C gets it right, too. So, there is memory safety without thread safety. And it’s really not that hard.
Memory safety is a separate property unless your language chooses to gate it on thread safety. Go (and some other languages) have such a gate. Not all memory safe languages have such a gate.
> At this point you might be wondering, isn’t this a problem in many languages? Doesn’t Java also allow data races? And yes, Java does allow data races, but the Java developers spent a lot of effort to ensure that even programs with data races remain entirely well-defined. They even developed the first industrially deployed concurrency memory model for this purpose, many years before the C++11 memory model. The result of all of this work is that in a concurrent Java program, you might see unexpected outdated values for certain variables, such as a null pointer where you expected the reference to be properly initialized, but you will never be able to actually break the language and dereference an invalid dangling pointer and segfault at address 0x2a. In that sense, all Java programs are thread-safe.
And:
> Java programmers will sometimes use the terms “thread safe” and “memory safe” differently than C++ or Rust programmers would. From a Rust perspective, Java programs are memory- and thread-safe by construction. Java programmers take that so much for granted that they use the same term to refer to stronger properties, such as not having “unintended” data races or not having null pointer exceptions. However, such bugs cannot cause segfaults from invalid pointer uses, so these kinds of issues are qualitatively very different from the memory safety violation in my Go example. For the purpose of this blog post, I am using the low-level Rust and C++ meaning of these terms.
Java is in fact thread-safe in the sense of the term used in the article, unlike Go, so it is not a counterexample to the article's point at all.
The title is wrong. That's important.
> Java is in fact thread-safe in the sense of the term used in the article
The article's notion of thread safety is wrong. Java is not thread safe by construction, but it is memory safe.
These terms are used slightly differently by different communities, which is why I discuss this point in the article. But you seem adamant that you have the sole authority for defining these terms so :shrug:
They also count data race freedom as part of memory safety, which I think is wrong (and contradicts their inclusion of Java and even Go in the list of memory safe languages).
So no, I’m not an authority. I’m just following the general trend of how the term is used.
And ive never heard “memory safe” used in relation to not having null pointer exceptions. That’s a new one and sounds nonsensical, frankly
If a language is "memory safe" but not "thread safe", is the result "the language is free from 'memory faults', unless threads are involved"?
Or to put it another way; when used however the term of art is intended, "memory safety" is meant to provide some guarantees about not triggering certain erroneous conditions. "not thread safe" seems to mean that those same erroneous conditions can be triggered by threads, which seems to amount to '"memory safety" does not guarantee the absence of erroneous memory conditions'.
Yes.
If a language is memory safe but not thread safe, then you can race, but the outcome of those races won't be memory corruption or the violation of the language's type system. It will lead to weird stuff, however - just a different kind of weirdness than breaking out of the language's sandbox
By these definitions, doesn't that mean go is neither memory or thread safe? It looks like concurrent modification can result in memory corruption, e.g. the attempted access 0x42 example in the article
Yes, with the caveat that you can't treat "memory safe" as a binary condition.
The strictest notion of memory safety is what I call GIMSO: "Garbage In, Memory Safety Out". I.e. there does not exist any sequence of bytes you could feed to the compiler that would result in a memory-unsafe outcome at runtime. Java aims for this. Fil-C does too. JavaScript also does.
But there are languages that I think it's fair to consider to be memory safe that offer escape hatches that violate GIMSO. Rust with `unsafe` is an example. C# with `unsafe` is another. Java if you include `sun.misc.Unsafe` (arguably it's not part of the language).
So I think if a language is memory safe, not thread safe, and the memory safety is gated on thread safety, then it's kinda fair to make statements like, "it's memory safe", if you have fine print somewhere that says "but the memory safety does not hold under the following kinds of races".
All of that said, I'd rather we just said that "memory safety" means what I call "GIMSO". But the ship has sailed. Lots of languages are called "memory safe" to mean something like, "you can get memory safety in this language if you obey certain idioms" - and in Rust that means "don't use unsafe" while in Go that means "don't race in certain ways".
You have a good point otherwise, but Go is considered memory safe anyway. And it probably makes sense that it is, since the chances of exploitation due to memory safety issues caused by races in Go are infinitesimal. It’s not at all fair to compare to the exploited-all-the-time issues of C/C++ (when you make the mistake of compiling with something other than Fil-C)
Basically, functional languages make it easier to write code that is safe. But they aren't necessarily the fastest or the easiest to deal with. Erlang and related languages are a good example. And they are popular for good reasons.
Java got quite a few things right but it took a while for it to mature. Modern day Java is quite a different beast than the first versions of Java. The Thread class, API, and the language have quite a few things in there that aren't necessarily that great of an idea. E.g. the synchronized keyword might bite you if you are trying to use the new green threads implementation (you'll get some nice deadlocks if you block the one thread you have that does everything). The modern java.concurrent package is implemented mostly without it.
Of course people that know their history might remember that green threads are actually not that new. Java did not actually support real threads until v1.1. Version 1.0 only had green threads. Those went out of fashion for about two decades and then came back with recent versions. And now it does both. Which is dangerous if you are a bit fuzzy on the difference. It's like putting spoilers on your fiesta. Using green threads because they are "faster" is a good sign that you might need to educate yourself and shut up.
On the JVM, if you want to do concurrent and parallel stuff, Scala and Kotlin might be better options. All the right primitives are there in the JVM of course. And Java definitely gives you access to all it. But it also has three decades of API cruft and a conservative attitude about keeping backwards compatible with all of that. And not all of it was necessarily that all that great. I'm a big fan of Kotlin's co-routine support that is rooted in a lot of experience with that. But that's subjective of course. And Scala-ists will probably insist that Scala has even better things. And that's before we bring up things like Clojure.
Go provides a good balance between ease of use / simplicity and safety. But it has quite a few well documented blind spots as well. I'm not that big of a fan but I appreciate it for what it is. It's actually a nice choice for people that aren't well versed in this topic and it naturally nudges people in a direction where things probably will be fine. Rust is a lot less forgiving and using it will make you a great engineer because your code won't even compile until you properly get it and do it right. But it won't necessarily be easy (humbled by experience here).
With languages the popular "if you have a hammer everything looks like a nail" thing is very real. And stepping out of your comfort zone and realizing that other tools are available and might be better suited to what you are trying to do is a good skill to have.
IMHO python is actually undervalued. It was kind of shit at all of this for a long time. But they are making a lot of progress modernizing the language and platform and are addressing its traditional weaknesses. Better interpreting and jit performance, removing the GIL, async support that isn't half bad, etc. We might wake up one day and find it doing a lot of stuff that we'd traditionally use JVM/GO/Rust for a few years down the line. Acknowledging weaknesses and addressing those is what I'm calling out here as a very positive thing. Oddly, I think there are a lot of python people that are a bit conflicted about progress like this. I see the same with a lot of old school Java people. You get that with any language that survives that long.
Note how I did not mention C/C++ here so far. There's a lot of it out there. But if you care about safety, you should probably not go near it. I don't care how disciplined you are. Your C/C++ code has bugs. Any insistence that it doesn't just means you haven't found them yet. Possibly because you are being sloppy looking for them. Does it even have tests? There are whole classes of bugs that we can prevent with modern languages and practices. It's kind of negligent and irresponsible not to. There are attempts to make C++ better of course.
The issue with Python isn't just the GIL and lack of support for concurrency. It uses dynamic types (i.e. variant types) for everything. That's way too slow, it means every single variable access must go through a dispatch step. About the only thing Python has going for it is the easy FFI with C-like languages.
Safe Rust doesn't seem that limited to me.
I don't think any of the C# work I do wouldn't be possible in Rust, if we disregard the fact that the rest of the team don't know Rust.
Most of the programs you eliminate when you have these "onerous" requirements like memory safety are nonsense, they either sometimes didn't work or had weird bugs that would be difficult to understand and fix - sometimes they also had scary security implications like remote code execution. We're better off without them IMNSHO.
Go (and previously Swift) fails at this. There data races can result in UB and thus break memory safety
I worry about the Win95-era "Microsoft Pragmatism" at work and a concrete example which comes to mind is nullability. In the nice modern software I often work on I can say some function takes a string and in that program C# will tell me that's not allowed to be null, it has to be an actual string - a significant engineering benefit. But, the CLR does not enforce such rules, so that function may still receive a null instead e.g. if called by some ten year old VB.NET code which has no idea about "nullability" and so just fills out a null for that parameter anyway.
Of course the CLR memory model might really be set in stone and 100% proof against such problems, but I haven't seen anything to reassure me as I did for Java and I fear that if it were convenient for Windows to not quite do that work they would say eh, good enough.
For a comparison, the x86 has what that document calls TSO, a very strict "free" ordering (in fact you pay all the time, but, you can't opt out so in that sense it's free to get this ordering on Intel) so 1990s C++ written for Windows just assumes volatile means you get memory ordering -- even though that's not what that means. If you compile brand new code for x86 on Microsoft's compilers today you get the exact same promise, but if you target their ARM platforms you don't get that because it would be expensive so, too bad.
It's saying the opposite – that if you want memory safety, thread safety is a requirement – and Java and C# refute it.
No, they don't. They're using a different meaning for "thread safety" that's more useful in context since they do ensure data race safety - which is the only kind of thread safety OP is talking about. By guaranteeing data race safety as a language property, Java and C# are proving OP's point, not refuting it.
Indeed, you're correct, I interpreted the implications in reverse.
That's a too low bar to clear to call it safe.
The bad news ought to be obvious, this "goal" is not achievable, it's a fantasy that somehow we should be able to see the future, divine that some value stored won't be needed in the future and thus we don't need to store it. Goals like "We shouldn't store things we can't even refer to" are already solved in languages used today, so a goal to "not have memory leaks" refers only to that unachievable fantasy.
- The above is true
- If I'm writing something using a systems language, it's because I care about performance details that would include things like "I want to spawn and curate threads."
- Relative to the borrow-checker, the Rust thread lifecycle static typing is much more complicated. I think it is because it's reflecting some real complexity in the underlying problem domain, but the problem stands that the description of resource allocation across threads can get very hairy very fast.
The same memory corruption gotchas caused by threads exist, regardless of whether there is a borrow checker or not.
Rust makes it easier to work with non-trivial multi-threaded code thanks to giving robust guarantees at compile time, even across 3rd party dependencies, even if dynamic callbacks are used.
Appeasing the borrow checker is much easier than dealing with heisenbugs. Type system compile-time errors are a thing you can immediately see and fix before problems happen.
OTOH some racing use-after-free or memory corruption can be a massive pain to debug, especially when it may not be possible to produce in a debugger due to timing, or hard to catch when it happens when the corruption "only" mangles the data instead of crashing the program.
This is an aesthetics argument more than anything else, but I don't think the type theory around threads and memory safety in Rust is as "cooked" as single-thread borrow checking. The type assertions necessary around threads just get verbose and weird. I expect with more time (and maybe a new paradigm after we've all had more time to use Rust) this is a solvable problem, but I personally shy away from Rust for multi-threaded applications because I don't want to please the type-checker.
Borrow checking is orthogonal to threads.
You may be referring to the difficulty satisfying the 'static liftime (i.e. temporary references are not allowed when spawning a thread that may live for an arbitrarily long time).
If you just spawn an independent thread, there's no guarantee that your code will reach join(), so there's no guarantee that references won't be dangling. The scoped threads API catches panics and ensures the thread will always finish before references given to it expire.
https://doc.rust-lang.org/stable/std/thread/fn.scope.html
But really, that first type signature is not very complex. It can get far, far, far worse. That’s just what happens when you encode things in types.
(It reads as “spawn is a function that accepts a closure that returns a type T. It returns a JoinHandle that also wraps a T. Both the closure and the T must be able to be sent to another thread and have a static lifetime.”)
A memory safe, managed language doesn't become unsafe just because you have a race condition in a program.
Like, say, reading and writing several related shared variables without a mutex.
Say that the language ensures that the reads and writes themselves of these word-sized variables are safe without any lock, and that memory operations and reclamation of memory are thread safe: there are no low-level pointers (or else only as an escape hatch that the program isn't using).
The rest is your bug; the variable values coming out of sync with each other, not maintaining the invariant among their values.
It could be the case that a thread-unsafe program breaks a managed run-time, but not an unvarnished truth.
A managed run-time could be built on the assumption that the program will not create two or more threads such that those threads will invoke concurrent operations on the same objects. E.g. a managed run time that needs a global interpreter lock, but which is missing.
The author's point is that Go is not a memory safe language according to that distinction.
There are values that are a single "atomic" write in the language semantics (interface references, slices) that are implemented with multiple non-atomic writes in the compiler/runtime. The result is that you can observe a torn write and break the language's semantics.
If the language and its runtime let me break their invariant, then that's their bug, not mine. This is the fundamental promise of type-safe languages: you can't accidentally break the language abstraction.
> It could be the case that a thread-unsafe program breaks a managed run-time, but not an unvarnished truth.
I demonstrated that the Go runtime is such a case, and I think that should be considered a memory safety violation. Not sure which part of that you disagree with...
No it isn't, because the torn write cannot have arbitrary effects that potentially break the program. It only becomes such if you rely on such a variable to establish an invariant about memory that's broken if a torn write occurs (such as by encoding a ptr+len in it), which is just silly. Don't do that!
tell that to the Go runtime, which relies on slices always being valid and not being able to create invalid ones.
There is no pedestrian safety without mandatory helmet laws.
There is no car safety without driving a tank.
The Wikipedia definition of memory safety is not the Go definition of memory safety, and in Go programs it is the Go definition of memory safety that matters.
The program in the article is obviously racy according to the Go language spec and memory model. So this is all very much tilting at windmills.
(But also, it'd be kind of silly for every language to make up their own definition of memory safety. Then even C is memory safe, they just have to define it the right way. ;)
No comments yet