To be fair though, go has a big emphasis on using its communication primitives instead of directly sharing memory between goroutines [1].
select {
case <-ctx.Done():
return context.Cause(ctx)
case msg := <-ch:
...
}
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.
"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.
>
> 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.
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.
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.
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.
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.
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.
Compare to an innocent looking map operation, and it's not even in the same league.
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.
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.
I could also argue C is memory safe and all the exploits that have been made weren’t real C programs
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.
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.
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.
(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)
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.
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 )
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.
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.
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.
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.
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.
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.
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...
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.
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.
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.
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).
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).
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.
Memory safety is a much bigger problem.
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.
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.
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.
(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…
strong claim. care to back it up?
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.
~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.
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.
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...
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.
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.
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.
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.
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.
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.
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.
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?
Rust avoids all this entirely, by using its type system.
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.
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.
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.
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.
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.
[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.
> 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'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.
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.
Denial of service can absolutely be a security issue, as can any correctness bug if it leads to unintended behavior or corrupted data.
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 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.
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.
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.
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...
> 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.
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.
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.
> 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.)
Opposite really. I like rust because I can be care free and have fun.
I had to convince Go people that you can segfault with Go. Or you mean the language designers with using everybody?
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.
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.
Or put another way what is the likelihood that a go program is memory unsafe?
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).
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.
- 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.
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
... yikes. This is getting into "As easy to read as a C++ template" territory.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.”)
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.
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.
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.
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 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.
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.
> 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.
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.
There is no pedestrian safety without mandatory helmet laws.
There is no car safety without driving a tank.
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
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.
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)
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
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.
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. ;)
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.
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.
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.
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."
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.
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.
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.
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?
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.
That's a too low bar to clear to call it safe.
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.
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.
jchw•1d ago
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
bombela•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.
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? ;)
jchw•1d ago
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.
zozbot234•1d ago
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.
jchw•1d ago
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.
sophacles•1d ago
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.
zozbot234•1d ago
jchw•1d ago
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.
lossolo•7h ago
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?
josephg•23h ago
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.
Philpax•6h ago
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.
Mawr•1d ago
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
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.
sophacles•21h ago
brabel•16h ago
zozbot234•16h ago
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•8h ago
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•6h ago
I have written Rust for around 6 years now.
sophacles•5h ago
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).
ralfj•6h ago
Ar-Curunir•5h ago
bombela•1d ago
jchw•23h ago
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•6h ago
jchw•6h ago
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•5h ago
jchw•5h ago
zozbot234•5h ago
jchw•4h ago
When working on Go services it is nearly the last thing I am concerned about.
zozbot234•3h ago
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?
jchw•34m ago
Rust is surely good for when you are doing something vastly more complicated than boring web services, but if you try to write a database or message queue you are not going to pass Jepsen testing because you have a borrow checker present. Some of the most proven software in the world is written in programming languages with worse concurrency control than Go, like sqlite in C.
But, if you wanted to write something with super complex concurrency from scratch, you probably would opt to use Rust, because well, it's just good at that, and it probably is worth the up front and ongoing investment to entirely eliminate some classes of concurrency issues. But in those cases you need much more rigorous testing that will likely help to prevent menial concurrency bugs too, like running torture tests with race detection that try to ensure consistency guarantees hold up in all situations.
So are all Go programs of note just boring glue logic? Also no. I use tons of Go software every day that is a lot more than glue logic. Some examples include ESBuild, SyncThing, rclone, restic, and probably a bunch of other utilities of various shapes and sizes. People write databases and message queues and whatever else in Go too.
Still, yes most software is terribly boring. Most software is doing glue shit and basic CRUD operations and not much more. That doesn't mean that companies that write these kinds of software do nothing interesting, but even if they do, most of the software is going to be really god damned boring, because a lot of what we need to do is not super novel cutting edge stuff, and you don't rewrite a relational database or message queue system every single time you need one, you pick an off the shelf option and go on your way.
dev_l1x_be•16h ago
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•6h ago
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.
norir•1d ago
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
tomp•1d ago
so in reality, it's just "pick your own poison" to various degrees...
tialaramex•1d ago
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•21h ago
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•16h ago
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•18h ago
There's really no excuse for a modern PL to not have, at the very least, overflow detection by default.
devnullbrain•1d ago
jeffparsons•22h ago
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•16h ago
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)
kbolino•10h ago
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
astrange•18h ago
Thaxll•1d ago
swiftcoder•1d ago
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
jason_oster•1d ago
[^1]: https://www.reddit.com/r/ProgrammerTIL/comments/4tspsn/c_it_...
[^2]: https://stackoverflow.com/questions/69375375/is-it-safe-to-a...
wavemode•1d ago
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•1d ago
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.
qcnguy•1d ago
wmf•1d ago
ameliaquining•1d ago
ralfj•11h ago
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•11h ago
We had a rule at my last gig: avoid anonymous functions and always recover from them.