24 ms·
There is no memory safety without thread safety
- PatriciaKim 1y ago[dead]
- jchw 1y agoThis comes up now and again, somewhat akin to the Rust soundness hole issue. To be fair, it is a legitimate issue, and you could definitely cause it by accident, which is more than I can say about the Rust soundness hole(s?), which as far as I know are basically incomprehensible and about as likely to come across naturally as guessing someone's private key. That said in many years of using Go in production I don't think I've ever come across a situation where the exact requirements to cause this bug have occurred. Uber has talked a lot about bugs in Go code. This article is useful to understand some of the practical problems facing Go developers actually wind up being, particularly the table at the bottom summarizing how common each issue is. https://www.uber.com/en-US/blog/data-race-patterns-in-go/ 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 https://www.youtube.com/watch?v=K_q-xnYRugQ
- bombela 1y agoIt took months to finally solve a data race in Go. No race detector would see anything. Nobody understood what was happening. It ultimately resulted in a loop counter overflowing, which recomputed the same thing a billion of time (but always the same!). So the visible effect was a request would randomly take 3 min instead of 100ms. I ended up using perf in production, which indirectly lead me to understand the data race. I was called in to help the team because of my experience debugging the weirdest things as a platform dev. Because of this I was exposed to so many races in Go, from my biased point of view, I want Rust everywhere instead. But I guess I am putting myself out of a job? ;)
- jchw 1y agoI think the true answer is that the moment you have to do tricky concurrency in Go, it becomes less desirable. I think that Go is still better at tricky concurrency than C, though there are some downsides too (I think it's a bit easier to sneak in a torn read issue in Go due to the presence of fat pointers and slice headers everywhere.) Go is really good at easy concurrency tasks, like things that have almost no shared memory at all, "shared-nothing" architectures, like a typical web server. Share some resources like database handles with a sync.Pool and call it a day. Go lets you write "async" code as if it were sync with no function coloring, making it decidedly nicer than basically anything in its performance class for this use case. Rust, on the other hand, has to contend with function coloring and a myriad of seriously hard engineering tasks to deal with async issues. Async Rust gets better every year, but personally I still (as of last month at least) think it's quite a mess. Rust is absolutely excellent for traditional concurrency, though. Anything where you would've used a mutex lock, Rust is just way better than everything else. It's beautiful. But I struggle to be as productive in Rust as I am in Go, because Rust, the standard library, and its ecosystem gives the programmer so much to worry about. It sometimes reminds me of C++ in that regard, though it's nowhere near as extremely bad (because at least there's a coherent build system and package manager.) And frankly, a lot of software I write is just boring, and Go does fine for a lot of that. I try Rust periodically for things, and romantically it feels like it's the closest language to "the future", but I think the future might still have a place for languages like Go.
- zozbot234 1y ago> And frankly, a lot of software I write is just boring, and Go does fine for a lot of that. I try Rust periodically for things, and romantically it feels like it's the closest language to "the future", but I think the future might still have a place for languages like Go. It's not so much about being "boring" or not; Rust does just fine at writing boring code once you get familiar with the boilerplate patterns (Real-world experience has shown that Rust is not really at a disadvantage wrt. productivity or iteration speed). There is a case for Golang and similar languages, but it has to do with software domains where there literally is no viable alternative to GC, such as when dealing with arbitrary, "spaghetti" reference graphs. Most programs aren't going to look like that though, and starting with Rust will yield a higher quality solution overall.
- qcnguy 1y agoWhat do Uber mean in that article when they say that Go programs "expose 8x more concurrency compared to Java microservices"? They're using the word concurrency as if it were a countable noun.
- wmf 1y agoIf the Java version creates 4 concurrent tasks (could be threads, fibers, futures, etc.) but the Go version creates 32 goroutines, that's 8x the concurrency.
- ameliaquining 1y agoI think it's also worth noting that Rust's maintainers acknowledge its various soundness holes as bugs that need to be fixed. It's just that some of them, like https://github.com/rust-lang/rust/issues/25860 https://github.com/rust-lang/rust/issues/25860 (which I assume you're referring to), need major refactors of certain parts of the compiler in order to fix, so it's taking a while.
- ralfj 1y agoYeah, I can totally believe that this is not a big issue in practice. But I think terms like "memory safety" should have a reasonably strict meaning, and languages that go the extra mile of actually preventing memory corruption even in concurrent programs (which is basically everything typically considered "memory safe" except Go) should not be put into the same bucket as languages that decide not to go through this hassle.
- sethammons 1y agoThat Uber article is fantastic. I believe Go fixed the first example recently. We had a rule at my last gig: avoid anonymous functions and always recover from them.
- advisedwang 1y agoWow that's a really big gotcha in go! To be fair though, go has a big emphasis on using its communication primitives instead of directly sharing memory between goroutines [1]. [1] https://go.dev/blog/codelab-share https://go.dev/blog/codelab-share
- zozbot234 1y agoReal-world golang programs share memory all the time, because the "share by communicating" pattern leads to pervasive logical problems, i.e. "safe" race conditions and "safe" deadlocks.
- jrockway 1y agoI am not sure sync.Mutex fixes either of these problems. Press C-\ on a random Go server that's been up for a while and you'll probably find 3000 goroutines stuck on a Lock() call that's never going to return. At least you can time out channel operations: select { case <-ctx.Done(): return context.Cause(ctx) case msg := <-ch: ... }
- TheDong 1y agoEven if you use channels to send things between goroutines, go makes it very hard to do so safely because it doesn't have the idea of sendable types, ownership, read-only references, and so on. For example, is the following program safe, or does it race? func processData(lines <-chan []byte) { for line := range lines { fmt.Printf("processing line: %v\n", line) } } func main() { lines := make(chan []byte) go processData(lines) var buf bytes.Buffer for range 3 { buf.WriteString("mock data, assume this got read into the buffer from a file or something") lines <- buf.Bytes() buf.Reset() } } The answer is of course that it's a data race. Why? Because `buf.Bytes()` returns the underlying memory, and then `Reset` lets you re-use the same backing memory, and so "processData" and "main" are both writing to the same data at the same time. In rust, this would not compile because it is two mutable references to the same data, you'd either have to send ownership across the channel, or send a copy. In go, it's confusing. If you use `bytes.Buffer.ReadBytes("\n")` you get a copy back, so you can send it. Same for `bytes.Buffer.String()`. But if you use `bytes.Buffer.Bytes()` you get something you can't pass across a channel safely, unless you also never use that bytes.Buffer again. Channels in rust solve this problem because rust understands "sending" and ownership. Go does not have those things, and so they just give you a new tool to shoot yourself in the foot that is slower than mutexes, and based on my experience with new gophers, also more difficult to use correctly.
- tptacek 1y agoThis is a canard. What's happening here, as happens so often in other situations, is that a term of art was created to describe something complicated; in this case, "memory safety", to describe the property of programming languages that don't admit to memory corruption vulnerabilities, such as stack and heap overflows, use-after-frees, and type confusions. Later, people uninvolved with the popularization of the term took the term and tried to define it from first principles, arriving at a place different than the term of art. We saw the same thing happen with "zero trust networking". The fact is that Go doesn't admit memory corruption vulnerabilities, and the way you know that is the fact that there are practically zero exploits for memory corruption vulnerabilities targeting pure Go programs, despite the popularity of the language. Another way to reach the same conclusion is to note that this post's argument proves far too much; by the definition used by this author, most other higher-level languages (the author exempts Java, but really only Java) also fail to be memory safe. Is Rust "safer" in some senses than Go? Almost certainly. Pure functional languages are safer still. "Safety" as a general concept in programming languages is a spectrum. But "memory safety" isn't; it's a threshold test. If you want to claim that a language is memory-unsafe, POC || GTFO.
- kllrnohj 1y ago> in this case, "memory safety", to describe the property of programming languages that don't admit to memory corruption vulnerabilities, such as [..] type confusions > The fact is that Go doesn't admit memory corruption vulnerabilities Except it does. This is exactly the example in the article. Type confusion causes it to treat an integer as a pointer & deference it. This then trivially can result in memory corruption depending on the value of the integer. In the example the value "42" is used so that it crashes with a nice segfault thanks to lower-page guarding, but that's just for ease of demonstration. There's nothing magical about the choice of 42 - it could just as easily have been any number in the valid address space.
- dboreham 1y agoEveryone knows that there's something very magical about the choice of 42.
- 1y ago
- 20k 1y ago[flagged]
- philosophty 1y agoIsn't it funny how anything you don't understand very well can seem weird?
- ameliaquining 1y agoWhat precisely are you accusing your interlocutor of not understanding?
- philosophty 1y agoWhy people with vastly more skill and experience programming and writing programming languages made the decisions they did.
- henry700 1y agoSkill? Go? With the amount of mistakes piling up over the years comparable to PHP at this point? Really??
- philosophty 1y agoThese guys have each forgotten 10x than you know about programming. https://swtch.com/~rsc/ https://swtch.com/~rsc/ https://en.wikipedia.org/wiki/Ken_Thompson https://en.wikipedia.org/wiki/Ken_Thompson https://en.wikipedia.org/wiki/Robert_Griesemer https://en.wikipedia.org/wiki/Robert_Griesemer https://en.wikipedia.org/wiki/Rob_Pike https://en.wikipedia.org/wiki/Rob_Pike
- henry700 1y agoFair point. The tragedy is that it could’ve been much more. But that was never the goal...
- danbruc 1y agoNope. You can have programs without undefined behavior and still not have thread safety. In .NET, for example, writes to variables that are wider then the machine width or not aligned properly, are not guaranteed to be atomic. So if you assign some value to an Int128 variable, it will not be updated atomically - how could it, that is just beyond the capabilities of the processor - and therefore a different thread can observe a state where only half of the variable has been updated. No undefined behavior here but also sharing this variable between threads is not thread safe. And having the language synchronize all such writes - just in case some other thread might want tot look at it - is a performance disaster. And disallowing anything that might be a potential thread safety issue will give you a pretty limited language.
- kibwen 1y agoThe statement "there is no memory safety without thread safety" does not suggest that memory safety is sufficient to provide thread safety. Instead, it's just saying that if you want thread safety, then memory safety is a requirement.
- minitech 1y ago> Instead, it's just saying that if you want thread safety, then memory safety is a requirement. It's saying the opposite – that if you want memory safety, thread safety is a requirement – and Java and C# refute it.
- zozbot234 1y ago> Java and C# refute it. No, they don't. They're using a different meaning for "thread safety" that's more useful in context since they do ensure data race safety - which is the only kind of thread safety OP is talking about. By guaranteeing data race safety as a language property, Java and C# are proving OP's point, not refuting it.
- kibwen 1y ago> It's saying the opposite Indeed, you're correct, I interpreted the implications in reverse.
- kazinator 1y agoThis is false as a generality. A memory safe, managed language doesn't become unsafe just because you have a race condition in a program. Like, say, reading and writing several related shared variables without a mutex. Say that the language ensures that the reads and writes themselves of these word-sized variables are safe without any lock, and that memory operations and reclamation of memory are thread safe: there are no low-level pointers (or else only as an escape hatch that the program isn't using). The rest is your bug; the variable values coming out of sync with each other, not maintaining the invariant among their values. It could be the case that a thread-unsafe program breaks a managed run-time, but not an unvarnished truth. A managed run-time could be built on the assumption that the program will not create two or more threads such that those threads will invoke concurrent operations on the same objects. E.g. a managed run time that needs a global interpreter lock, but which is missing.
- qcnguy 1y agoThe author knows that. His point is that Go doesn't work that way because it uses greater-than-word-sized values that can suffer torn writes leading to segfaults in some cases.
- munificent 1y ago> A memory safe, managed language doesn't become unsafe just because you have a race condition in a program. The author's point is that Go is not a memory safe language according to that distinction. There are values that are a single "atomic" write in the language semantics (interface references, slices) that are implemented with multiple non-atomic writes in the compiler/runtime. The result is that you can observe a torn write and break the language's semantics.
- dodobirdlord 1y agoIf the variables are word-sized, sure. But what if they are larger? Now a race condition between one thread writing and another thread reading or writing a variable is a memory safety issue.
- kazinator 1y ago
- qcnguy 1y agoThe point being made is sound, but I can never escape the feeling that most concurrency discussion in programming language theory is ignoring the elephant in the room. The concurrency bugs that matter in most apps are all happening inside the database due to lack of proper locking, transactions or transactional isolation. PL theory ignores this and so things like Rust's approach to race freedom ends up not mattering much outside of places like kernels. A Rust app can avoid use of unsafe entirely and still be riddled with race conditions because all the data that matters is in an RDBMS and someone forgot a FOR UPDATE in their SELECT clause.
- layer8 1y agoWhat’s worse, even if you use proper transactions for everything, it’s hard to reason about visibility and data races when performing SQL across tables, or multiple dependent SQL statements within a transaction.
- munificent 1y agoI agree with the author's claim that you need thread safety for memory safety. But I don't agree with: > I will argue that this distinction isn’t all that useful, and that the actual property we want our programs to have is absence of Undefined Behavior. There is plenty of undefined behavior that can't lead to violating memory safety. For example, in many languages, argument evaluation order is undefined. If you have some code like: foo(print(1), print(2)); In some languages, it's undefined as to whether "1" is printed before "2" or vice versa. But there's no way to violate memory safety with this. I think the only term the author needs here is "memory safety", and they correctly observe that if the language has threading, then you need a memory model that ensures that threads can't break your memory safety. Go lacks that. It seems to be a rare problem in practice, but if you want guarantees, Go doesn't give you them. In return, I guess it gives you slightly faster execution speed for writes that it allows to potentially be torn.
- zozbot234 1y agoThat's "unspecified" not "undefined". "Undefined behavior" literally means "anything goes", so any program that invokes it is broken by definition.
- bigstrat2003 1y agoThat is not true, that is a very specific definition of UB which C developers (among others) favor. That doesn't mean that another language can't say "this is undefined behavior" without all the baggage that accompanies the term in C.
- zozbot234 1y agoIt's literally how the term "UB" is defined, and understood by experts. Why would anyone want to say "undefined" when they really mean "unspecified"? That's just confusing.
- bigstrat2003 1y agoNo, it's how one very specific community of experts understands it. It is not some kind of universal law of definition that it must mean that always and everywhere. As far as what is confusing, that is a matter of perspective. I think it is confusing (to put it mildly) that the C community has chosen to use "undefined behavior" to mean "it must never happen, and anything goes if it does". That is extremely counterintuitive, and only makes sense to those who live and breathe that world. So if the standard is to be "avoiding confusion", then we better change the definition used by the C community ASAP.
- nromiun 1y agoI bet not even 5% of all programs are multi-threaded, or even concurrent. Memory safety is a much bigger problem.
- norir 1y agoThe sad thing is that most languages with threads have a default of global variables and unrestricted shared memory access. This is the source of the vast majority of data corruption and races. Processes are generally a better concurrency model than threads, but they are unfortunately too heavyweight for many use cases. If we defaulted to message passing all required data to each thread (either by always copying or tracking ownership to elide unnecessary copying), most of these kinds of problems would go away. In the meantime, we thankfully have agency and are free to choose not to use global variables and shared memory even if the platform offers them to us.
- zozbot234 1y agoMessage passing can easily lead to more logical errors (such as race conditions and/or deadlocks) than sharing memory directly with properly synchronized access. It's not a silver bullet.
- umpalumpaaa 1y ago100%. 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.
- kibwen 1y ago> The sad thing is that most languages with threads have a default of global variables and unrestricted shared memory access. This is the source of the vast majority of data corruption and races. Processes are generally a better concurrency model than threads Modern languages have the option of representing thread-safety in the type system, e.g. what Rust does, where working with threads is a dream (especially when you get to use structured concurrency via thread::scope). People tend to forget that Rust's original goal was not "let's make a memory-safe systems language", it was "let's make a thread-safe systems language", and memory safety just came along for the ride.
- chadaustin 1y agoEvery time this conversation comes up, I'm reminded of my team at Dropbox, where it was a rite of passage for new engineers to introduce a segfault in our Go server by not synchronizing writes to a data structure. Swift has (had?) the same issue and I had to write a program to illustrate that Swift is (was?) perfectly happy to segfault under shared access to data structures. Go has never been memory-safe (in the Rust and Java sense) and it's wild to me that it got branded as such.
- commandersaki 1y agoTo put things in perspective, I posit to you, how many memory unsafe things can you do in Go that isn’t a variant of the same thing? Or put another way what is the likelihood that a go program is memory unsafe?
- junebash 1y agoSwift is in the process of fixing this, but it’s a slow and painful transition; there’s an awful lot of unsafe code in the wild that wasn’t unsafe until recently.
- cosmic_cheese 1y agoOne of the biggest hurdles is just getting all the iOS/macOS/etc APIs up to speed with the thread safety improvements. It won’t make refactoring all that application code any easier, but as things stand even if you’ve done that, you’re going to run into problems anywhere your code makes contact with UI code because there’s a lot of AppKit and UIKit that have yet to make the transition.
- RetpolineDrama 1y agoSwift 6 is only painful if you wrote a ton of terrible Swift 5, and even then Swift 5 has had modes where you could gracefully adopt the Swift 6 safety mechanisms for a long time (years?) ~130k LoC Swift app was converted from 5 -> 6 for us in about 3 days.
- 1y ago
- shadowgovt 1y agoThis is, in my mind, the trickiest issue with Rust right now as a language project, to wit: - The above is true - If I'm writing something using a systems language, it's because I care about performance details that would include things like "I want to spawn and curate threads." - Relative to the borrow-checker, the Rust thread lifecycle static typing is much more complicated. I think it is because it's reflecting some real complexity in the underlying problem domain, but the problem stands that the description of resource allocation across threads can get very hairy very fast.
- pornel 1y agoI don't know what you're referring to. Rust's threads are OS threads. There's no magic runtime there. The same memory corruption gotchas caused by threads exist, regardless of whether there is a borrow checker or not. Rust makes it easier to work with non-trivial multi-threaded code thanks to giving robust guarantees at compile time, even across 3rd party dependencies, even if dynamic callbacks are used. Appeasing the borrow checker is much easier than dealing with heisenbugs. Type system compile-time errors are a thing you can immediately see and fix before problems happen. OTOH some racing use-after-free or memory corruption can be a massive pain to debug, especially when it may not be possible to produce in a debugger due to timing, or hard to catch when it happens when the corruption "only" mangles the data instead of crashing the program.
- shadowgovt 1y agoIt's not the runtime; it's how the borrow-checker interoperates with threads. This is an aesthetics argument more than anything else, but I don't think the type theory around threads and memory safety in Rust is as "cooked" as single-thread borrow checking. The type assertions necessary around threads just get verbose and weird. I expect with more time (and maybe a new paradigm after we've all had more time to use Rust) this is a solvable problem, but I personally shy away from Rust for multi-threaded applications because I don't want to please the type-checker.
- pornel 1y ago
- chc4 1y agoThis is one of the things that I'm also looking on at Zig like a slow moving car crash about: they claim they are memory safe (or at least "good enough" memory safe if you use the safe optimization level, which is it's own discussion), but they don't have the equivalent to Rust's Send/Sync types. It just so happens that in practice no one was writing enough concurrent Zig code to get bitten by it a lot, I guess...except that now they're working on bringing back first-class async support to the language, which will run futures on other threads and presumably a lot of feet are going to be fired at once that lands.
- ameliaquining 1y agoIIUC even single-threaded Zig programs built with ReleaseSafe are not guaranteed to be free of memory corruption vulnerabilities; for example, dereferencing a pointer to a local variable that's no longer alive is undefined behavior in all optimization modes.
- skeezyboy 1y agowell just dont do it then
- ameliaquining 1y agoThat's also the standard advice in C and C++, and yet, people screw it up frequently enough to merit a CWE category: https://cwe.mitre.org/data/definitions/562.html https://cwe.mitre.org/data/definitions/562.html
- cibyr 1y agoZig's claims of memory safety are a bad joke. Sure, it's easier to avoid memory safety bugs in Zig than it is in C, but that's also true of C++ (which nobody claims is a memory safe language).
- Thaxll 1y agoGo is memory safe by the most common definition, does not matter if you have segfault in some scenario. How many exploits or security issues have there been related to data race on dual word values? I work with Go for the last 10 years and I never heard of such issues. Not a single time.
- zozbot234 1y agoThe most common definition of memory safe is literally "cannot segfault" (unless invoking some explicitly unsafe operation - which is not the case here unless you think the "go" keyword should be unsafe).
- Thaxll 1y agoI don't know the NSA with their white house paper about memory safe language mentioned Go, maybe you should tell that there are wrong.
- Sesse__ 1y agohttps://en.wikipedia.org/wiki/Argument_from_authority https://en.wikipedia.org/wiki/Argument_from_authority
- Thaxll 1y agoSo NSA does not have the relevant authority to qualify a language as memory safe, is it what you're saying? The document is backed by foreign government as well. https://media.defense.gov/2023/Dec/06/2003352724/-1/-1/0/THE-CASE-FOR-MEMORY-SAFE-ROADMAPS-TLP-CLEAR.PDF https://media.defense.gov/2023/Dec/06/2003352724/-1/-1/0/THE...
- SkiFire13 1y ago> NSA does not have the relevant authority You did not even read the link the parent comment provided and are continuing with the same flawed argument.
- Mawr 1y agoThere is no house safety without nuclear warhead detonation safety. There is no pedestrian safety without mandatory helmet laws. There is no car safety without driving a tank.
- FiloSottile 1y agoI have never seen real Go code (i.e. not code written purposefully to be exploitable) that was exploitable due to a data race. This doesn’t prove a negative, but is probably a good hint that this risk is not something worth prioritizing for Go applications from a security point of view. Compare this with C/C++ where 60-75% of real world vulnerabilities are memory safety vulnerabilities. Memory safety is definitely a spectrum, and I’d argue there are diminishing returns.
- stouset 1y agoMaintenance in general is a burden much greater than CVEs. Exploits are bad, certainly, but a bug not being exploitable is still a bug that needs to be fixed. With maintenance being a "large" integer multiple of initial development, anything that brings that factor down is probably worth it, even if it comes at an incremental cost in getting your thing out the door.
- deleted 1y ago[deleted]
- 9rx 1y ago> but a bug not being exploitable is still a bug that needs to be fixed. Do you? Not every bug needs to be fixed. I've never see a data race bug in documented behaviour make it past initial development. I have seen data races in undocumented behaviour in production, but as it isn't documented, your program doesn't have to do that! It doesn't matter if it fails. It wasn't a concern of your program in the first place. That is still a problem if an attacker uses undocumented behaviour to find an exploit, but when it is benign... Oh well. Who cares?
- LtWorf 1y agoI have! What do i win?
- EE84M3i 1y agoWas it open source? Would be interested to know more.
- crawshaw 1y agoMemory safety is a big deal because many of the CVEs against C programs are memory safety bugs. Thread safety is not a major source of CVEs against Go programs. It’s a nice theoretical argument but doesn’t hold up in practice.
- nine_k 1y agoA typical memory safety issue in a C program is likely to generate an RCE. A thread-safety issue that leads to a segfault can likely only lead to a DoS attack, unpleasant but much less dangerous. A race condition can theoretically lead to more powerful attacks, but triggering it should be much harder.
- SkiFire13 1y agoA thread-safety issue does not always lead to a segfault. Here it did because the address written was 42, but if you somehow manage to obtain the address of some valid value then you could read from that instead, and not cause an immediate segfault. I agree with the sentiment that data races are generally harder to exploit, but it _is possible_ to do.
- stouset 1y agoA CVE is worse, but a threading bug resulting in corrupted data or a crash is still a bug that needs someone to triage, understand, and fix.
- crawshaw 1y agoBut it's not why I stopped writing C programs. It's just a bug and I create and fix a dozen bugs every day. Security is the only argument for memory safety that moves mountains.
- okanat 1y agoIt depends on what threads can do. Threads share memory with other threads and you can corrupt the data structure to force the other thread to do an unsafe / invalid operation. It can be as simple as changing the size of a vector from one thread while the other one accesses it. When executed sequentiality, the operations are safe. With concurrency all bets are off. Even with Go. Hence the argument in TFA.
- loeg 1y agoAre we still have semantic fights about what exactly memory safety means? Why?
- sapiogram 1y agoBecause people think Golang is immune to bugs that it's not immune from.
- pizlonator 1y agoFalse. Java got this right. Fil-C gets it right, too. So, there is memory safety without thread safety. And it’s really not that hard. Memory safety is a separate property unless your language chooses to gate it on thread safety. Go (and some other languages) have such a gate. Not all memory safe languages have such a gate.
- glowcoil 1y agoI would recommend reading beyond the title of a post before leaving replies like this, as your comment is thoroughly addressed in the text of the article: > At this point you might be wondering, isn’t this a problem in many languages? Doesn’t Java also allow data races? And yes, Java does allow data races, but the Java developers spent a lot of effort to ensure that even programs with data races remain entirely well-defined. They even developed the first industrially deployed concurrency memory model for this purpose, many years before the C++11 memory model. The result of all of this work is that in a concurrent Java program, you might see unexpected outdated values for certain variables, such as a null pointer where you expected the reference to be properly initialized, but you will never be able to actually break the language and dereference an invalid dangling pointer and segfault at address 0x2a. In that sense, all Java programs are thread-safe. And: > Java programmers will sometimes use the terms “thread safe” and “memory safe” differently than C++ or Rust programmers would. From a Rust perspective, Java programs are memory- and thread-safe by construction. Java programmers take that so much for granted that they use the same term to refer to stronger properties, such as not having “unintended” data races or not having null pointer exceptions. However, such bugs cannot cause segfaults from invalid pointer uses, so these kinds of issues are qualitatively very different from the memory safety violation in my Go example. For the purpose of this blog post, I am using the low-level Rust and C++ meaning of these terms. Java is in fact thread-safe in the sense of the term used in the article, unlike Go, so it is not a counterexample to the article's point at all.
- pizlonator 1y ago> I would recommend reading beyond the title of a post before leaving replies like this, as your comment is thoroughly addressed in the text of the article: The title is wrong. That's important. > Java is in fact thread-safe in the sense of the term used in the article The article's notion of thread safety is wrong. Java is not thread safe by construction, but it is memory safe.
- kiitos 1y ago> To see what I mean by this, consider this program written in Go, which according to Wikipedia is memory-safe: The Wikipedia definition of memory safety is not the Go definition of memory safety, and in Go programs it is the Go definition of memory safety that matters. The program in the article is obviously racy according to the Go language spec and memory model. So this is all very much tilting at windmills.
- ralfj 1y agoCan you point me to the Go definition of memory safety? I searched all over their website, and couldn't find any. (But also, it'd be kind of silly for every language to make up their own definition of memory safety. Then even C is memory safe, they just have to define it the right way. ;)
- deleted 1y ago[deleted]
- kiitos 1y agoFor the purposes of this discussion, sure: https://go.dev/ref/mem https://go.dev/ref/mem Relevant bit for the OP is probably: A data race is defined as a write to a memory location happening concurrently with another read or write to that same location, unless all the accesses involved are atomic data accesses as provided by the sync/atomic package. Which describes exactly what is happening in the OP's program: func repeat_get() { for { x := globalVar // <-- unsynchronized read of globalVar x.get() // <-- unsynchronized call to Thing.get() } } By itself this isn't a problem, these are just reads, and you don't need synchronization for concurrent reads by themself. The problem is introduced here: func repeat_swap() { var myval = 0 for { globalVar = &Ptr { val: &myval } // <-- unsynchronized write to globalVar globalVar = &Int { val: 42 } // <-- unsynchronized write to globalVar } } func main() { go repeat_get() // <-- one goroutine is doing unsynchronized reads repeat_swap() // <-- another goroutine is doing unsynchronized writes } Just a (chef's kiss) textbook example of a data race, and a clearly unsound Go program. I don't know how or why the OP believes "this program ... [is] according to Wikipedia memory-safe" -- it very clearly is not. But, you know, I think everyone here is basically talking past each other.
- corysama 1y agoThis is why I’m excited about https://www.hylo-lang.org/ https://www.hylo-lang.org/ as a new, statically-compiled language with all the safeties!
- singpolyma3 1y agoHonestly what I mostly want is to not have memory leaks. Which somehow stopped being a focus at some point
- Wowfunhappy 1y agoBecause we have so much memory no one cares if it leaks. <_<
- astrange 1y agoThis is harder than it looks as soon as you start counting abandoned memory (stuff that's still referenced but not actually used.)
- singpolyma3 1y agoWell if it's still referenced it's still used from my PoV
- astrange 1y agoIf it's never read again then it's not used.
- tialaramex 1y agoThe "good" news is that Bjarne Stroustrup is right there with you, Bjarne sees eliminating all memory leaks as a high priority for C++ and one of his main goals. The bad news ought to be obvious, this "goal" is not achievable, it's a fantasy that somehow we should be able to see the future, divine that some value stored won't be needed in the future and thus we don't need to store it. Goals like "We shouldn't store things we can't even refer to" are already solved in languages used today, so a goal to "not have memory leaks" refers only to that unachievable fantasy.
- codys 1y agoCuriously, Go itself is unclear about its memory safety on go.dev. It has a few references to memory safety in the FAQ (https://go.dev/doc/faq#Do_Go_programs_link_with_Cpp_programs https://go.dev/doc/faq#Do_Go_programs_link_with_Cpp_programs, https://go.dev/doc/faq#unions https://go.dev/doc/faq#unions) implying that Go is memory safe, but never defines what those FAQ questions mean with their statements about "memory safety". There is a 2012 presentation by Rob Pike (https://go.dev/talks/2012/splash.slide#49 https://go.dev/talks/2012/splash.slide#49) where it is stated that go is "Not purely memory safe", seeming to disagree with the more recent FAQ. What is meant by "purely memory safe" is also not defined. The Go documentation for the race detector talks about whether operations are "safe" when mutexes aren't added, but doesn't clarify what "safe" actually means (https://go.dev/doc/articles/race_detector#Unprotected_global_variable https://go.dev/doc/articles/race_detector#Unprotected_global...). The git record is similarly unclear. In contrast to the go project itself, external users of Go frequently make strong claims about Go's memory safety. fly.io calls Go a "memory-safe programming language" in their security documentation (https://fly.io/docs/security/security-at-fly-io/#application-security-appsec 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/ 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.
- dataflow 1y agoAm I missing something or is that bold claim obviously wrong on its face? This seems like a Go deficiency (lack of atomicity for it pointers), not some sort of law about programming languages. Can you violate memory safety in C# without unsafe{} blocks (or GCHandle/Marshal/etc.)? (No.) Can you write thread-unsafe code in C# without using unsafe{} blocks etc.? (Yes, just make your integers race.) Doesn't that contradict the claim that you can't have memory safety without thread safety?
- swiftcoder 1y agoI wish we had picked a better name than "thread safety". This is really more like "concurrency safety", since it applies even in the absence of hardware threads.
- layer8 1y agoThreads aren’t hardware, they are OS. Multihreading != multiprocessing.
- zozbot234 1y agoHardware threads are a thing.
- layer8 1y agoOther than in the sense of SMT (Hyper-Threading)? I don't think so. Threads are a software concept. One can distinguish between native (OS) threads and green (language-runtime) threads which may use a different context-switching mechanism. But that's more of a spectrum in terms of thread-safety; similar to how running multiple threads on a single CPU core without SMT, single CPU core with SMT, multiple CPU cores, with different possible CPU cache coherency guarantees, create a spectrum of possible thread-safety issues.
- deleted 1y ago[deleted]
- zozbot234 1y agoRuntime-switched tasks cannot lead to memory unsafety unless multiple OS threads are involved, because that's the only case where torn writes are possible. And a typical configuration of Go will not be running multiple OS threads unless multiple hardware threads (aka "logical cores", "virtual processors" etc.) are available.
- layer8 1y ago
- unit149 1y ago[dead]
- goodpoint 1y ago> safety is not binary, it is a spectrum, and on that spectrum Go is much closer to a typical safe language than to C That's a too low bar to clear to call it safe.
- alkonaut 1y agoAnd here I thought the type system and error handling were the two biggest Go warts. You’re now telling me their memory model is basically ”YOLO”?
- camgunz 1y agoI feel like I'm defending Go constantly these days. I don't even like Go! Go can already ensure "consistency of multi-word values": use whatever synchronization you want. If you don't, and you put a race into your code, weird shit will happen because torn reads/writes are fuckin weird. You might say "Go shouldn't let you do that", but I appreciate that Go lets me make the tradeoff myself, with a factoring of my choosing. You might not, and that's fine. But like, this effort to blow data races up to the level of C/C++ memory safety issues (this is what is intended by invoking "memory safety") is polemic. They're nowhere near the same problem or danger level. You can't walk 5 feet through a C/C++ codebase w/o seeing a memory safety issue. There are... zero Go CVEs resulting from this? QED. 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 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."
- dcsommer 1y agoUnsafety in a language is fine as long as it is clearly demarcated. The problem with Go's approach is there no clear demarcation of the unsafety, making reasoning about it much more difficult.
- camgunz 1y agoThe "go" keyword is that demarcation
- bobbylarrybobby 1y ago“go” being a necessary keyword even for benign operations makes its use an unsafety marker pointless; you end up needing to audit your entire codebase anyway. The whole point of demarcation is that you have a small surface area to go over with a fine-toothed comb.
- 1y ago
- aatd86 1y agoWhy does it segfault? Because you have not used a sufficiently clever value for the integer that wouldn't when used as an address? Just wondering. Realistically that would be quite rare since it is obvious that this is unprotected shared mutable access. But interesting that such a conversion without unsafe may happen. If it segfaults all the time though then we still have memory safety I guess. The article is interesting but I wish it would try to provide ideas for solutions then.