Hacker News

Top stories

Live mirror
30 storiesupdated just nowView source snapshot
  1. Platform-Independent SIMD in Go (go.dev)
    85comments
  2. Git-bug: Distributed, offline-first bug tracker embedded in Git (github.com/git-bug)
    61comments
  3. First Principles Thinking (sunilsadasivan.com)
    39comments
  4. U.S. appeals court upholds designation of Anthropic as supply chain risk (cnbc.com)
    221comments
  5. Classified Estimates Show the NSA Is Paying Billions to Test AI Models (washingtonsun.com)
    56comments
  6. Pentium II at 600Mhz with Voodoo 3 Emulated on 86Box with M6 Mac Mini (nyaa.sh)
    91comments
  7. F-Droid 2.0 (f-droid.org)
    399comments
  8. Ink and Switch Interactive Homepage (inkandswitch.com)
    21comments
  9. Dutch governments builds alternative for Microsoft based on NixOS (dawo.community)
    487comments
  10. Factorio that you can touch (factorio.com)
    21comments
  11. Allow Carriers on Planes (jefftk.com)
    177comments
  12. Show HN: Make cursed fonts like Times New Bastard (mitpit.com)
    116comments
  13. Amiga Screens: A Primer (datagubbe.se)
    18comments
  14. CVE-2025-13032: Entering and Breaking the Avast Antivirus Sandbox Part 2 (safateam.com)
    26comments
  15. Show HN: Whiteboard (YC W26) – An open-source IDE for thoughtful software design (github.com/devdotfast)
    126comments
  16. Boards of Casio (ambionix.com)
    20comments
  17. What About Rails? (jardo.dev)
    135comments
  18. Gravity Seems Holographic. What Does That Mean for Reality? (quantamagazine.org)
    43comments
  19. Why is the liver so weirdly regenerative? (dynomight.substack.com)
    262comments
  20. Jevmem – automatic project memory for Claude Code, built on Jev (github.com/avinash-jetwani)
    32comments
  21. Microsoft Abandons Personal AI Chatbot Race with Copilot Reboot (bloomberg.com)
    36comments
  22. The Test (tante.cc)
    43comments
  23. 2DWillNeverDie (2dwillneverdie.com)
    76comments
  24. Rails World 2026 Opening Keynote [video] (youtube.com)
    442comments
  25. Opus 5.5 is good at explainer videos (launchvideo.io)
    200comments
  26. Fearless SIMD v1.0 (linebender.org)
    48comments
  27. Toyota is taking the Corolla electric (electrek.co)
    742comments
  28. My weird new hobby: Wandering around Tokyo on Google Maps (ahmedhossamdev.com)
    171comments
  29. Two-tier encryption in the UK (macanorak.com)
    449comments
  30. Using LLMs to trace alchemical knowledge and decode 17th century letters (resobscura.substack.com)
    38comments

There is no memory safety without thread safety

456 pointsby 1y agoralfj.de
491 comments
1y agoHN ↗

This comes up now and again, somewhat akin to the Rust soundness hole issue. To be fair, it is a legitimate issue, and you could definitely cause it by accident, which is more than I can say about the Rust soundness hole(s?), which as far as I know are basically incomprehensible and about as likely to come across naturally as guessing someone's private key.

That said in many years of using Go in production I don't think I've ever come across a situation where the exact requirements to cause this bug have occurred.

Uber has talked a lot about bugs in Go code. This article is useful to understand some of the practical problems facing Go developers actually wind up being, particularly the table at the bottom summarizing how common each issue is.

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

1y agoHN ↗

It took months to finally solve a data race in Go. No race detector would see anything. Nobody understood what was happening.

It ultimately resulted in a loop counter overflowing, which recomputed the same thing a billion of time (but always the same!). So the visible effect was a request would randomly take 3 min instead of 100ms.

I ended up using perf in production, which indirectly lead me to understand the data race.

I was called in to help the team because of my experience debugging the weirdest things as a platform dev.

Because of this I was exposed to so many races in Go, from my biased point of view, I want Rust everywhere instead.

But I guess I am putting myself out of a job? ;)

1y agoHN ↗

I think the true answer is that the moment you have to do tricky concurrency in Go, it becomes less desirable. I think that Go is still better at tricky concurrency than C, though there are some downsides too (I think it's a bit easier to sneak in a torn read issue in Go due to the presence of fat pointers and slice headers everywhere.)

Go is really good at easy concurrency tasks, like things that have almost no shared memory at all, "shared-nothing" architectures, like a typical web server. Share some resources like database handles with a sync.Pool and call it a day. Go lets you write "async" code as if it were sync with no function coloring, making it decidedly nicer than basically anything in its performance class for this use case.

Rust, on the other hand, has to contend with function coloring and a myriad of seriously hard engineering tasks to deal with async issues. Async Rust gets better every year, but personally I still (as of last month at least) think it's quite a mess. Rust is absolutely excellent for traditional concurrency, though. Anything where you would've used a mutex lock, Rust is just way better than everything else. It's beautiful.

But I struggle to be as productive in Rust as I am in Go, because Rust, the standard library, and its ecosystem gives the programmer so much to worry about. It sometimes reminds me of C++ in that regard, though it's nowhere near as extremely bad (because at least there's a coherent build system and package manager.) And frankly, a lot of software I write is just boring, and Go does fine for a lot of that. I try Rust periodically for things, and romantically it feels like it's the closest language to "the future", but I think the future might still have a place for languages like Go.

1y agoHN ↗

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.

1y agoHN ↗

Rust can yield a higher quality solution, but we can't make a perfect solution, we can only approach perfection. If we want to go further, we could introduce formally-proven code, too. Personally I'm interested in the intersection of proof assistants and Rust, like creusot-rs, and have been investigating it.

But as much as I love LARPing about correctness (believe me I do,) it's just simply the case that we won't right perfect software and it's totally OK. It's totally OK that our software will have artificial limitations, like with Go, only accepting filenames that are valid UTF-8, or taking some unnecessary performance/latency hits, or perhaps even crashing in some weird ass edge case. There are very few domains in which correctness issues can't be tolerated.

I don't deal with domains that are truly mission critical, where people could die if the code is incorrect. At worst, people could lose some money if my code is incorrect. I still would prefer not to cause that to happen, but those people are generally OK with taking that risk if it means getting features faster.

That's why Go has a future really. It's because for most software, some correctness issues are not the end of the world, and so you can rely on not fully sound approaches to finding bugs, like automated testing, race detection, and so on.

Rust can also make some types of software more productive to write, but it is unlikely to beat Go in terms of productivity when it comes to a lot of the stuff SaaS shops deal with. And boy, the software industry sure is swamped in fucking SaaS.

1y agoHN ↗

A lot of sass people i know are more and more choosing rust for boring code. This includes several people who said things like "go is good enough, i don't want to deal with all the rust completely".

Once your sass products get enough users, and you're dealing with millions or billions of requests per day, those rare bugs start showing up quite often... And it turns out programming towards correctness is desirable, if for no other reason than to keep pagerduty quiet. Tolerating correctness issues isn't cost-free... People having to respond during off hours costs money and stress. I think most people would rather pay the costs at dev time, when they aren't under the pressure of an incident, than during an outage.

1y agoHN ↗

This is also in line with everything we know about good software engineering. Putting out fires in production is extremely costly, hence potential issues should be addressed at the earliest feasible stage.

1y agoHN ↗

But correctness is not binary, it's more like a multidimensional spectrum. Your choice of programming language has some influence, as does standards and conventions, the ecosystem of your programming language, use of automated tooling like linting and testing, or even just ol' unreliable, discipline. Being a relatively greenfield language, Go is not in a terrible place when it comes to most of those things. Tons of automated tooling, including tools like the Checklocks analyzer or the many tools bundled with golangci-lint. Uber has done a pretty good job enumerating the challenges that remain, and even working at improving those issues too, such as with NilAway.

The question isn't "wouldn't you prefer more correctness?" it's "how much would you pay for how much of an improvement in correctness?".

Rust is still growing rapidly though, whereas Go is probably not growing rapidly anymore, I think Go has at least saturated it's own niche more than 50% and is on the other end of the curve by now. Last I checked Rust is the trendiest language by far, the one that people most wish they were writing, and the one that you want to be able to say your project is written in. So it would be extremely surprising to hear if there wasn't a growing Rust presence basically everywhere, SaaS's included.

1y agoHN ↗

A lot of sass people i know are more and more choosing rust for boring code

It seems like you're in some kind of bubble, especially when looking at Rust usage in the industry.

Once your sass products get enough users, and you're dealing with millions or billions of requests per day, those rare bugs start showing up quite often...

This is a blanket statement that's simply not true and I'm speaking as someone who uses Go in the exact scenario you described.

What kind of bugs are actually happening to these people? Do you have any real-world examples of the issues you're referring to, ones that suddenly start occurring only at the scale of millions or billions of requests per day to them?

1y agoHN ↗

Rust can also make some types of software more productive to write, but it is unlikely to beat Go in terms of productivity when it comes to a lot of the stuff SaaS shops deal with. And boy, the software industry sure is swamped in fucking SaaS.

I just wish Go supported parametric enums (sum types) and Option, rather than copying Hoare’s billion dollar mistake.

I ported some code to Go and rust a few years ago to try both languages out. The rust code ended up being 30% smaller because I could use an enum and a match expression. In Go I needed to make a set of types and interface{} to achieve the same thing - which was both slower and way more verbose. My rust implementation was as fast as my C implementation in 2/3rds as much code. And it was trivial to debug. My Go implementation took way more code to write - about the same amount of code as C, but it was harder to read than C and ran much slower.

For cookie cutter SAAS and prototypes, I prefer typescript. It’s fast enough for most things, and the type system is much more expressive without getting in your way. Not as convenient to deploy as go - especially on mobile. And the standard library is more like an attic. But in my opinion it’s a much better designed language.

1y agoHN ↗

You're not the only one who wishes Go was just a bit more like Rust: https://github.com/borgo-lang/borgo

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.

1y agoHN ↗

(Real-world experience has shown that Rust is not really at a disadvantage wrt. productivity or iteration speed).

I don't believe that for a second. Even just going from Python to Go drops my productivity by maybe about 50%. Rust? Forget it.

Sure, if you have a project that demands correctness and high performance that requires tricky concurrency to achieve, something like Rust may make sense. Not for your run-of-the-mill programs though.

1y agoHN ↗

Hey, going from Rust to Go drops my productivity by maybe about 50% :)

But more seriously, yeah, Rust doesn't make sense for trivial programs. But these days, I write Python for a living, and it doesn't take long to stumble upon bugs that Rust would have trivially detected from within the comfort from my IDE.

1y agoHN ↗

I believe your productivity drops as you say. I don't think it's inherent to the language though, at least not most of it. Rather it think it's a matter of familiarity and experience in each. When you're less practiced in a language, you're slower at it. I can write python fast, but im pretty slow at ruby. I've written a lot of python rust and go, and am about equally productive in them (although how that productivity is distributed through the dev cycle is different). It wasn't always this way, I was slow in each of them at first.

1y agoHN ↗

Rust is objectively harder to write than Go (or any other GC-language) because it exposes more concerns for the programmer to take care of. With Rust, you must always ensure your program complies with the borrow checker's rules (which is what makes Rust memory-safe) which includes having to add lifetime annotations to your variables in many cases. Go just doesn't have any of that. You could argue Rust still has an advantage in that it prevents bugs that in Go you're free to write, but then what you're claiming is that this compensates for the extra work you have to do upfront in Rust. That's a difficult position to defend, though, because an experienced Go developer probably has internalized how to avoid those bugs and the cost of preventing them can be nearly negligible. I do agree that they will still make mistakes, and those can have a cost, but they may also be quite rare, or may not matter much most of the time depending on your domain. I think that's why most people seem to agree Rust is probably only advantageous where the cost of data races in production is higher than the cognitive cost (which translates into increased effort) on the programmer.

1y agoHN ↗

I am much more productive with Rust than any other programming language, except maybe python for programs shorter than 100 lines. Does that mean every other language has terrible productivity? No, it just means that I am more experienced with Rust. In general, experienced rust devs tend to be as efficient with Rust as other devs with other languages. There’s even Google data corroborating that internally Rust teams are as productive as Go teams

1y agoHN ↗

It wasn't really tricky concurrency. Somebody just made the mistake of sharing a pointer across goroutines. It was quite indirect. Boils down to a function takeing a param and holds onto it. `go` is used at some point closing over this pointer. And now we have a data race in the waiting.

1y agoHN ↗

Aside from the type system bypass described in the article though, this is basically no different from the status quo for virtually all languages with free threading that aren't Rust. I argue that while everyone is fallible, experienced programmers usually don't make this sort of mistake directly, because they are usually well aware that sharing pointers over a closure on another thread is a recipe for disaster. Instead, I think that most of these issues are actually involving tricky circumstances that result in this happening by accident, like accidentally re-using an err variable from the enclosing function of a closure. These sorts of bugs are really bad, because they happen in spite of everyone knowing not to inappropriately share data across goroutines, and they are easy to sneak by in code reviews unnoticed. They don't intuitively look like concurrency bugs, and sometimes they're as little as one single : away from being correct. (Which I agree is a bad place for a language design to be.)

Thankfully though, people don't just throw their hands up there; a good amount of work has gone into figuring out the kinds of mistakes that often lead to Go concurrency bugs in the real world and writing static analysis tools that can help prevent them. That work, combined with Go's builtin tools and standard library, and the memory safety of individual isolated goroutines, makes most production Go concurrency bugs fairly boring even compared to C concurrency bugs, even though they theoretically have the same basic problem where you can freely share mutable data unsafely across concurrent threads.

So yes, it is still possible to write trivial, obvious concurrency bugs. The language won't stop you. However I've used Go across almost every job I've had since like 2016 and it has been rare to come across a concurrency bug this trivial. I hope I would catch flagrantly shared mutable state across threads during code review.

1y agoHN ↗

Yes experienced programmers won't make the obvious mistake... until they do because the distance from the source of the bug to it's manifestation is too great to notice until it fails in production.

1y agoHN ↗

Look, this is pointless. I'm not learning anything new when you tell me that it can and will happen. How will it happen and how much will it happen?

Hence linking to Uber's case study on the issue. The answer? Not that much.

Uber started performing race detection in production over a 6 month period and found 2,000 different race conditions. Ouch, that sounds horrible!

But wait, we're talking about 50 million lines of Go code and 2,100 services at the time of that writing. That means they were seeing approximately 1 race condition per 25,000 lines of code and about 1 race condition per service. That actually lines up pretty well with my experiences. Although I haven't had a production outage or serious correctness issue caused by a race condition in Go, I have seen probably about one or two race conditions that made it to production per service. I reckon those codebases were likely somewhere between 10,000 and 25,000 lines of code most likely, so not so far off of the scale.

But again it doesn't always lead to a serious production outage, it's just that simple. It could be worse too (could corrupt some data and pollute your production database or something, in the worst case) but usually it's better (wonky behavior but no long-term effects, maybe the service periodically crashes but restarts, leading to some dropped requests but no long term downtime.) Uber has no doubt seen at least some Go data races that have caused actual production outages, but they've seen at least 2,000 Go data races that haven't, otherwise they would've probably been caught before the race detector caught them, Go dumps stacktraces on crash. That has to tell you something about the actual probability of causing a production outage due to a data race.

Again, you do you, but I will not be losing sleep over this. It is something to be weary of when working on Go services, but it is manageable.

1y agoHN ↗

But I struggle to be as productive

You should calculate TCO in productivity. Can you write Python/Go etc. faster? Sure! Can you operate these in production with the same TCO as Rust? Absolutely not. Most of the time the person debugging production issues and data races is different than the one who wrote the code. This gives the illusion of productivity being better with Python/Go.

After spending 20+ years around production systems both as a systems and a software engineer I think that Rust is here for reducing the TCO by moving the mental burden to write data race free software from production to development.

1y agoHN ↗

With the notable inclusion of Google where the SRE team is usually separate from the SWE team (but it wasn't for my particular case) I actually was always doing operations and code at all of my jobs at least at points and usually during most of the job. This is in part my own election. I do mean all of them though I don't really love listing my work history publicly everywhere just to keep some separation.

So, my first job actually started as a pure Python gig. Operations for Python/Django absolutely sucked ass. Deploying Django code reliably was a serious challenge. We got better over time by using tools like Vagrant and Docker and eventually Kubernetes, so the differences between production and dev/testing eventually faded and become less notable. But frankly no matter what we did, not causing production issues with Django/Python was a true-to-life nightmare. Causing accidental type errors not caught by tests was easy and MyPy couldn't really cover all that much of the code easily, and the Django ORM was very easy to accidentally cause horrible production behavior with (that, of course, would look okay locally with tiny amounts of data.) This is actually the original reason why I switched to Go in the first place, at my first job in around 2016. The people who I worked with are still around to attest to this fact, if you want I can probably get them to chime in on this thread, I still talk to some of them.

Go was a totally different story. Yes, we did indeed have some concurrency pains, which really didn't exist in Python for obvious reasons, but holy shit, we could really eek a lot of performance out of Go code compared to Python. We were previously afraid we might have to move data heavy workloads from Twisted (not related to the Django stuff) to something like C++ or maybe even optimized Java, but Go handily took it and allowed us to saturate the network interface on our EC2 boxes. (A lot of communications were going over Websockets, and the standards for compression in websockets took a long time to settle and become universally supported, so we actually played with implementing the lz4 compression scheme in JS. I wound up writing my own lz4 implementation based on the algorithms, I believe, from the C version. It wound up being too much compute, though. But, we had to try, anyway.)

So how much reliability problems did we wind up having doing all this? Honestly not a whole lot on the Go side of things. The biggest production issue I ever ran into was one where the Kubernetes AWS integration blew up because we wound up having too many security groups. I wound up needing to make an emergency patch to kubelet in the early hours to solve that one :) We did run into at least one serious Go related issue over time, which was indeed concurrency related: when Go 1.6 came out, it started detecting concurrent misuses of maps. And guess what? We had one! It wasn't actually triggering very often, but in some cases we could run into a fairly trivial concurrent map access. It didn't seem to crash before but it could at least cause some weird behaviors in the event that it actually triggered before Go 1.6; now it was a crash that we could debug. It was a dumb mistake and it definitely underscores the value of borrow checking; "just don't mess up" will never prevent all mistakes, obviously. I will never tell you that I think borrow checking is useless, and really, I would love to just always write 100% correct software all the time.

That said though, that really is most of the extent of the production issues we had with Go. Go was a serious workhorse and we were doing reasonably non-trivial things in Go. (I had essentially built out a message queue system for unreliable delivery of very small events. We had a firehose of data coming in with many channels of information and needed to route those to the clients that needed them and handle throttling/etc. Go was just fantastic at this task.) Over time things got easier too, as Go kept updating and improving, helping us catch more bugs.

I can only come to one conclusion: people who treat Go and Python in the same class are just ignorant to the realities of the situation. There are cases where Rust will be immensely valuable because you really can't tolerate a correctness problem, but here's the thing about that Go concurrent map access issue: while it could cause some buggy behavior and eventually caused some crashing, it never really caused any serious downtime or customer issues. The event delivery system was inherently dealing with unreliable data streams, and we had multiple instances. If there was a blip, clients would just reconnect and people would barely notice anything even if they were actively logged in. (In fact, we really didn't do anything special for rolling deployments to this service, because the frontend component was built to just handle a disconnection gracefully. If it reconnected quickly enough, there was no visual disturbance.)

That's where the cost/benefit analysis gets tricky though. Python and Django and even Twisted are actually pretty nice and I'm sure it's even better than when we originally left it (to be clear we did still have some minor things in Django after that, too, but they were mostly internal-only services.) Python and Django had great things like the built-in admin panel which, while it couldn't solve everyone's needs, was pretty extensible and usable on its own. It took us a while to outgrow it for various use cases. Go has no equivalent to many Django conveniences, so if you haven't fully outgrown e.g. the Django admin panel and ORM, it's hard to fully give up on those features.

Throughout all of this, we had a lot more issues with our JS frontend code than we ever did with either Python/Django or Go, though. We went through trying so many things to fix that, including Elm and Flow, and eventually the thing that really did fix it, TypeScript. But that is another story. (Boy, I sure learned a lot on my first real career job.)

At later jobs, Go continued to not be at the center of most of the production issues I faced running Go software. That's probably partly because Go was not doing a lot of the most complicated work, often times the most complicated bits were message queues, databases and even to some degree memory caches, and the Go bits were mostly acting like glue (albeit definitely glue with application logic, to be sure.)

So is the TCO of Go higher than Rust? I dunno. You can't really easily measure it since you don't get to explore parallel universes where you made different choices.

What I can say is that Go has been a choice I never regretted making all the way from the very first time and I would choose it again tomorrow.

1y agoHN ↗

TCO? Tail call optimization? But that doesn't make sense in this context. hm.

1y agoHN ↗

It is very unfortunate that we use fixed width numbers by default in most programming languages and that common ops will silently overflow. Smarter compilers can work with richer numeric primitives and either automatically promote machine words to big numbers or throw an error on overflow.

People talk a lot about the productivity gains of ai, but fixing problems like this at the language level could have an even bigger impact on productivity, but are far less sensational. Think about how much productivity is lost due to obscure but detectable bugs like this one. I don't think rust is a good answer (it doesn't check overflow by default), but at least it points a little bit in the vaguely correct direction.

1y agoHN ↗

The situation with numbers in basically every widely used programming language is kind of an indictment of our industry. Silent overflow for incorrect results, no convenient facilities for units, lossy casts everywhere. It's one of those things where standing in 1975 you'd think surely we'll spend some of the next 40 years of performance gains to give ourselves nice, correct numbers to work with, but we never did.

1y agoHN ↗

"nice, correct numbers" end somewhere between 1/3 and sqrt(2)

so in reality, it's just "pick your own poison" to various degrees...

1y agoHN ↗

The square root of two is still a computable Real. We choose not to cope with that, but it's not actually impossible it was merely inconvenient. I've mentioned elsewhere that my Rust care realistic is quite happy to work with these numbers e.g. take the square root of ten, and the square root of forty, multiply them together and get the quite ordinary integer twenty.

The non computable reals are a huge problem because, as their name suggests, we can't compute them - and in the strict sense that's Almost All reals, but none of the ones you're thinking of are non-computable so you'll likely be fine.

For the merely rational numbers like a third, or sixteen hundred and five sevenths, it's even more so a matter of choosing not to address it rather than it being out of reach.

1y agoHN ↗

The problem with computables is that equivalence between them is only semi-decidable. (If the two numbers are different, it is decidable, but if they are not, it isn't. The problem is that you don't know if they are different a priori, so you might get lucky and find difference, but you might as well not.)

We know for sure that algebraic numbers behave nicely in terms of equivalence, and there are other, bigger number systems that are conjectured to behave nicely ( https://en.wikipedia.org/wiki/Period_(algebraic_geometry) ), but the problem with these and computers is that they are hard to represent.

1y agoHN ↗

Yeah, all these types have problems, we've decided to put up with the IEEE floating point numbers, we could have chosen to have the big rationals, or drawn any other line. I don't disagree that there's no satisfying "correct" answer but it's a little disappointing that programmers so easily accept the status quo as though nothing else could be in its place.

Maybe Python having automatic big numbers like Lisps often did will help introduce new programmers to the idea that the 32-bit two's complement integer provided on all modern computers isn't somehow "really" how numbers work.

1y agoHN ↗

Even so, we don't need to pick the more potent poison. And most certainly not when we had decades of awareness about better alternatives.

There's really no excuse for a modern PL to not have, at the very least, overflow detection by default.

1y agoHN ↗

Rust checks overflow by default in debug builds

1y agoHN ↗

I've often thought that I'd prefer it to check by default in release builds, too, but I understand that comes with a performance penalty that a lot of folks aren't happy with.

I assume this implies that common processor architectures (x86_64, aarch64) lack trap-on-overflow variants of their integer arithmetic instructions? If the explanation really is that simple, it's pretty disappointing.

1y agoHN ↗

You can tell the compiler that you want your crate to perform the checks regardless.

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)

1y agoHN ↗

I assume this implies that common processor architectures (x86_64, aarch64) lack trap-on-overflow variants of their integer arithmetic instructions?

Yes*. But all modern instruction sets have condition flags and conditional instructions, so it's still very much possible to implement the checks robustly in machine code. However, doing so would generally require injecting at least one additional conditional-branch instruction, and in some cases, switching from non-flag-setting instructions to flag-setting instructions (which can be slower).

* = true "trap on overflow" existed in 32-bit x86 but was tricky to use and got removed when going to 64-bit

1y agoHN ↗

Swift traps on overflow, which I think is the correct solution. You shouldn't make all your numbers infinitely-ranged, that turns all O(1) operations into O(N) in time and memory, and introduces a lot of possibilities for remote DoS.

1y agoHN ↗

Theoretically you can construct a loop counter that overflows, but I don't that there is any reasonable way to do it accidentally?

Within safe rust you would likely need to be using an explicit .wrapping_add() on your counter, and explicitly constructing a for loop that wasn't range-based...

1y agoHN ↗

Well, in debug mode yes, but in release mode overflow is wrapping by default unless you explicitly set a flag to make it panic.

1y agoHN ↗

It ultimately resulted in a loop counter overflowing, which recomputed the same thing a billion of time (but always the same!). So the visible effect was a request would randomly take 3 min instead of 100ms.

This means that multiple goroutines were writing to the same local variable. I've never worked on a Go team where code that is structured in such a way would be considered normal or pass code review without good justification.

1y agoHN ↗

It happens all the time sadly.

It's not because people intentionally write this way. A function takes a parameter (a Go slice for example) and calls another function and so one. Deep down a function copies the pointer to the slice (via closure for example). And then a goroutine is spawned with this closure.

The most obvious mistakes are caught quickly. Buu sharing a memory address between two threads can happen very indirectly.

And somehow in Go, everybody feels incredibly comfortable spawning millions of coroutines/threads.

1y agoHN ↗

What do Uber mean in that article when they say that Go programs "expose 8x more concurrency compared to Java microservices"? They're using the word concurrency as if it were a countable noun.

1y agoHN ↗

If the Java version creates 4 concurrent tasks (could be threads, fibers, futures, etc.) but the Go version creates 32 goroutines, that's 8x the concurrency.

1y agoHN ↗

I think it's also worth noting that Rust's maintainers acknowledge its various soundness holes as bugs that need to be fixed. It's just that some of them, like https://github.com/rust-lang/rust/issues/25860 (which I assume you're referring to), need major refactors of certain parts of the compiler in order to fix, so it's taking a while.

1y agoHN ↗

Yeah, I can totally believe that this is not a big issue in practice.

But I think terms like "memory safety" should have a reasonably strict meaning, and languages that go the extra mile of actually preventing memory corruption even in concurrent programs (which is basically everything typically considered "memory safe" except Go) should not be put into the same bucket as languages that decide not to go through this hassle.

1y agoHN ↗

That Uber article is fantastic. I believe Go fixed the first example recently.

We had a rule at my last gig: avoid anonymous functions and always recover from them.

1y agoHN ↗

Real-world golang programs share memory all the time, because the "share by communicating" pattern leads to pervasive logical problems, i.e. "safe" race conditions and "safe" deadlocks.

1y agoHN ↗

I am not sure sync.Mutex fixes either of these problems. Press C-\ on a random Go server that's been up for a while and you'll probably find 3000 goroutines stuck on a Lock() call that's never going to return. At least you can time out channel operations:

   select {
   case <-ctx.Done():
          return context.Cause(ctx)
   case msg := <-ch:
          ...
   }
1y agoHN ↗

Even if you use channels to send things between goroutines, go makes it very hard to do so safely because it doesn't have the idea of sendable types, ownership, read-only references, and so on.

For example, is the following program safe, or does it race?

    func processData(lines <-chan []byte) {
      for line := range lines {
        fmt.Printf("processing line: %v\n", line)
      }
    }

    func main() {
      lines := make(chan []byte)
      go processData(lines)

      var buf bytes.Buffer
      for range 3 {
        buf.WriteString("mock data, assume this got read into the buffer from a file or something")
        lines <- buf.Bytes()
        buf.Reset()
      }
    }

The answer is of course that it's a data race. Why?

Because `buf.Bytes()` returns the underlying memory, and then `Reset` lets you re-use the same backing memory, and so "processData" and "main" are both writing to the same data at the same time.

In rust, this would not compile because it is two mutable references to the same data, you'd either have to send ownership across the channel, or send a copy.

In go, it's confusing. If you use `bytes.Buffer.ReadBytes("\n")` you get a copy back, so you can send it. Same for `bytes.Buffer.String()`.

But if you use `bytes.Buffer.Bytes()` you get something you can't pass across a channel safely, unless you also never use that bytes.Buffer again.

Channels in rust solve this problem because rust understands "sending" and ownership. Go does not have those things, and so they just give you a new tool to shoot yourself in the foot that is slower than mutexes, and based on my experience with new gophers, also more difficult to use correctly.

1y agoHN ↗

That code would never pass a human pull request review. It doesn't even pass AI code review with a simple "review this code" prompt: https://chatgpt.com/share/68829f14-c004-8001-ac20-4dc1796c76...

"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.

1y agoHN ↗

If you are familiar with the internals of bytes/buffer you would catch this. But it would be great for the compiler to catch this instead of a human reviewer. In Rust, this code wouldn't even compile. And I'd argue even in C++, this mistake would be clearer to see in just the code.

1y agoHN ↗

I mean, you're basically passing a pointer to another thread to processData()

And yet, "bytes.Buffer.ReadBytes(delim)" returns a copy of the underlying data which would be safe in this context.

The type system does not make it obvious when this is safe or not, and passing pointers you own across channels is fine and common.

That code would never pass a human pull request review

Yes, that was a simplified example that a human or AI could spot.

When you actually see this in the wild, it's not a minimal example, it's a small bug in hundreds of lines of code.

I've seen this often enough that it obviously does actually happen, and does pass human code review.

1y agoHN ↗

In go, it's confusing. If you use `bytes.Buffer.ReadBytes("\n")` you get a copy back, so you can send it. Same for `bytes.Buffer.String()`.

But if you use `bytes.Buffer.Bytes()`

If you're experienced, it's pretty obvious that a `bytes.Buffer` will simply return its underlying storage if you call `.Bytes()` on it, but will have to allocate and return a new object if you call say `.String()` on it.

unless you also never use that bytes.Buffer again.

I'm afraid that's concurrency 101. It's exactly the same in Go as in any language before it, you must make sure to define object lifetimes once you start passing them around in concurrent fashion.

Channels are nice in that they model certain common concurrency patterns really well - pipelines of processing. You don't have to annotate everything with mutexes and you get backpressure for free. But they are not supposed to be the final solution to all things concurrency and they certainly aren't supposed to make data races impossible.

Even if you use channels to send things between goroutines, go makes it very hard to do so safely

Really? Because it seems really easy to me. The consumer of the channel needs some data to operate on? Ok, is it only for reading? Then send a copy. For writing too? No problem, send a reference and never touch that reference on our side of the fence again until the consumer is done executing.

Seems about as hard to understand to me as the reason why my friend is upset when I ate the cake I gave to him as a gift. I gave it to him and subsequently treated it as my own!

Such issues only arise if you try to apply concurrency to a problem willy-nilly, without rethinking your data model to fit into a concurrent context.

Now, would the Rust approach be better here? Sure, but not if that means using Rust ;) Rust's fancy concurrency guarantees come with the whole package that is Rust, which as a language is usually wildly inappropriate for the problem at hand. But if I could opt into Rust-like protections for specific Go data structures, that'd be great.

1y agoHN ↗

Wow who knew concurrency is hard!

This isn't anything special, if you want to start dealing with concurrency you're going to have to know about race conditions and such. There is no language that can ever address that because your program will always be interacting with the outside world.

1y agoHN ↗

This is a canard.

What's happening here, as happens so often in other situations, is that a term of art was created to describe something complicated; in this case, "memory safety", to describe the property of programming languages that don't admit to memory corruption vulnerabilities, such as stack and heap overflows, use-after-frees, and type confusions. Later, people uninvolved with the popularization of the term took the term and tried to define it from first principles, arriving at a place different than the term of art. We saw the same thing happen with "zero trust networking".

The fact is that Go doesn't admit memory corruption vulnerabilities, and the way you know that is the fact that there are practically zero exploits for memory corruption vulnerabilities targeting pure Go programs, despite the popularity of the language.

Another way to reach the same conclusion is to note that this post's argument proves far too much; by the definition used by this author, most other higher-level languages (the author exempts Java, but really only Java) also fail to be memory safe.

Is Rust "safer" in some senses than Go? Almost certainly. Pure functional languages are safer still. "Safety" as a general concept in programming languages is a spectrum. But "memory safety" isn't; it's a threshold test. If you want to claim that a language is memory-unsafe, POC || GTFO.

1y agoHN ↗

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.

1y agoHN ↗

Everyone knows that there's something very magical about the choice of 42.

1y agoHN ↗

to describe the property of programming languages that don't admit to memory corruption vulnerabilities, such as stack and heap overflows, use-after-frees, and type confusions.

And data races allow all of that. There cannot be memory-safe languages supporting multi-threading that admit data races that lead to UB. If Go does admit data races it is not memory-safe. If a program can end up in a state that the language specification does not recognize (such as termination by SIGSEGV), it’s not memory safe. This is the only reasonable definition of memory safety.

1y agoHN ↗

If that were the case, you'd be able to support the argument with evidence.

1y agoHN ↗

You mean like the program in the article where code that never dereferences a non-pointer causes the runtime to dereference a non-pointer? That seems like evidence to me.

1y agoHN ↗

An exploit against a real Go program that relies on memory corruption.

1y agoHN ↗

It should be possible to construct an exploit for such programs. But even for truly unsafe languages, vulnerabilities just from data races are very rare, because they are much harder to exploit.

You could argue Go is safe from memory vulnerabilities, and that'll be 99% correct (we can't know what will happen if some very strong organization (e.g. a nation-state actor) will heavily invest in exploiting some Go program), but it still isn't memory safe, as per the definition in Wikipedia:

Memory safety is the state of being protected from various software bugs and security vulnerabilities when dealing with memory access, such as buffer overflows and dangling pointers.

1y agoHN ↗

There's enormous incentive to construct those exploits. Why don't they exist?

1y agoHN ↗

Go programs are normally running on the server side and are often proprietary, so you can't see the bugs exist to exploit them. It's not something like Chrome where someone can spend weeks finding a race and exploiting it to get a bug bounty, with full visibility of the source code and ability to develop an exploit in the lab.

1y agoHN ↗

What about this particular bug do you think makes it likely to be exploitable? I'm not asking you to write an RCE POC, just to tell a story of the sequence of events involving this bug that results in attacker-controlled code. What does the attacker control here, and how do they use that control to divert execution?

1y agoHN ↗

Hide the same program into some dependency of a dependency and you have a nice little security vulnerability in your prod app. It's actually very easy to hide such a vulnerability as an innocent bug.

1y agoHN ↗

If you're stipulating deliberately inserted vulnerabilities then there are much easier ways, e.g., with a plausibly-deniable logic bug in code that calls os/exec or reflect (both of which can execute arbitrary code by design).

1y agoHN ↗

That’s called "moving the goal posts".

A definition of memory safety that permits unsoundness as long as nobody has exploited said unsoundness is not a definition that anyone serious about security is going to accept. Unsoundness is unsoundness, undefined behavior is undefined behavior. The conservative stance is that once execution hits UB, anything can happen.

1y agoHN ↗

I think your security background is coloring your perception of the term memory safety. Specifically the requirement that the various issues lead to exploitation. These issues can lead to many other issues that are not vulnerability in the security sense, e.g. data corruption, incorrect (but not insecure) behavior, performance issues, and more. I don't think any of those were ever dismissed or excluded from memory safety discussion. Infosec circles tend to evaluate most ideas in the context of (anti)exploitation, and the rest of programming tends to focus on what the cool kids argue (that is they often weigh security concerns higher than other issues as well), so the other problems caused by double-free or buffer overruns (etc) just may not have been given as much weight in your mind.

1y agoHN ↗

"Memory safety" is a security term, not a PLT term.

1y agoHN ↗

First off, in any and every engineering discipline it would be absurd to claim that "safety" only means security against intentional malice.

Second, the burden of proof goes the other way. It’s absurd to claim that UB is safe unless proven otherwise. Unsafety must obviously be the default assumption.

1y agoHN ↗

This is no true Scotsman for programming languages.

I could also argue C is memory safe and all the exploits that have been made weren’t real C programs

1y agoHN ↗

"What's happening here, as happens so often in other situations, is that a term of art was created to describe something complicated; [..] Later, people uninvolved with the popularization of the term took the term and tried to define it from first principles, arriving at a place different than the term of art."

Happens all the time in math and physics but having centuries of experience with this issue we usually just slap the name of a person on the name of the concept. That is why we have Gaussian Curvature and Riemann Integrals. Maybe we should speak of Jung Memory Safety too.

Thinking about it, the opposite also happens. In the early 19th century "group" had a specific meaning, today it has a much broader meaning with the original meaning preserved under the term "Galois Group".

Or even simpler: For the longest time seconds were defined as fraction of a day and varied in length. Now we have a precise and constant definition and still call them seconds and not ISO seconds.

1y agoHN ↗

How does Java "fail" to be memory safe by the definition used by the author ? Please give an example.

1y agoHN ↗

This is a good post and I agree with it in full, but I just wanted to point out that (safe) Rust is safer from data races than, say, Haskell due to the properties of an affine type system.

Haskell in general is a much safer than Rust thanks to its more robust type system (which also forms the basis of its metaprogramming facilities), monads being much louder than unsafe blocks, etc. But data races and deadlocks are one of the few things Rust has over it. There are some pure functional languages that are dependently typed like Idris, and thus far safer than Rust, but they're in the minority and I've yet to find anybody using them industrially. Also Fortnite's Verse thing? I don't know how pure that language is though.

1y agoHN ↗

I don't think it's true that Rust is safer, using the terminology from the article. Both languages prevent you from doing things that will result in safety violations unless you start mucking with unsafe internals.

Rust absolutely does make it easier to write high-performance threaded code correctly, though. If your system depends on high amounts of concurrent mutation, Rust definitely makes it easier to write correct code.

On the other hand, a system like STM in Haskell can make it easier to write complex concurrency logic correctly in Haskell than Rust, but it can have very bad performance overhead and needs to be treated with extreme suspicion in performance-sensitive code. It's a huge win for simple expression of complex concurrency, but you have to pay for it somewhere. It can be used in ways where that overhead is acceptable, but you absolutely need to be suspicious in a way that's never a concern in Rust.

1y agoHN ↗

Another way to reach the same conclusion is to note that this post's argument proves far too much; by the definition used by this author, most other higher-level languages (the author exempts Java, but really only Java) also fail to be memory safe.

Yes I mean that was the whole reason they invented rust. If there were a bunch of performant memory safe languages already they wouldn't have needed to.

1y agoHN ↗

If you want to claim that a language is memory-unsafe, POC || GTFO.

There's a POC right in the post, demonstrating type confusion due to a torn read of a fat pointer. I think it could have just as easily been an out-of-bounds write via a torn read of a slice. I don't see how you can seriously call this memory safe, even by a conservative definition.

Did you mean POC against a real program? Is that your bar?

1y agoHN ↗

You need a non-contrived example of a memory-corrupting data race that gives attackers the ability to control memory, through type confusion or a memory lifecycle bug or something like it. You don't have to write the exploit but you have to be able to tell the story of how the exploit would actually work --- "I ran this code and it segfaulted" is not enough. It isn't even enough for C code!

1y agoHN ↗

The post is a demonstration that a class of problems: causing Go to treat a integer field as a pointer and access the memory behind that pointer without using any of Go's documented "unsafe.Pointer" (or other documented as unsafe operations).

We're talking about programming languages being memory safe (like fly.io does on it's security page [1]), not about other specific applications.

It may be helpful to think of this as talking about the security of the programming language implementation. We're talking about inputs to that implementation that are considered valid and not using "unsafe" marked bits (though I do note that the Go project itself isn't very clear on if they claim to be memory-safe). Then we want to evaluate whether the programming language implementation fulfills what people think it fulfills; ie: "being a memory safe programming language" by producing programs under some constraints (ie: no unsafe) that are themselves memory-safe.

The example we see in the OP is demonstrating a break in the expectations for the behavior of the programming language implementation if we expected the programming language implementation to produce programs that are memory safe (again under some conditions of not using "unsafe" bits).

[1]: https://fly.io/docs/security/security-at-fly-io/#application...

1y agoHN ↗

In this thread I linked the fly.io security page because it helps us establish that one can talk about _languages_ as being memory safe specifically, which is something it seems you're rejecting as a concept in the parent and other comments.

(In a separate comment about "what do people claim about Go anyhow", I linked the memorysafety.org page, but I did not expect it to help in getting you to the understanding that we can evaluate programming languages as being memory safe or not, where something from the company where someone was a founder seemed more likely to get a person to reconsider the framing of what we're examining)

1y agoHN ↗

Huh? No, I'm not. Go is a memory-safe programming language, like Java before it, like Python, Ruby, Javascript, and of course Rust.

1y agoHN ↗

The older I get the more I just see these kinds of threads like I see politics: Exaggerate your "opponents" weaknesses, underplay/ignore its strengths and so on. So if something no matter how disproportionate can be construed to be, or be associate with, a current zeitgeist with a negative sentiment, it's an opportunity to gain ground.

I really don't understand why people get so obsessed with their tools that it turns into a political battleground. It's a means to an end. Not the end itself.

1y agoHN ↗

Another way to reach the same conclusion is to note that this post's argument proves far too much; by the definition used by this author, most other higher-level languages (the author exempts Java, but really only Java) also fail to be memory safe.

This is wrong.

I explicitly exempt Java, OCaml, C#, JavaScript, and WebAssembly. And I implicitly exempt everyone else when I say that Go is the only language I know of that has this problem.

(I won't reply to the rest since we're already discussing that at https://news.ycombinator.com/item?id=44678566 )

1y agoHN ↗

The fact is that Go doesn't admit memory corruption vulnerabilities, and the way you know that is the fact that there are practically zero exploits for memory corruption vulnerabilities targeting pure Go programs, despite the popularity of the language.

Another way to word it: If "Go is memory unsafe" is such a revelation after its been around for 13 years, it's more likely that such a statement is somehow wrong than that nobody's picked up on such a supposedly impactful safety issue in all this time.

As such, the burden of proof that addresses why nobody's ran into any serious safety issues in the last 13 years is on the OP. It's not enough to show some theoretical program that exhibits the issue, clearly that is not enough to cause real problems.

1y agoHN ↗

There's no "revelation" here, it's always been well known among experts that Go is not fully memory safe for concurrent code, same for previous versions of Swift. OP has simply spelled out the argument clearly and made it easier to understand for average developers.

1y agoHN ↗

It's made what would be a valid point using misleading terminology and framing that suggests these are security issues, which they simply are not.

"One could easily turn this example into a function that casts an integer to a pointer, and then cause arbitrary memory corruption."

No, one couldn't! One has contrived a program that hardcodes precisely the condition one wants to achieve. In doing so, one hasn't even demonstrated even one of the two predicates for a memory corruption vulnerability (attacker control of the data, and attacker ability to place controlled data somewhere advantageous to the attacker).

What the author is doing is demonstrating correctness advantages of Rust using inappropriate security framing.

1y agoHN ↗

misleading terminology and framing that suggests these are security issues

Could you quote where exactly OP has misleadingly "suggested" that these concerns lead to security issues in the typical case?

attacker control of the data, and attacker ability to place controlled data somewhere advantageous to the attacker

Under this definition the Rowhammer problem with hardware DRAM does not qualify as a genuine security concern since it inherently relies on fiddly non-determinism that cannot possibly be "controlled" by any attacker. (The problem with possible torn writes in concurrent Go code is quite similar in spirit; it's understood that an actually observed torn write might only occur rarely.) Needless to say there is a fairly strong case for addressing these problems anyway, as a matter of defence in depth.

correctness advantages of Rust

Memory safety in OP's sense is not exclusive to Rust. Swift has it. Even Java/C# cannot access arbitrary memory as a result of torn writes. It would be more accurate to say that OP has identified a correctness issue that's apparently exclusive to Go.

1y agoHN ↗

To use your definition, that quote is clearly making a point about correctness, not necessarily about real-world security.

1y agoHN ↗

As long as we agree that there isn't a meaningful security implication, we don't need to keep litigating.

1y agoHN ↗

Nope. You can have programs without undefined behavior and still not have thread safety. In .NET, for example, writes to variables that are wider then the machine width or not aligned properly, are not guaranteed to be atomic. So if you assign some value to an Int128 variable, it will not be updated atomically - how could it, that is just beyond the capabilities of the processor - and therefore a different thread can observe a state where only half of the variable has been updated. No undefined behavior here but also sharing this variable between threads is not thread safe. And having the language synchronize all such writes - just in case some other thread might want tot look at it - is a performance disaster. And disallowing anything that might be a potential thread safety issue will give you a pretty limited language.

1y agoHN ↗

disallowing anything that might be a potential thread safety issue will give you a pretty limited language.

Safe Rust doesn't seem that limited to me.

I don't think any of the C# work I do wouldn't be possible in Rust, if we disregard the fact that the rest of the team don't know Rust.

Most of the programs you eliminate when you have these "onerous" requirements like memory safety are nonsense, they either sometimes didn't work or had weird bugs that would be difficult to understand and fix - sometimes they also had scary security implications like remote code execution. We're better off without them IMNSHO.

1y agoHN ↗

The statement "there is no memory safety without thread safety" does not suggest that memory safety is sufficient to provide thread safety. Instead, it's just saying that if you want thread safety, then memory safety is a requirement.

1y agoHN ↗

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.

1y agoHN ↗

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.

1y agoHN ↗

It's saying the opposite

Indeed, you're correct, I interpreted the implications in reverse.

1y agoHN ↗

Critically to the authors point that type of data race does not result in UB and does not break the language and thus does not create any memory safety issues. Ergo, it's a memory safe language.

Go (and previously Swift) fails at this. There data races can result in UB and thus break memory safety

1y agoHN ↗

See the article's comments on Java, which is "thread safe" in the sense of preventing undefined behavior but not in the sense of preventing data-race-related logic bugs. .NET is precisely analogous in this respect.

1y agoHN ↗

I can buy that claim for the .NET CLR but I've never seen it nailed down properly the way Java did which gives me pause.

I worry about the Win95-era "Microsoft Pragmatism" at work and a concrete example which comes to mind is nullability. In the nice modern software I often work on I can say some function takes a string and in that program C# will tell me that's not allowed to be null, it has to be an actual string - a significant engineering benefit. But, the CLR does not enforce such rules, so that function may still receive a null instead e.g. if called by some ten year old VB.NET code which has no idea about "nullability" and so just fills out a null for that parameter anyway.

Of course the CLR memory model might really be set in stone and 100% proof against such problems, but I haven't seen anything to reassure me as I did for Java and I fear that if it were convenient for Windows to not quite do that work they would say eh, good enough.

1y agoHN ↗

So, the answer is that I've read that and I wasn't as reassured as I'd like. ECMA definitely isn't enough as it acknowledges. The platforms which exist today are fine. And future platforms? Well we're told it will be difficult to change these assumptions. Yeah, it would be difficult.

For a comparison, the x86 has what that document calls TSO, a very strict "free" ordering (in fact you pay all the time, but, you can't opt out so in that sense it's free to get this ordering on Intel) so 1990s C++ written for Windows just assumes volatile means you get memory ordering -- even though that's not what that means. If you compile brand new code for x86 on Microsoft's compilers today you get the exact same promise, but if you target their ARM platforms you don't get that because it would be expensive so, too bad.

1y agoHN ↗

This is false as a generality.

A memory safe, managed language doesn't become unsafe just because you have a race condition in a program.

Like, say, reading and writing several related shared variables without a mutex.

Say that the language ensures that the reads and writes themselves of these word-sized variables are safe without any lock, and that memory operations and reclamation of memory are thread safe: there are no low-level pointers (or else only as an escape hatch that the program isn't using).

The rest is your bug; the variable values coming out of sync with each other, not maintaining the invariant among their values.

It could be the case that a thread-unsafe program breaks a managed run-time, but not an unvarnished truth.

A managed run-time could be built on the assumption that the program will not create two or more threads such that those threads will invoke concurrent operations on the same objects. E.g. a managed run time that needs a global interpreter lock, but which is missing.

1y agoHN ↗

The author knows that. His point is that Go doesn't work that way because it uses greater-than-word-sized values that can suffer torn writes leading to segfaults in some cases.

1y agoHN ↗

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.

1y agoHN ↗

If the variables are word-sized, sure. But what if they are larger? Now a race condition between one thread writing and another thread reading or writing a variable is a memory safety issue.

1y agoHN ↗

Don't have such things, if you know what's good for you, or else don't have threads.

1y agoHN ↗

Now a race condition between one thread writing and another thread reading or writing a variable is a memory safety issue.

No it isn't, because the torn write cannot have arbitrary effects that potentially break the program. It only becomes such if you rely on such a variable to establish an invariant about memory that's broken if a torn write occurs (such as by encoding a ptr+len in it), which is just silly. Don't do that!

1y agoHN ↗

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.

1y agoHN ↗

race condition != data race. Specifically, in go, a race condition can cause application level bugs but won't affect, directly, the runtime consistency; on the other hand a data race on a slice can cause torn writes and segfaults in the best case, and fandango on core in the worst case.

1y agoHN ↗

Your fantasy language doesn't have a race condition.

1y agoHN ↗

The rest is your bug; the variable values coming out of sync with each other, not maintaining the invariant among their values.

If the language and its runtime let me break their invariant, then that's their bug, not mine. This is the fundamental promise of type-safe languages: you can't accidentally break the language abstraction.

It could be the case that a thread-unsafe program breaks a managed run-time, but not an unvarnished truth.

I demonstrated that the Go runtime is such a case, and I think that should be considered a memory safety violation. Not sure which part of that you disagree with...

1y agoHN ↗

The point being made is sound, but I can never escape the feeling that most concurrency discussion in programming language theory is ignoring the elephant in the room. The concurrency bugs that matter in most apps are all happening inside the database due to lack of proper locking, transactions or transactional isolation. PL theory ignores this and so things like Rust's approach to race freedom ends up not mattering much outside of places like kernels. A Rust app can avoid use of unsafe entirely and still be riddled with race conditions because all the data that matters is in an RDBMS and someone forgot a FOR UPDATE in their SELECT clause.

1y agoHN ↗

What’s worse, even if you use proper transactions for everything, it’s hard to reason about visibility and data races when performing SQL across tables, or multiple dependent SQL statements within a transaction.

1y agoHN ↗

I agree with the author's claim that you need thread safety for memory safety.

But I don't agree with:

I will argue that this distinction isn’t all that useful, and that the actual property we want our programs to have is absence of Undefined Behavior.

There is plenty of undefined behavior that can't lead to violating memory safety. For example, in many languages, argument evaluation order is undefined. If you have some code like:

    foo(print(1), print(2));

In some languages, it's undefined as to whether "1" is printed before "2" or vice versa. But there's no way to violate memory safety with this.

I think the only term the author needs here is "memory safety", and they correctly observe that if the language has threading, then you need a memory model that ensures that threads can't break your memory safety.

Go lacks that. It seems to be a rare problem in practice, but if you want guarantees, Go doesn't give you them. In return, I guess it gives you slightly faster execution speed for writes that it allows to potentially be torn.

1y agoHN ↗

That's "unspecified" not "undefined". "Undefined behavior" literally means "anything goes", so any program that invokes it is broken by definition.

1y agoHN ↗

That is not true, that is a very specific definition of UB which C developers (among others) favor. That doesn't mean that another language can't say "this is undefined behavior" without all the baggage that accompanies the term in C.

1y agoHN ↗

It's literally how the term "UB" is defined, and understood by experts. Why would anyone want to say "undefined" when they really mean "unspecified"? That's just confusing.

1y agoHN ↗

No, it's how one very specific community of experts understands it. It is not some kind of universal law of definition that it must mean that always and everywhere. As far as what is confusing, that is a matter of perspective. I think it is confusing (to put it mildly) that the C community has chosen to use "undefined behavior" to mean "it must never happen, and anything goes if it does". That is extremely counterintuitive, and only makes sense to those who live and breathe that world. So if the standard is to be "avoiding confusion", then we better change the definition used by the C community ASAP.

1y agoHN ↗

I agree that the term "undefined behavior", when used as in C/C++/Rust/Swift/.NET, isn't very good at communicating to non-experts what's at stake, not least because it doesn't sound scary enough (the security community remains indebted to whoever coined the term "nasal demons"). That said, is there a specific other community of practice where there's a shared understanding that the term "undefined behavior" means something different?

1y agoHN ↗

It is also not what the C community has chosen. It is what was imposed on us by certain optimizing compilers that used the interpretation that gave them maximum freedom to excel in benchmarks, and it was then endorsed by C++. The C definition is that "undefined behavior" can have arbitrary concrete behavior, not that a compiler can assume it does not happen. (that form semantic people prefer the former because it makes their life easier did not help)

1y agoHN ↗

The C definition is that "undefined behavior" can have arbitrary concrete behavior, not that a compiler can assume it does not happen.

What is the difference between those? How does a compiler that assumes UB never happens violate the requirement that UB can have arbitrary concrete behavior? If we look at a simple example like optimizing "x + y > x" (signed arithmetic, y known to be positive) to "true" -- that will lead to some arbitrary concrete behavior of the program, so it seems covered by the definition.

I assume that what the original C authors meant was closer to "on signed integer overflow, non-deterministically pick some result from the following set", but that's not what they wrote in the standard... if you want to specify that something is non-deterministic, you need to spell out exactly what the set of possible choices are. Maybe for singed integer overflow one could infer this (though it really should be made explicit IMO), but C also says that the program has UB "by default" if it runs into a case not described by the standard, and there's just no way to infer a set of choices from that as far as I can see.

1y agoHN ↗

"Undefined behavior" is not a meaningless made up term that you can redefine at will.

The word "undefined" has a clear meaning: there is no behavior defined at all for what a given piece of code will do, meaning it can literally do anything. If the language spec defines the possible behaviors you can expect (even if the behavior can vary between implementations), then by definition it's not undefined.

1y agoHN ↗

"Undefined behavior" is not a meaningless made up term that you can redefine at will.

Sure, I agree with that.

The word "undefined" has a clear meaning: there is no behavior defined at all for what a given piece of code will do...

That is true, but...

...meaning it can literally do anything.

This is not at all true! That is a different (but closely related) matter, which is "what is to be done about undefined behavior". Which is certainly something one has to take a stance on when working to a language spec that has undefined behavior, but that does not mean that "undefined" automatically means your preferred interpretation of how to handle undefined behavior.

1y agoHN ↗

The original question is how UB is defined, not about the preferred way of dealing with it in a practical sense. And the definition of UB is behavior for which the language definition imposes no requirements, and explicitly leaves open the possibility of ignoring the situation altogether with unpredictable results.

1y agoHN ↗

The author is using the term in the way that everyone else understands it. They are not aware of your unusual definition.

1y agoHN ↗

Your example does not classify as 'undefined behavior'. Something is 'undefined behavior' if it is specified in the language spec, and in such case yes, the language is capable of doing anything including violating memory safety.

1y agoHN ↗

The evaluation order is _unspecified_, not undefined behaviour.

1y agoHN ↗

Interestingly, at least in C++, this was changed in the recent past. It used to be that evaluation of arguments was not sequenced at all and if any evaluation touched the same variable, and at least one was a write, it was UB.

It was changed as part of the C++11 memory model and now, as you said, there is a sequenced-before order, it is just unspecified which one it is.

I don't know much about C, but I believe it was similarly changed in C11.

1y agoHN ↗

Yes, but that's just a subset of expressions where unspecified sequencing applied. For instance, the example with two `print()` as parameters would have a sequence point (in pre-C++11 terminology) separating any reads/writes inside the `print` due to the function calls. It would never be UB even though the order in which the prints are called is still unspecified.

1y agoHN ↗

IIRC the point was that there was no sequence point between argument evaluation, so for example f(++i, ++i) was UB. Or maybe it was only for builtin operators?

Cppreference is not authoritative[1], but seems to support my recollection. In fact it states that the f(++i, ++i) was UB till C++17.

[1] https://en.cppreference.com/w/cpp/language/eval_order.html, Pre C++11 Ordering Rules, point (2).

1y agoHN ↗

`f(++i, ++i)` is/was indeed UB, but the example in munificent's comment was `foo(print(1), print(2))` which as far as I know is not even if both `print` calls read/write the same memory.

1y agoHN ↗

(5) in the paragraph I mentioned earlier seems to prevent interleaving of function calls, which admittedly would make the language hard to use. So I think you are right.

1y agoHN ↗

Sure, prior to the C++ 11 memory model there just isn't a memory ordering model in C++ and all programs in either C or C++ which would need ordering for correctness did not have any defined behaviour in the language standard.

This is very amusing because that means in terms of the language standard Windows and Linux, which both significantly pre-date C++ 11 and thus its memory model, were technically relying on Undefined Behaviour. Of course, as operating systems they're already off piste because they're full of raw assembly and so on.

Linux has its own ordering model as a result, pre-dating the C++ 11 model. Linus is writing software for multi-processor computers more than a decade before the C++ 11 model so obviously he can't wait around for that.

[Edit: Corrected Linux -> Linux when talking about the man]

1y agoHN ↗

It is not so much that windows and linux were relying on UB, but that these platforms, with their compilers, provided guarantees beyond the standard. e.g. GCC not only aims for C/C++ standard compliance, but also POSIX.

Of course these guarantees were often not fully written down nor necessarily self consistent (but then again, neither is the current standard).

1y agoHN ↗

There is plenty of undefined behavior that can't lead to violating memory safety. For example, in many languages, argument evaluation order is undefined. If you have some code like:

You are mixing up non-determinism and UB. Sadly that's a common misunderstanding.

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.

1y agoHN ↗

I bet not even 5% of all programs are multi-threaded, or even concurrent.

Memory safety is a much bigger problem.

1y agoHN ↗

The sad thing is that most languages with threads have a default of global variables and unrestricted shared memory access. This is the source of the vast majority of data corruption and races. Processes are generally a better concurrency model than threads, but they are unfortunately too heavyweight for many use cases. If we defaulted to message passing all required data to each thread (either by always copying or tracking ownership to elide unnecessary copying), most of these kinds of problems would go away.

In the meantime, we thankfully have agency and are free to choose not to use global variables and shared memory even if the platform offers them to us.

1y agoHN ↗

Message passing can easily lead to more logical errors (such as race conditions and/or deadlocks) than sharing memory directly with properly synchronized access. It's not a silver bullet.

1y agoHN ↗

100%.

Some more modern languages - eg. Swift – have "sendable" value types that are inherently thread safe. In my experience some developers tend to equate "sendable" / thread safe data structures with a silver bullet. But you still have to think about what you do in a broader sense… You still have to assemble your thread safe data structures in a way that makes sense, you have to identify what "transactions" you have in your mental model and you still have to think about data consistency.

1y agoHN ↗

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.

1y agoHN ↗

Originally Rust is something altogether different. Graydon has written about that extensively. Graydon wanted tail calls, reflection, more "natural" arithmetic with Python style automatic big numbers, decimal for financial work and so on.

The Rust we have from 1.0 onwards is not what Graydon wanted at all. Would Graydon's language have been broadly popular? Probably not, we'll never know.

1y agoHN ↗

Oh, I do know you can have arbitrary precision. I'm the author of realistic, which isn't "just" arbitrary precision it's an approximation of the computable reals as well, which is sometimes just enough more power than you'd hardly notice you have arbitrary precision too.

https://crates.io/crates/realistic

1y agoHN ↗

Even in pre-1.0 Rust, concurrency was a primary goal; there's a reason that Graydon listed Newsqueak, Alef, Limbo, and Erlang in the long list of influences for proto-Rust.

1y agoHN ↗

And yet it ignored the primary lesson of Erlang: no shared memory access whatsoever, which is what makes it so robust.

1y agoHN ↗

Because the other teams members (IIRC brson and pcwalton) wanted Rust to be as performant as C++, which means you must have a way to have shared memory.

1y agoHN ↗

Which was the ultimate failure of Rust, because as Pony benchmarks have shown, you get safety and speed by proper security and architecture. Rust just survived by lying about the its safeties.

What kills performance are not memory copies, but locks. Parallel nonblocking IO and a non POSIX stdlib will bring you far away from C++ or Rust performance.

1y agoHN ↗

Rust just survived by lying about the its safeties.

strong claim. care to back it up?

1y agoHN ↗

Every time this conversation comes up, I'm reminded of my team at Dropbox, where it was a rite of passage for new engineers to introduce a segfault in our Go server by not synchronizing writes to a data structure.

Swift has (had?) the same issue and I had to write a program to illustrate that Swift is (was?) perfectly happy to segfault under shared access to data structures.

Go has never been memory-safe (in the Rust and Java sense) and it's wild to me that it got branded as such.

1y agoHN ↗

Swift is in the process of fixing this, but it’s a slow and painful transition; there’s an awful lot of unsafe code in the wild that wasn’t unsafe until recently.

1y agoHN ↗

One of the biggest hurdles is just getting all the iOS/macOS/etc APIs up to speed with the thread safety improvements. It won’t make refactoring all that application code any easier, but as things stand even if you’ve done that, you’re going to run into problems anywhere your code makes contact with UI code because there’s a lot of AppKit and UIKit that have yet to make the transition.

1y agoHN ↗

Swift 6 is only painful if you wrote a ton of terrible Swift 5, and even then Swift 5 has had modes where you could gracefully adopt the Swift 6 safety mechanisms for a long time (years?)

~130k LoC Swift app was converted from 5 -> 6 for us in about 3 days.

1y agoHN ↗

Yes and no, our app is considerably larger than 130k LoC. While we’ve migrated some modules there are some parts that do a lot of multithreaded work that we probably will never migrate because they’d need to essentially be rewritten and the tradeoff isn’t really worth it for us.

1y agoHN ↗

It's also painful if you wrote good Swift 5 code but now suddenly you need to closely follow Apple's progress on porting their own frameworks, filling your code base with #if and control flow just to make the compiler happy.

1y agoHN ↗

It is still incomplete and a mess. I don't think they thought through the actual main cases Swift is used for (ios apps), and built a hypothetical generic way which is failing on most clients. Hence lots of workarounds, and ways to get around it (The actor system). The isolated/nonisolated types are a bit contrived and causing real productivity loss, when the old way was really just 'everything ui in main thread, everything that takes time, use a dispatch queue, and call main when done'.

Swift is strating to look more like old java beans. (if you are old enough to remember this, most swift developers are too young). Doing some of the same mistakes.

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.

1y agoHN ↗

You're getting downvoted but I fully agree. The problem with Swift's safety has now moved to the tooling. While your code doesn't fail so often at runtime (still does, because the underlying system SDKs are not all migrated), the compiler itself often fails. Even the latest developer snapshot with Swift 6.2 it's quite easy to make it panic with just... "weird syntax".

A much bigger problem I think are the way concurrency settings are provided via flags. It's no longer possible to know what a piece of code does without knowing the exact build settings. For example, depending on Xcode project flags, a snippet may always run on the main loop, or not at all or on a dedicated actor all together.

A piece of code in a library (SPM) can build just fine in one project but fail to build in another project due to concurrency settings. The amount of overhead makes this very much unusable in a production / high pressure environment.

1y agoHN ↗

Their goal was always to be able to evolve to the point of being able fully replace C, Objective-C and C++ with Swift, it has been on their documentation and plenty of WWDC sessions since the early days.

1y agoHN ↗

Mostly because it was a remarkable improvement over what came before (and what came before was hilariously fragile).

1y agoHN ↗

Only for those not paying attention outside mainstream, or too young to remember former languages.

1y agoHN ↗

or too young to remember former languages.

Do you have any good examples? Not trying to argue, just genuinely curious as someone who hasn't been in this field for decades.

1y agoHN ↗

Basically go was designed ignoring all the research and progress that had been made in programming languages until then.

It was designed with contempt for developers, for example disallowing developers to create generic data structures, or lacking a decent way of error checking that is not extremely error prone and verbose.

1y agoHN ↗

I'm certainly not disagreeing, but I will note that by definition, most people are in the mainstream, so something being a remarkable improvement over what came before (in the mainstream) is a remarkable improvement (for most people).

1y agoHN ↗

It was certainly not a remarkable improvement in the sense of being memory safe even in the face of race conditions. As the article points out, Java and C# both managed to do that, and both predate Go.

1y agoHN ↗

Safety isn't binary, so your comment makes no sense.

1y agoHN ↗

I’d argue that unsafety is binary. If a normal eng doing normal things can break it without going out of their way to deliberately fool the compiler or runtime, I’d call it unsafe.

1y agoHN ↗

By that definition Rust also counts as unsafe. Even managed languages like C# and Java would be unsafe.

1y agoHN ↗

My impression of the Rust devs is that they’d agree with you about any easy-to-trigger calamities. So would Java contributors. C# might not because MS is institutionally not good about admitting mistakes, but I bet the individual devs would agree over a beer.

1y agoHN ↗

What kinds of breakage do you have in mind though? The number of times I've segfaulted the JVM is tiny.

1y agoHN ↗

Do you have some examples? I think JDK developers make a lot of effort to make sure users bugs will not corrupt the runtime.

1y agoHN ↗

There's a reason why rust devs qualify it as "memory safe" so frequently, we tend to agree that rust is, like virtually every current programming language, unsafe in other ways.

Memory safety is just the source of bugs that we've figured out how to eliminate. It's a significant source of really bad (hard to debug due to action at a distance, high impact, etc) bugs so that's worth a lot, but it's not perfect. And even then we have a more frequently used escape hatch to the memory-unsafe world than would be ideal from a safety perspective for practical reasons.

A more complete version of safety would be achieved with a language that proves code correct to arbitrary specifications. We aren't there yet for there being such a language that is practical for every day use. Personally I'm increasingly optimistic we'll get there sooner rather than later (say, within 20 years). Even then there will probably be specification level bugs that prevent a claim of complete safety...

1y agoHN ↗

"Memory safety" has a specific, binary definition.

1y agoHN ↗

this discussion should be ample evidence that it very clearly does not

1y agoHN ↗

It is kind of wild that for a 21st century programming language, the amount of stuff in Go that should have been but never was, but hey Docker and Kubernetes.

1y agoHN ↗

On the flip side, what would be the point? There are already a million other languages that have everything and the kitchen sink.

Not going down the same road is the only reason it didn't end up on the pile of obscure languages nobody uses.

1y agoHN ↗

The only reason it didn't end on pile of obscure languages nobody uses, it called Google, followed by luck with Docker and Kubernetes adoption on the market, after they decided to rewrite from Python and Java respectively into Go, after Go heads joined their teams.

Case in point, Limbo and Oberon-2, the languages that influenced its design, and authors were involved with.

1y agoHN ↗

The only reason it didn't end on pile of obscure languages nobody uses, it called Google

Dart ended up on the pile of languages nobody uses. And Carbon? What's Carbon? Exactly!

Case in point, Limbo and Oberon-2, the languages that influenced its design

Agreed. Limbo and Oberon-2, as primitive as they may look now, had the kitchen sinks of their time. Why wouldn't they have ended up on the pile of languages nobody uses?

1y agoHN ↗

People love to bring those as counter examples, without actually knowing a single fact about them.

Dart was a victim of internal politics between the Chrome team, Dart team, AdWords moving away from GWT wanting AngularDart (see Angular documentary), and the Web in general.

Had Chrome team kept pushing DartVM, it might have been quite different story.

Carbon, good example of failure to actually know what the team purposes are. It is officially a research project for Google themselves, where the team is the first to advise using Rust or another MSL.

One just needs to actually spend like a couple of minutes on their wiki, but I guess that is asking too much on modern times.

Limbo and Oberon-2 were definitely not kitchen sinks of their time, their failure was that neither Bell Labs in 1996, nor ETHZ in 1992, were that relevant for the programming language community in the industry.

1y agoHN ↗

Had Chrome team kept pushing DartVM, it might have been quite different story.

Trouble with that line of thinking is that Google never pushed Go either. It didn't even bother to use it internally (outside from the occasional side project here and there). Google paid some salaries. I'll give you that. But it has paid salaries for a lot of different languages. That is not some kind of secret sauce.

It is officially a research project for Google themselves

It's not just a research project. It is officially "not ready for use", but its roadmap has a clear "ready for use" plan in the coming months. Rust was also "not ready for use" when it hit the streets, it officially being a Mozilla research project, but every second discussion on HN was about it and what is to come. And that was without Google backing. If what you say is true, why isn't Carbon being shouted from every rooftop right now?

I know you're struggling to grasp at straws here, but let's just be honest for a moment: If it hasn't caught attention already, it isn't going to. Just another language to add to the pile.

1y agoHN ↗

The strength of Go is not the language. It's that the libraries you need for web back-end stuff are written, maintained, and used in production by Google. All the obscure cases get exercised in production due to sheer volume of internal usage.

At one time, Go maps were not thread-safe. Was that fixed?

1y agoHN ↗

I'd be surprised if the JSON module was used within Google, though. It's neither particularly fast nor particularly convenient nor particularly suited to properly handle edge cases. But it's still in the stdlib for compatibility reasons.

1y agoHN ↗

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.

1y agoHN ↗

I don't think that's the (only) reason Go became popular. The huge thing about Go is the runtime: it's the only language runtime available today, at least in any language with a large org behind it, that offers (a) GC, (b) fast start-up time, (c) static types, (d) fast execution, and (e) multi-threading.

This is a killer combination for any team looking to write code for auto-scalable microservices, to run for example on Kubernetes. Java is not great in this niche because of its slow startup time, relatively large memory overhead, and the need for warm-up before code actually starts executing fast (so scaling up and down has a very large cost for Java services). .NET has similar problems, and also a huge container size. Python is far too slow, and not typed. TypeScript is single threaded, and still has a pretty hefty runtime. OCaml doesn't have any large org behind it, is quite obscure syntax, and was still single-threaded at the time Kubernetes started. Haskell has similar issues, and is also large and slow starting. Rust, C++, C all require manual memory management.

So, it's no surprise that Go was used for Kubernetes services themselves, and it's no surprise that people designing for Kubernetes mostly chose to write their new stuff in Go. Go the language, with its antiquated design, is actually quite secondary to all of that. But Go's runtime is completely unmatched in this space.

1y agoHN ↗

F# does that too.

.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++).

1y agoHN ↗

Not sure what you mean about F# - being a CLR language, it has the same runtime issues as C# (and IronPython, managed C++, etc).

The article you quote is a toy example - if you write a C# or F# web API server, you'll see that it takes up way more space than a Go one with similar functionality (and has way higher memory overhead as well). A Go API web server is maybe 10MB on disk, with no dependencies (that is, you can run it perfectly in a container that is defined as `FROM scratch; COPY my-go-exec /my-go-exec `). The equivalent Java or .NET container is somewhere around 2-400MB at the minimum.

As for the syntax and constructs, I don't care so much. If OCaml or SML had comparable support and a comparable ecosystem to Go, I'd bet plenty of people would have chosen them instead.

1y agoHN ↗

OP's point is about why Go took off, which happened more than a decade ago.

Native AOT compilation didn't exist for .NET then, and .NET was, for all intents and purposes, Windows-only.

(Last I checked, the AOT story for C# is not that great even today. No idea about F#.)

1y agoHN ↗

The issue is that some people still fighting against the concepts ML family languages

To be fair, everyone was fighting against ML concepts at the time. Ruby on Rails was "in" and "doing situps" was "out". Go was built for the time it was created. It was, quite explicitly as told at its launch announcement, made to be a "dynamically typed" language with static type performance. It is unlikely it would have had a static type system at all if they knew how to achieve the same performance optimizations without a type system.

I think Rust opted for the best combinations

But built in another time. Ruby on Rails was "out" and static typing (ML style in particular) was "in" by the time Rust finally got around to showing up to the party. Looking back, it may not seem like there was much time between the creation of Go and the creation of Rust, but on the tech scale it was created eons later. The fashion of tech can change on a dime — as captured in the humorous fable about JS having a new "must-use" framework every week.

The fashion trends will change again at some point. They always do.

1y agoHN ↗

Kubernetes services are one of the places where you don't care about startup time. Likewise for Docker itself. These are the things that do the scaling, normally.

Go is not particularly fast. People often see that Java gets faster as it runs and thinks, oh, it must be slow at the start then. But when you compare like with like, Go ends up being stuck at the bottom of the curve that Java then sharply climbs. The difference in GC quality is big, or at least, used to be? The only place where you really notice the difference is command line tools, and Java has GraalVM for that.

1y agoHN ↗

Kubernetes services are one of the places where you don't care about startup time.

There are some kubernetes services that scale up and down. And even for those that don't normally, if they have some kind of failure, the difference between taking a millisecond to get back up and taking a second can actually matter for a web host.

Go is not particularly fast. People often see that Java gets faster as it runs and thinks, oh, it must be slow at the start then. But when you compare like with like, Go ends up being stuck at the bottom of the curve that Java then sharply climbs.

Go starts up much faster than Java. And Go code runs measurably faster than interpreted Java code, even though it's slower than the JITed code you'll eventually have if your JVM runs long enpigh. But un-JITed Java code is very slow, more comparable to Python than JITed Java or with Go . This has nothing to do with the GC - where I do agree Go is mediocre at best.

1y agoHN ↗

When using HPAs to quickly ramp replica sets up and down based on load, startup time is absolutely an important factor. You want services to start within hundreds of milliseconds at most.

1y agoHN ↗

Well, that and the slight fact that it bears Google's brand name.

I personally appreciate Go as a research experiment. Plenty of very interesting ideas, just as, for instance, Haskell. I don't particularly like it as a development language, but I can understand why some people do.

1y agoHN ↗

Plenty of very interesting ideas

Is there? When you get down to it, it is really just a faster Python. Which is exactly what it was said to be when it was released. Their goal was to create a "dynamically-typed" language that was more performant. It is likely that it wouldn't have had a static type system at all if they figured out how to achieve on the performance end without needing types.

You can tell who is clueless when you hear someone say its type system is lacking. I mean, technically it is, but it is supposed to be. Like saying Javascript or Ruby's type system is lacking.

1y agoHN ↗

Two examples of interesting ideas:

- using zero values as an optimization mechanism;

- (non-)pointers and passing self by copy.

I mean, I hate both mechanisms, but intellectually, I find them quite interesting.

Also, I'd not classify it as a faster Python. It's more of a cousin of Obj-C if the authors of Obj-C had fallen in love of Erlang instead of Smalltalk.

1y agoHN ↗

Those ideas where already present in C/C++ decades prior. BSS program area. And passing struct by value vs pointer.

1y agoHN ↗

Faster Python with a very small set of its capabilities.

1y agoHN ↗

Having the weight of Google behind it is the primary reason it didn't end up on the pile of obscure languages nobody uses.

1y agoHN ↗

Oh...? Dart never gained much steam. And let's not forget about Carbon! Can you name even just one person who has tried Carbon? Have more than a handful of people even heard of Carbon?

I will grant you that Carbon is still in its infancy, but when Rust was in the same youthful stage we never heard an end to all the people playing with it. You, even if not tried it yourself, definitely knew about it.

You've made up a fun idea, but reality doesn't support it. Google has not shown its weight carries anything. They have really struggled to get any for-profit business units off the ground since they gained the weight, never mind their hobbies! If anything, Google is detrimental to a project.

1y agoHN ↗

Already explained in another thread, learn the politics of Dart, and Carbon is still on the drawing board.

1y agoHN ↗

Already...? Said "explanation" was posted over an hour after the comment replied to here.

If only Google put their weight into a watch, maybe you'd have one?

Oh wait. They did! Google can't successfully turn their weight into much of anything. Go's success, if we can call it that, clearly happened in spite of Google.

1y agoHN ↗

So failures are some deep valid reasons whereas success is developers don't know any better language.

1y agoHN ↗

Necessary and sufficient are two separate concepts.

1y agoHN ↗

I am curious. Generally basic structures like map are not thread safe and care has to be taken while modifying it. This is pretty well documented in go spec. In your case in dropbox, what was essentially going on?

1y agoHN ↗

I thought the same thing. Maybe the point of the story isn’t “we were surprised to learn you had to synchronize access” but instead “we all thought we were careful, but each of us made this mistake no matter how careful we tried to be.”

1y agoHN ↗

In Java, there are separate synchronized collections, because acquiring a lock takes time. Normally one uses thread-unsafe collections. Java also gives a very ergonomic way to run any fragment under a lock (the `synchronized` operator).

Rust avoids all this entirely, by using its type system.

1y agoHN ↗

Java has separate synchronized collections only because that was initially the default, until people realized that it doesn’t help for the common cases of check-and-modify operations or of having consistency invariants with state outside a single collections (besides the performance impact). In practice, synchronized collections are rarely useful, and instead accesses are synchronized externally.

1y agoHN ↗

I think the surprise here is that failing to synchronize writes leads to a SEGFAULT, not a panic or an error. This is the point GP was making, that Go is not fully memory safe in the presence of unsynchronized concurrent writes. By contrast, in Java or C#, unsynchronized writes will either throw an exception (if you're lucky and they get detected) or let the program continue with some unexpected values (possibly ones that violate some invariants). Getting a SEGFAULT can only happen if you're explicitly using native code, raw memory access APIs, or found a bug in the runtime.

1y agoHN ↗

Segfault sounds better than running with inconsistent data.

1y agoHN ↗

Go/C programs that race can also run with inconsistent data. Nothing guarantees you a segfault under torn writes.

In practice, Java programs tend to pick up on data races very quickly because they mutate some collection and the collections framework has safety checks for this.

1y agoHN ↗

The inconsistent data thing could happen in Go too. A segfault is not guaranteed, it’s just one of the more likely possibilities.

1y agoHN ↗

A segfault is not guaranteed, it’s just one of the more likely possibilities.

Is it? It will depend on the code, but my gut feeling is that you typically would get a few (if not lot of) unnoticed non-segfaulting issues before you get the segfaulting one that tells you straight in your face that you have a problem.

1y agoHN ↗

It probably depends on how exactly the corruption happens. If you overwrite a pointer with an integer value, then the integer is statistically unlikely to correspond to a valid memory address. On the other hand, if you overwrite a pointer with a pointer, or an integer with an integer, all bets are off.

1y agoHN ↗

No, SEGFAULT means you're lucky and your corrupted memory caused you to access something the OS knew you can't access. But every SEGFAULT means that you have a memory safety violation, and so if you get unlucky, the exact same code that SEGFAULTED once will read oe write random objects in your memory (which might include code areas, GC data structures, etc).

Inconsistent data is pretty bad, but it's not as bad as memory corruption.

1y agoHN ↗

Well it depends on what we mean by 'inconsistent'.

In C# For example, if a structure is over CPU arch Word size (i.e. 32 or 64 bits) then you could have a torn read if it's being written. However object refs themselves are always word size so you'll never have a torn pointer read on those.

However, in either case there is still a need in multithreaded environments to remember the CPU's memory ordering rules and put proper fences (or, to be safe, locks, since memory barrier rules are different between ARM and x86 for example).

But that second bit is a fairly hard problem to solve for without having the right type of modelling around your compiler.

1y agoHN ↗

When I said "inconsistent", I was referring to things like getting a length field updated by one thread, but the actual list contents by another - if you have thread safety violations you will end up with exactly this type of issue in any language that allows unsafe threading code (Rust wouldn't outside `unsafe` blocks, for example), even in fully memory safe ones like Java or C#, and even without any bugs in the VM.

1y agoHN ↗

Crashing on shared access is the safe thing to do

1y agoHN ↗

An intentional exit by a runtime is a safe crash. A segfault is not, and is here a clear sign that memory safety has been violated.

1y agoHN ↗

I guess I was thinking specifically of the swift case where values have exclusive access enforcement. Normally caught by a compiler, they will safely crash if the compiler didn’t catch it. I think the only way to segfault would be by using Unsafe*Pointer types, which are explicitly marked unsafe

1y agoHN ↗

Yeah it's not the segfault that's bad, it's when it's when the write to address 0x20001854 succeeds and now some hapless postal clerk is going to jail.

1y agoHN ↗

a segfault is completely unintentional. Had the kernel been older it could be used to execute code.

1y agoHN ↗

Faulted trying to access 0x10 - the offset in the string we were trying to read from :)

Is guaranteed that every offset you can try to read is guaranteed to create a segfault?

1y agoHN ↗

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.

1y agoHN ↗

"Crashing" is a very positive spin. "The heap getting the corrupted until it was killed by the operating system" is another interpretation.

1y agoHN ↗

Right, the issue here is that the "Rust and Java sense" of memory safety is not the actual meaning of the term. People talk as if "memory safety" was a PLT axiom. It's not; it's a software security term of art.

This is just two groups of people talking past each other.

It's not as if Go programmers are unaware of the distinction you're talking about. It's literally the premise of the language; it's the basis for "share by communicating, don't communicate by sharing". Obviously, that didn't work out, and modern Go does a lot of sharing and needs a lot of synchronization. But: everybody understands that.

1y agoHN ↗

People talk as if "memory safety" was a PLT axiom. It's not; it's a software security term of art.

It's been in usage for PLT for at least twenty years[1]. You are at least two decades late to the party.

    Software is memory-safe if (a) it never references a memory location outside the address space allocated by or that entity, and (b) it never executes intstruction outside code area created by the compiler and linker within that address space.

[1]https://llvm.org/pubs/2003-05-05-LCTES03-CodeSafety.pdf

1y agoHN ↗

Not GP, but that definition seems not to be the one in use when describing languages like Rust--or even tools like valgrind. Those tools value a definition of "memory safety" that is a superset (a big one) of the definition referenced in that paper: safety as preventing incorrect memory accesses within a program, regardless of whether those accesses are out of bounds/segmentation violations.

1y agoHN ↗

it's not, but for a very subtle reason. To prove memory safety, you need to know that the program never encounters UB (since at that point you have nothing known about the program)

1y agoHN ↗

...by that definition, can a C program be memory safe as long as it doesn't have any relevant bugs, despite the choice of language? (I realize that in practice, most people are not aware of every bug that exists in their program.)

1y agoHN ↗

This is way outside my domain but isn’t the answer: yes, if the code is formally proven safe?

Doesn’t NASA have an incredibly strict, specific set of standards for writing safety critical C that helps with writing programs that can be formalized?

1y agoHN ↗

There are safety recommendations / best practice standards like CERT. None of them will prevent you from making intentional looking but logically unsound memory unsafe operations with C and C++. The code can be very indistinguishable from safe code. The things that C and C++ allow you to do basically makes code written in those languages impossible to fully formally prove. Although there are subsets, the basic integer operations and primitive types are messed up with C. So without uprooting how basic integer and pointer types work, it is impossible to make C and C++ safer. Such change will make all C and C++ programs invalid.

C and C++ always defaults to minimum amount of safety for maximum allowance of the compiler interpretation. The priority of the language designers of them is keeping existing terrible code running as long as possible first, letting compilers interpret the source code as freely as possible second.

That's why many military and aerospace code actually uses much safer and significantly more formally verifiable Ada.

1y agoHN ↗

Thanks for explaining that. It really fills in a lot for me.

1y agoHN ↗

There is also a lot of formally verified in C.

1y agoHN ↗

impossible to formally prove

If you assume the entire lang, yes. If you use a large subset, no. Furthermore, compiler interpretation might actually be sane! There are more compilers out there than GCC, Clang or MSVC. I suspect many assumptions are being made on this claim.

1y agoHN ↗

It just seems like a bad definition (or at least ambiguous), it should say "cannot", or some such excluding term. By the definition as given if a program flips a coin and performs an illegal memory access, are runs where the access does not occur memory safe?

1y agoHN ↗

Can a C program be memory safe as long as it doesn't have any relevant bugs? Yes, and you can even prove this about some C programs using tools like CBMC.

1y agoHN ↗

Sure. It can be. In the same way, a C program can be provably correct. I.e., for all inputs it doesn't exhibit unexpected behavior. Memory safety and correctness are properties of the program being executed.

But a memory-safe program != memory safe language. Memory safe language helps you maintain memory-safety by reducing the chances to cause memory unsafety.

1y agoHN ↗

There is a huge difference between "a C program can be memory safe if it is proven to be so by an external static analysis tool" and "the Java/Rust language is memory safe except for JNI resp unsafe sections of the code".

1y agoHN ↗

I agree that there are two groups here talking past each other. I think it would help a lot to clarify this:

the issue here is that the "Rust and Java sense" of memory safety is not the actual meaning of the term

So what is the actual meaning? Is it simply "there are no cases of actual exploited bugs in the wild"?

Because in another comment you wrote:

a term of art was created to describe something complicated; in this case, "memory safety", to describe the property of programming languages that don't admit to memory corruption vulnerabilities, such as stack and heap overflows, use-after-frees, and type confusions. Later, people uninvolved with the popularization of the term took the term and tried to define it from first principles, arriving at a place different than the term of art.

But type confusion is exactly what has been demonstrated in the post's example. So what kind of memory safety does Go actually provide, in the term of art sense?

1y agoHN ↗

It's a contrived type confusion bug. It reads 42h because that address is hardcoded, and it does something that ordinary code doesn't do.

If you were engaged to do a software security assessment for an established firm that used Go (or Python, or any of the other mainstream languages that do shared-memory concurrency and don't have Rust's type system), and you said "this code is memory-unsafe", showing them this example, you would not be taken seriously.

If people want to make PLT arguments about Rust's correctness advantages, I will step out of the way and let them do that. But this article makes a security claim, and that claim is in the practical sense false.

1y agoHN ↗

I have recently come to the conclusion that everything I ever thought was "contrived" is currently standard practice in some large presently existing organization.

1y agoHN ↗

Take that to the Apple bounty program with your crasher bug and tell them they should pay out as if you'd confirmed RCE, see how it goes. This is an engineering question; it's not about vibes.

It's not even always the case that corrupted data structures (or even pointers) in C code are exploitable. You need attacker control of data and where it goes in memory. It's far less often the case in Python or Go --- in fact, it's basically never the case. As evidence for that claim: the zero memory corruption RCEs in all of shipping Go code, of which there is a lot.

1y agoHN ↗

Dunno about Apple, but goog sometimes pays out bugs that are "theoretical" in the way you describe. That is, you show that there's a bug somewhere, but you can't "reach" it from user data. They'll pay less than a PoC, obviously, but will pay. YMMV, etc.

1y agoHN ↗

You are however replying to thread where a Dropbox engineer calls it "a right of passage" to introduce such bugs to their codebase. Which suggests that it is by no means unheard of for these problems to crop up in real-world code.

1y agoHN ↗

Doesn't Dropbox write a lot of Python extensions in C for speedup?

1y agoHN ↗

Excuse me, but in this thread we are bashing go, not making logical arguments.

1y agoHN ↗

Many FAANG & co engineers are overrated. If every new hire is introducing concurrency bugs in a Golang codebase, refactor, do better review and maybe use concurrency questions instead of leetcode.

I’ll take tptacek’s word over most FAANG type on such topics if we’re doing appeals to authority. The guy is very practical, unlike the Rust community which is incredibly focused on theoretical correctness instead of real-world experiences.

1y agoHN ↗

Again: introducing surprising correctness bugs? Crashing programs? Absolutely. I don't know how many different ways I can say that my concern here is the misuse of a security term of art. Dropbox engineers do not have as a rite of passage introducing or finding RCE vulnerabilities in Go code. Would that it were so! My job would be much more interesting.

1y agoHN ↗

correctness bugs? Crashing programs? Absolutely.

Denial of service can absolutely be a security issue, as can any correctness bug if it leads to unintended behavior or corrupted data.

1y agoHN ↗

My own takeaway after looking at corporate codebases for four decades is that the state of the art in software development at banks, governments, insurance companies, airlines, health care and so on is such that I long for the time before the internet.

Sure, those mainframes from the 80's weren't bullet proof either. But you first had to get to them. And even if the data traveled in plain text on leased lines (point-to-point but not actually point-to-point (that would require a lot of digging), no multiplexing) you had to physically move to the country where they were located to eavesdrop on them, and injecting data into the stream was a much harder problem.

1y agoHN ↗

It reads 42h because that address is hardcoded,

It is trivial to change this example into an arbitrary int2ptr cast.

Go (or Python, or any of the other mainstream languages that do shared-memory concurrency and don't have Rust's type system),

As the article discusses, only Go has this issue. Python and Java and JavaScript and so on are all memory-safe. Maybe you are mixing up "language has data races" and "data races can cause the language itself to be broken"?

If people want to make PLT arguments about Rust's correctness advantages, I will step out of the way and let them do that. But this article makes a security claim, and that claim is in the practical sense false.

This article makes a claim about the term "memory safety". You are making the claim that that's a security term. I admit I am not familiar with the full history of the term "memory safety", but I do know that "type safety" has been used in PLT for many decades, so it's not like all "safety" terms are somehow in the security domain.

I am curious what your definition of "memory safety" is such that Go satisfies the definition. Wikipedia defines it as

Memory safety is the state of being protected from various software bugs and security vulnerabilities when dealing with memory access, such as buffer overflows and dangling pointers.

My example shows that Go does not enforce memory safety according to that definition -- and not through some sort of oversight or accident, but by design. Out-of-bounds reads and writes are possible in Go. The example might be contrived, but the entire point of memory safety guarantees is that it doesn't matter how contrived the code is.

I'm completely fine with Go making that choice, but I am not fine with Go then claiming to be memory safe in the same sense that Java or Rust are, when it is demonstrably not the case.

1y agoHN ↗

The problem isn't that you couldn't hardcode a scarier value; it's that you have to demonstrate a plausible scenario in realistic code where an attacker controls both the value and the address it's written to.

While you're wondering why I keep claiming Go is a memory-safe language, you can also go ask the ISRG, which says the same thing I am at (checks notes) https://www.memorysafety.org/.

1y agoHN ↗

While you're wondering why I keep claiming Go is a memory-safe language, you can also go ask the ISRG, which says the same thing I am at

And yet Go violates the definition they give -- it doesn't prevent out-of-bounds accesses. (And just to be sure we're talking about the same thing, I'm specifically talking about Go here. All the other languages on their list are actually memory safe, as far as I know.)

you have to demonstrate a plausible scenario in realistic code where an attacker controls both the value and the address it's written to.

So your definition of memory safety includes some notion of "plausible" and "realistic"? Neither https://www.memorysafety.org/docs/memory-safety/ nor Wikipedia have such a qualification in their definition. It would help if you could just spell out your definition in full, rather than having us guess.

1y agoHN ↗

But: everybody understands that.

Everybody does not understand that otherwise there would be zero of these issues in shipping code.

This is the problem with the C++ crowd hoping to save their language. Maybe they'll finally figure out some --disallow-all-ub-and-be-memory-safe-and-thread-safe flag but at the moment it's still insanely trivial to make a mistake and return a reference to some value on the stack or any number of other issues.

The answer can not be "just write flawless code and you'll never have these issues" but at the moment that's all C++, and Go, from this article has.

1y agoHN ↗

Again: if you want to make that claim about correctness bugs, that's fine, I get it. But if you're trying to claim that naive Go code has memory safety security bugs: no, that is simply not true.

1y agoHN ↗

I cannot find anyone in this thread (nor in the article) making the claim you are arguing against, though... the reason for the example isn't "this demonstrates all Go code is wrong", but merely that "you can't assume that all Go code is correct merely because it is written in Go"; now, most code written in Go might, in fact, be safe, and it might even be difficult to write broken Go code; but, I certainly have come across a LOT of people who are claiming that we don't even have to analyze their code for mistakes because it is written in Go, which is not the case, because people do, in fact, share stuff all over the place and Go, in fact, doesn't prevent you from all possible ways of writing broken code. To convince these people that they have to stop making that assumption requires merely any example of code which fails, and to those people these examples are, in fact, elucidating. Of course, clearly, this isn't showing a security issue, but it isn't claiming to be; and like, obviously, this isn't something that would be sent to a bug bounty program, as who would it even be sent to? I dunno... you seem to have decided you want to win a really minute pedantic point against someone who doesn't exist, and it makes this whole thing very confusing.

1y agoHN ↗

The terms correctness (from a PLT perspective) and safety (from a security perspective) are not equivalent and interchangeable. I see them mixed up too much in this discussion.

1y agoHN ↗

PLT has used the term "type safety" for a very long time -- so "safety" does not imply a security perspective. And yes it is indeed very different from correctness. But the article doesn't claim that memory safety should imply correctness -- that would be ridiculous, obviously you can write buggy programs in memory-safe languages. The article claims that Go is not memory-safe.

1y agoHN ↗

I'm not understanding - if you're able to produce segfaults by, presumably, writing to memory out of bounds, what's stopping a vulnerability? Surely, if I can write past an array's bounds, then I can do a buffer overflow of some sort in some situations?

1y agoHN ↗

This comment highlights a very important philosophical difference between the Rust community and the communities of other languages:

- in other languages, it’s understood that perhaps the language is vulnerable to certain errors and one should attempt to mitigate them. But more importantly, those errors are one class of bug and bugs can happen. Set up infra to detect and recover.

- in Rust the code must be safe, must be written in a certain way, must be proven correct to the largest extent possible at compile time.

This leads to the very serious, solemn attitude typical of Rust developers. But the reality is that most people just don’t care that much about a particular type of error as opposed to other errors.

1y agoHN ↗

... it's understood that perhaps the language is vulnerable to certain errors and one should attempt to mitigate them. But more importantly, those errors are one class of bug and bugs can happen. Set up infra to detect and recover.

in Rust the code must be safe, must be written in a certain way, must be proven correct to the largest extent possible at compile time.

Only for the Safe Rust subset. Rust has the 'unsafe' keyword that shows exactly where the former case does apply. (And even then, only for possible memory unsoundness. Rust does not attempt to fix all possible errors.)

1y agoHN ↗

A not-so-secret secret of Rust is that liberal use of 'unsafe' is pretty much required for certain classes of high-performance code.

1y agoHN ↗

imo if you're sprinkling around `unsafe` in your codebase "liberally", you're holding it wrong. In general it's really not that hard to encapsulate most unsafety into a wide-contract abstraction; I’d argue where Rust really shines is when you take advantage of the type system and static analyzer to automatically uphold invariants for you

1y agoHN ↗

Well yes, only in the safe subset, but the safe subset is the alpha and omega of Rust.

Woe to those that use unsafe, for the scorn of the community shall be upon them. :)

1y agoHN ↗

This leads to the very serious, solemn attitude typical of Rust developers.

Opposite really. I like rust because I can be care free and have fun.

1y agoHN ↗

But: everybody understands that.

I had to convince Go people that you can segfault with Go. Or you mean the language designers with using everybody?

1y agoHN ↗

You can segfault in Rust, too - there's a whole subset of the language marked "unsafe" that people ignore when making "safe language" arguments. The question is how difficult is it to have a segfault, and in Go it's honestly pretty hard. It's arguably harder in Rust but it's not impossible.

1y agoHN ↗

It's impossible in safe Rust (modulo compiler bugs and things like using a debugger to poke in the program's memory from the outside). That's the key difference.

Of course unsafe Rust is not memory safe. That's why it is called like that. :) Go has unsafe operations too (https://go.dev/ref/spec#Package_unsafe), and of course if you use those all bets are off. But as you will notice, my example doesn't use those operations.

1y agoHN ↗

The problem Rust has is that it’s not enough to be memory safe, because lots of languages are memory safe and have been for decades.

Hence the focus on fearless concurrency or other small-scale idioms like match in an attempt to present Rust as an overall better language compared to other safe languages like Go, which is proving to be a solid competitor and is much easier to learn and understand.

1y agoHN ↗

Except that Swift also has safe concurrency now. It's not just Rust. Golang is actually a very nice language for problems where you're inherently dependent on high-performance GC and concurrency, so there's no need to present it as "better" for everything. Nevertheless its concurrency model is far from foolproof when compared to e.g. Swift.

1y agoHN ↗

But that’s bad news for Rust adoption… Worst case for Rust is it takes just some (not all) marketshare from C and C++, because Swift, Golang, Java, Python, TypeScript, etc have cornered the rest.

1y agoHN ↗

A Java program can share mutable state between threads without synchronization, and it will compile and run. In Rust, such a program will not compile.

1y agoHN ↗

Yes, but even so you will never see e.g. an invalid pointer value as the result of a torn memory write. Basically, no matter what you do with threads in Java, it will not segfault.

TFA's point is that (safe) Rust is also like that, but achieves it by restricting all cases where a torn write could be observed through its type system instead of VM's memory model.

1y agoHN ↗

More specifically, Rust prevents data races.

1y agoHN ↗

No, rust forces you to use a mutex but nothing will prevent you from making the mutex too small and creating tearing in your own data structures by sequentially modifying things covered by mutexes so that in between acquisition of the locks you are violating invariants. The borrow checker certainly helps however, but not without cost that was finally minimized when the scoped threads api came along.

Java has a very specific memory model, so the behavior of variables across threads is quite well defined. Basic variables can tear however (a 64bit long on a 32bit architecture) without the volatile keyword and that is quite different than rust.

1y agoHN ↗

To put things in perspective, I posit to you, how many memory unsafe things can you do in Go that isn’t a variant of the same thing?

Or put another way what is the likelihood that a go program is memory unsafe?

1y agoHN ↗

Listens your team had not sufficient review capacity at that time.

1y agoHN ↗

Before Rust, I'd reached the personal conclusion that large-scale thread-safe software was almost impossible -- certainly it required the highest levels of software engineering. Multi-process code was a much more reasonable option for mere mortals.

Rust on the other hand solves that. There is code you can't write easily in Rust, but just yesterday I took a rust iteration, changed 'iter()' to 'par_iter()', and given it compiled I had high confidence it was going to work (which it did).

1y agoHN ↗

I'm pretty surprised by some other comments in this thread saying this is a rare occurrence in go. In my experience it's not rare at all.

1y agoHN ↗

I have a hard time believing that it's common to create SEGFAULT in Go, I worked with the language for a very long time and don't remember a single time where I've seen that. ( and i've seen many data race )

Not synchronizing writes on most data structure does not create a SEGFAULT, you have to be in a very specific condition to create one, those conditions are extremely rares and un-usual ( from the programmer perspective).

In OP blog to triggers one he's doing one of those condition in an infinite loop.

https://research.swtch.com/gorace

1y agoHN ↗

You really have to go hunting for a segfault in Go. The critical sentence in OP article is: in practice, of course, safety is not binary, it is a spectrum, and on that spectrum Go is much closer to a typical safe language than to C. OP just has a vested interest in proving safety of languages and is making a big deal where in practice there is none. People are not making loads of unsafe programs in Go nor deploying as such because it would be pretty quickly detected. This is much different to C and C++.

1y agoHN ↗

This is, in my mind, the trickiest issue with Rust right now as a language project, to wit:

- The above is true

- If I'm writing something using a systems language, it's because I care about performance details that would include things like "I want to spawn and curate threads."

- Relative to the borrow-checker, the Rust thread lifecycle static typing is much more complicated. I think it is because it's reflecting some real complexity in the underlying problem domain, but the problem stands that the description of resource allocation across threads can get very hairy very fast.

1y agoHN ↗

I don't know what you're referring to. Rust's threads are OS threads. There's no magic runtime there.

The same memory corruption gotchas caused by threads exist, regardless of whether there is a borrow checker or not.

Rust makes it easier to work with non-trivial multi-threaded code thanks to giving robust guarantees at compile time, even across 3rd party dependencies, even if dynamic callbacks are used.

Appeasing the borrow checker is much easier than dealing with heisenbugs. Type system compile-time errors are a thing you can immediately see and fix before problems happen.

OTOH some racing use-after-free or memory corruption can be a massive pain to debug, especially when it may not be possible to produce in a debugger due to timing, or hard to catch when it happens when the corruption "only" mangles the data instead of crashing the program.

1y agoHN ↗

It's not the runtime; it's how the borrow-checker interoperates with threads.

This is an aesthetics argument more than anything else, but I don't think the type theory around threads and memory safety in Rust is as "cooked" as single-thread borrow checking. The type assertions necessary around threads just get verbose and weird. I expect with more time (and maybe a new paradigm after we've all had more time to use Rust) this is a solvable problem, but I personally shy away from Rust for multi-threaded applications because I don't want to please the type-checker.

1y agoHN ↗

You know that Rust supports scoped threads? For the borrow checker, they behave like same-thread closures.

Borrow checking is orthogonal to threads.

You may be referring to the difficulty satisfying the 'static liftime (i.e. temporary references are not allowed when spawning a thread that may live for an arbitrarily long time).

If you just spawn an independent thread, there's no guarantee that your code will reach join(), so there's no guarantee that references won't be dangling. The scoped threads API catches panics and ensures the thread will always finish before references given to it expire.

1y agoHN ↗

I'll have to look more closely at scoped threads. What I'm referring to is that compared to the relatively simple syntax of declaring scopes for arguments to functions and return values to functions, the syntax when threads get involved is (to take an example from the Rust Book, Chapter 21):

  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.

1y agoHN ↗

The signature for scoped threads is both simpler and more complicated depending on how you look at it:

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.”)

1y agoHN ↗

This is one of the things that I'm also looking on at Zig like a slow moving car crash about: they claim they are memory safe (or at least "good enough" memory safe if you use the safe optimization level, which is it's own discussion), but they don't have the equivalent to Rust's Send/Sync types. It just so happens that in practice no one was writing enough concurrent Zig code to get bitten by it a lot, I guess...except that now they're working on bringing back first-class async support to the language, which will run futures on other threads and presumably a lot of feet are going to be fired at once that lands.

1y agoHN ↗

IIUC even single-threaded Zig programs built with ReleaseSafe are not guaranteed to be free of memory corruption vulnerabilities; for example, dereferencing a pointer to a local variable that's no longer alive is undefined behavior in all optimization modes.

1y agoHN ↗

Zig's claims of memory safety are a bad joke. Sure, it's easier to avoid memory safety bugs in Zig than it is in C, but that's also true of C++ (which nobody claims is a memory safe language).

1y agoHN ↗

Go is memory safe by the most common definition, does not matter if you have segfault in some scenario.

How many exploits or security issues have there been related to data race on dual word values? I work with Go for the last 10 years and I never heard of such issues. Not a single time.

1y agoHN ↗

The most common definition of memory safe is literally "cannot segfault" (unless invoking some explicitly unsafe operation - which is not the case here unless you think the "go" keyword should be unsafe).

1y agoHN ↗

I don't know the NSA with their white house paper about memory safe language mentioned Go, maybe you should tell that there are wrong.

1y agoHN ↗

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.

1y agoHN ↗

TBH segfaults are not necessarily a sign of memory unsafety, but _unexpected_ segfaults are.

For some examples, Rust (although this is not specific to it) uses stack guard pages to detect stack overflows by _forcing_ a segfault (as opposed to reading/writing arbitrary memory after the usual stack). Some JVMs also expect and handle segfaults when dereferencing null pointers, to avoid always paying the cost for checking them.

1y agoHN ↗

I've never heard anyone define memory safety that way. You can segfault by overflowing stack space and hitting the guard page or dereferencing a null pointer. Those are possible in languages that don't even expose their underlying pointers like Java. You can make Python segfault if you set the recursion limit too high. Meanwhile a memory access bug or exploit that does not result in a segfault would still be a memory safety issue.

Memory safe languages make it harder to segfault but that's a consequence, not the primary goal. Segfaults are just another memory protection. If memory bugs only ever resulted in segfaults the instant constraints are violated, the hardware protections would be "good enough" and we wouldn't care the same way about language design.

1y agoHN ↗

The definition has to do with certain classes of spatial and temporal memory errors. Ie., the ability to access memory outside the bounds of an array would be an example of a spatial memory error. Use-after-free would be an example of a temporal one.

The violation occurs if the program keeps running after having violated a memory safety property. If the program terminates, then it can still be memory safe in the definition.

Segfaults has nothing to do with the properties. There's some languages or some contexts in which segfaults is part of the discussion, but in general, the theory doesn't care about segfaults.

1y agoHN ↗

Both spatial and temporal memory unsafety can lead to segfaults, because that's how memory protection is intended to work in the first place. I don't believe it's feasible to write a language that manages to provably never trip a memory protection fault in your typical real-world system, yet still fails to be memory safe, at least in some loose sense. For example, such a language could never be made to execute arbitrary code, because arbitrary code can just trip a segfault. You'd be left with the sort of type confusion logical error that happens all the time anyway in all sorts of "weakly typed" languages - that's not what "memory safety" is about.

1y agoHN ↗

The violation occurs if the program keeps running after having violated a memory safety property. If the program terminates, then it can still be memory safe in the definition.

I don't know what you're trying to say here. C would also be memory-safe if the program just simply stopped after violating memory safety, but it doesn't necessarily do that, so it's not memory safe. And neither is Go.

1y agoHN ↗

The point is that a segfault is not an indication for memory unsafety. It is the opposite: The OS stops some unsafe access. The problem with C implementations is that it often comes to late and the segfault does not stop a prior unsafe read or write. But this is also an implementation property, you can implement C in a memory safe way as many have shown. Rust has, unfortunately, changed the narrative so that people now believe memory safety is a property of the language, when it is one of the implementation. (there are, of course, language properties that make it harder to implement C in a memory safe way without sacrificing performance and/or breaking ABI).

1y agoHN ↗

The segfault seen here is not a property of the language implementation, it's just a consequence of the address chosen by the attacker: 42. If you replicated this code in C you would get the same result, and if you used an address pointing to mapped memory in Go then the program would continue executing like in similar exploits in C.

The only reason this isn't a more critical issue is because data races are hard to exploit and there aren't lot of concurrent Go programs/system libraries that accept lot of attacker controlled inputs.

1y agoHN ↗

Whether you can a segfault if you access an out-of-bounds address or not is part of the language implementation. An implementation that guarantees a segfault for out-of-bounds accesses is memory safe.

1y agoHN ↗

(EDIT: removed the first part since I realized you were replying to some comment further up, not my example.)

Rust has, unfortunately, changed the narrative so that people now believe memory safety is a property of the language, when it is one of the implementation.

I am not sure I agree with that (the concept of memory-safe languages looong predates Rust), but you can just define a memory-safe language as one where all conforming implementations are memory-safe -- making it a feature of the language itself, not just a feature of a particular implementation.

1y agoHN ↗

I am not really saying the term is not useful, the idea that it is the only legitimate way of memory safety is what I am questioning.

1y agoHN ↗

Segfaults are just the simplest way of exposing a memory issue. It's quite easy to use a race condition to reproduce a state that isn't supposed to be reachable, and that's much worse than a segfault, because it means memory corruption.

Now the big question, as you mention, is "can it be exploited?" My assumption is that it can, but that there are much lower-hanging fruits. But it's just an assumption, and I don't even know how to check it.

1y agoHN ↗

There is no house safety without nuclear warhead detonation safety.

There is no pedestrian safety without mandatory helmet laws.

There is no car safety without driving a tank.

1y agoHN ↗

I have never seen real Go code (i.e. not code written purposefully to be exploitable) that was exploitable due to a data race.

This doesn’t prove a negative, but is probably a good hint that this risk is not something worth prioritizing for Go applications from a security point of view.

Compare this with C/C++ where 60-75% of real world vulnerabilities are memory safety vulnerabilities. Memory safety is definitely a spectrum, and I’d argue there are diminishing returns.

1y agoHN ↗

Maintenance in general is a burden much greater than CVEs. Exploits are bad, certainly, but a bug not being exploitable is still a bug that needs to be fixed.

With maintenance being a "large" integer multiple of initial development, anything that brings that factor down is probably worth it, even if it comes at an incremental cost in getting your thing out the door.

1y agoHN ↗

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?

1y agoHN ↗

Was it open source? Would be interested to know more.

1y agoHN ↗

Yeah, reading binary files in go with an mmap library and the whole file is based on offsets to point to other sections of the file. Damaged file or programming error and segfault.

1y agoHN ↗

How is that a data race? Also, you're using unsafe.

The post is about data races in safe Go leading to crashes or exploits, because things like "eface" (the tuple used to implement interfaces) and slices (again a tuple) are multi-word and thus impossible to update atomically.

This is not an issue in Java because such things always box behind a pointer.

1y agoHN ↗

Note that C#'s Memory<T> (a slice) isn't boxed either, but it stays memory-safe under tearing: the resulting slice might have unintended bounds, but it will either be in-bounds or will throw an exception.

This of course has some overhead, which is why you usually turn it into the cheaper, data race free Span<T>. Go could have the same safety and fix some of the overhead with compiler optimizations, they just don't want to take the trade-off.

1y agoHN ↗

Memory safety is a big deal because many of the CVEs against C programs are memory safety bugs. Thread safety is not a major source of CVEs against Go programs.

It’s a nice theoretical argument but doesn’t hold up in practice.

1y agoHN ↗

A typical memory safety issue in a C program is likely to generate an RCE. A thread-safety issue that leads to a segfault can likely only lead to a DoS attack, unpleasant but much less dangerous. A race condition can theoretically lead to more powerful attacks, but triggering it should be much harder.

1y agoHN ↗

A thread-safety issue does not always lead to a segfault. Here it did because the address written was 42, but if you somehow manage to obtain the address of some valid value then you could read from that instead, and not cause an immediate segfault.

I agree with the sentiment that data races are generally harder to exploit, but it _is possible_ to do.

1y agoHN ↗

A CVE is worse, but a threading bug resulting in corrupted data or a crash is still a bug that needs someone to triage, understand, and fix.

1y agoHN ↗

But it's not why I stopped writing C programs. It's just a bug and I create and fix a dozen bugs every day. Security is the only argument for memory safety that moves mountains.

1y agoHN ↗

It depends on what threads can do. Threads share memory with other threads and you can corrupt the data structure to force the other thread to do an unsafe / invalid operation.

It can be as simple as changing the size of a vector from one thread while the other one accesses it. When executed sequentiality, the operations are safe. With concurrency all bets are off. Even with Go. Hence the argument in TFA.

1y agoHN ↗

All bets aren’t off, we empirically measure the safety of software based on exploits. C memory handling is most of its exploits.

Show me the exploits based on Go parallelism. This issue has been discussed publicly for 10 years yet the exploits have not appeared. That’s why it's a nice theoretical argument but does not hold up in practice.

1y agoHN ↗

This isn't arguing about exploit risks of the language but simply whether or not it meets the definition of memory safe. Go doesn't satisfy the definition, so it's not memory safe. It's quite black & white here.

Nice strawman though

1y agoHN ↗

Are we still have semantic fights about what exactly memory safety means? Why?

1y agoHN ↗

Because people think Golang is immune to bugs that it's not immune from.

1y agoHN ↗

False.

Java got this right. Fil-C gets it right, too. So, there is memory safety without thread safety. And it’s really not that hard.

Memory safety is a separate property unless your language chooses to gate it on thread safety. Go (and some other languages) have such a gate. Not all memory safe languages have such a gate.

1y agoHN ↗

I would recommend reading beyond the title of a post before leaving replies like this, as your comment is thoroughly addressed in the text of the article:

At this point you might be wondering, isn’t this a problem in many languages? Doesn’t Java also allow data races? And yes, Java does allow data races, but the Java developers spent a lot of effort to ensure that even programs with data races remain entirely well-defined. They even developed the first industrially deployed concurrency memory model for this purpose, many years before the C++11 memory model. The result of all of this work is that in a concurrent Java program, you might see unexpected outdated values for certain variables, such as a null pointer where you expected the reference to be properly initialized, but you will never be able to actually break the language and dereference an invalid dangling pointer and segfault at address 0x2a. In that sense, all Java programs are thread-safe.

And:

Java programmers will sometimes use the terms “thread safe” and “memory safe” differently than C++ or Rust programmers would. From a Rust perspective, Java programs are memory- and thread-safe by construction. Java programmers take that so much for granted that they use the same term to refer to stronger properties, such as not having “unintended” data races or not having null pointer exceptions. However, such bugs cannot cause segfaults from invalid pointer uses, so these kinds of issues are qualitatively very different from the memory safety violation in my Go example. For the purpose of this blog post, I am using the low-level Rust and C++ meaning of these terms.

Java is in fact thread-safe in the sense of the term used in the article, unlike Go, so it is not a counterexample to the article's point at all.

1y agoHN ↗

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.

1y agoHN ↗

If a language is "memory safe", by some definition we expect safety from memory faults (for example, not accessing memory incorrectly).

If a language is "memory safe" but not "thread safe", is the result "the language is free from 'memory faults', unless threads are involved"?

Or to put it another way; when used however the term of art is intended, "memory safety" is meant to provide some guarantees about not triggering certain erroneous conditions. "not thread safe" seems to mean that those same erroneous conditions can be triggered by threads, which seems to amount to '"memory safety" does not guarantee the absence of erroneous memory conditions'.

1y agoHN ↗

I guess to also elaborate the point; it's also entirely correct to say "Rust is guaranteed to be memory safe unless 'unsafe' is involved".

1y agoHN ↗

Yeah and Rust is guaranteed to be thread safe unless 'unsafe' is involved, I think

1y agoHN ↗

If a language is "memory safe" but not "thread safe", is the result "the language is free from 'memory faults', unless threads are involved"?

Yes.

If a language is memory safe but not thread safe, then you can race, but the outcome of those races won't be memory corruption or the violation of the language's type system. It will lead to weird stuff, however - just a different kind of weirdness than breaking out of the language's sandbox

1y agoHN ↗

If a language is memory safe but not thread safe, then you can race, but the outcome of those races won't be memory corruption or the violation of the language's type system.

By these definitions, doesn't that mean go is neither memory or thread safe? It looks like concurrent modification can result in memory corruption, e.g. the attempted access 0x42 example in the article

1y agoHN ↗

By these definitions, doesn't that mean go is neither memory or thread safe?

Yes, with the caveat that you can't treat "memory safe" as a binary condition.

The strictest notion of memory safety is what I call GIMSO: "Garbage In, Memory Safety Out". I.e. there does not exist any sequence of bytes you could feed to the compiler that would result in a memory-unsafe outcome at runtime. Java aims for this. Fil-C does too. JavaScript also does.

But there are languages that I think it's fair to consider to be memory safe that offer escape hatches that violate GIMSO. Rust with `unsafe` is an example. C# with `unsafe` is another. Java if you include `sun.misc.Unsafe` (arguably it's not part of the language).

So I think if a language is memory safe, not thread safe, and the memory safety is gated on thread safety, then it's kinda fair to make statements like, "it's memory safe", if you have fine print somewhere that says "but the memory safety does not hold under the following kinds of races".

All of that said, I'd rather we just said that "memory safety" means what I call "GIMSO". But the ship has sailed. Lots of languages are called "memory safe" to mean something like, "you can get memory safety in this language if you obey certain idioms" - and in Rust that means "don't use unsafe" while in Go that means "don't race in certain ways".

1y agoHN ↗

Java also sometimes uses "memory safe" to refer to programs that don't have null pointer exceptions. So in that sense, Java isn't memory safe by construction either.

These terms are used slightly differently by different communities, which is why I discuss this point in the article. But you seem adamant that you have the sole authority for defining these terms so :shrug:

1y agoHN ↗

When those US government articles about how we should switch to memory safe languages come out, they refer to Java as a “memory safe language”.

They also count data race freedom as part of memory safety, which I think is wrong (and contradicts their inclusion of Java and even Go in the list of memory safe languages).

So no, I’m not an authority. I’m just following the general trend of how the term is used.

And ive never heard “memory safe” used in relation to not having null pointer exceptions. That’s a new one and sounds nonsensical, frankly

1y agoHN ↗

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).

For Java, there's no contradiction if you define data race freedom as "data races cannot cause arbitrary memory corruption / UB".

And ive never heard “memory safe” used in relation to not having null pointer exceptions. That’s a new one and sounds nonsensical, frankly

I was also surprised, but it's what I was told by people working on verification of Java programs. And you can see e.g. at https://link.springer.com/content/pdf/10.1007/978-3-030-1750... that people are proving memory safety of Java programs, which would not make sense at all if all Java programs are memory safe by construction.

1y agoHN ↗

It's not that black and white and the solution isn't necessarily pick language X and you'll be fine. It never is that simple.

Basically, functional languages make it easier to write code that is safe. But they aren't necessarily the fastest or the easiest to deal with. Erlang and related languages are a good example. And they are popular for good reasons.

Java got quite a few things right but it took a while for it to mature. Modern day Java is quite a different beast than the first versions of Java. The Thread class, API, and the language have quite a few things in there that aren't necessarily that great of an idea. E.g. the synchronized keyword might bite you if you are trying to use the new green threads implementation (you'll get some nice deadlocks if you block the one thread you have that does everything). The modern java.concurrent package is implemented mostly without it.

Of course people that know their history might remember that green threads are actually not that new. Java did not actually support real threads until v1.1. Version 1.0 only had green threads. Those went out of fashion for about two decades and then came back with recent versions. And now it does both. Which is dangerous if you are a bit fuzzy on the difference. It's like putting spoilers on your fiesta. Using green threads because they are "faster" is a good sign that you might need to educate yourself and shut up.

On the JVM, if you want to do concurrent and parallel stuff, Scala and Kotlin might be better options. All the right primitives are there in the JVM of course. And Java definitely gives you access to all it. But it also has three decades of API cruft and a conservative attitude about keeping backwards compatible with all of that. And not all of it was necessarily that all that great. I'm a big fan of Kotlin's co-routine support that is rooted in a lot of experience with that. But that's subjective of course. And Scala-ists will probably insist that Scala has even better things. And that's before we bring up things like Clojure.

Go provides a good balance between ease of use / simplicity and safety. But it has quite a few well documented blind spots as well. I'm not that big of a fan but I appreciate it for what it is. It's actually a nice choice for people that aren't well versed in this topic and it naturally nudges people in a direction where things probably will be fine. Rust is a lot less forgiving and using it will make you a great engineer because your code won't even compile until you properly get it and do it right. But it won't necessarily be easy (humbled by experience here).

With languages the popular "if you have a hammer everything looks like a nail" thing is very real. And stepping out of your comfort zone and realizing that other tools are available and might be better suited to what you are trying to do is a good skill to have.

IMHO python is actually undervalued. It was kind of shit at all of this for a long time. But they are making a lot of progress modernizing the language and platform and are addressing its traditional weaknesses. Better interpreting and jit performance, removing the GIL, async support that isn't half bad, etc. We might wake up one day and find it doing a lot of stuff that we'd traditionally use JVM/GO/Rust for a few years down the line. Acknowledging weaknesses and addressing those is what I'm calling out here as a very positive thing. Oddly, I think there are a lot of python people that are a bit conflicted about progress like this. I see the same with a lot of old school Java people. You get that with any language that survives that long.

Note how I did not mention C/C++ here so far. There's a lot of it out there. But if you care about safety, you should probably not go near it. I don't care how disciplined you are. Your C/C++ code has bugs. Any insistence that it doesn't just means you haven't found them yet. Possibly because you are being sloppy looking for them. Does it even have tests? There are whole classes of bugs that we can prevent with modern languages and practices. It's kind of negligent and irresponsible not to. There are attempts to make C++ better of course.

1y agoHN ↗

IMHO python is actually undervalued. It was kind of shit at all of this for a long time. But they are making a lot of progress modernizing the language and platform and are addressing its traditional weaknesses. Better interpreting and jit performance, removing the GIL, async support that isn't half bad, etc.

The issue with Python isn't just the GIL and lack of support for concurrency. It uses dynamic types (i.e. variant types) for everything. That's way too slow, it means every single variable access must go through a dispatch step. About the only thing Python has going for it is the easy FFI with C-like languages.

1y agoHN ↗

That’s why I’m quite excited about Cinder, Meta’s CPython fork, that lets the programmer opt in “strict modules” and “static Python”, enabling many optimizations.

1y agoHN ↗

What does any of this have to do with memory safety?

1y agoHN ↗

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.

1y agoHN ↗

Can you point me to the Go definition of memory safety? I searched all over their website, and couldn't find any.

(But also, it'd be kind of silly for every language to make up their own definition of memory safety. Then even C is memory safe, they just have to define it the right way. ;)

1y agoHN ↗

For the purposes of this discussion, sure: 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.

1y agoHN ↗

Honestly what I mostly want is to not have memory leaks. Which somehow stopped being a focus at some point

1y agoHN ↗

Because we have so much memory no one cares if it leaks. <_<

1y agoHN ↗

This is harder than it looks as soon as you start counting abandoned memory (stuff that's still referenced but not actually used.)

1y agoHN ↗

Well if it's still referenced it's still used from my PoV

1y agoHN ↗

If it's never read again then it's not used.

1y agoHN ↗

The "good" news is that Bjarne Stroustrup is right there with you, Bjarne sees eliminating all memory leaks as a high priority for C++ and one of his main goals.

The bad news ought to be obvious, this "goal" is not achievable, it's a fantasy that somehow we should be able to see the future, divine that some value stored won't be needed in the future and thus we don't need to store it. Goals like "We shouldn't store things we can't even refer to" are already solved in languages used today, so a goal to "not have memory leaks" refers only to that unachievable fantasy.

1y agoHN ↗

Curiously, Go itself is unclear about its memory safety on go.dev. It has a few references to memory safety in the FAQ (https://go.dev/doc/faq#Do_Go_programs_link_with_Cpp_programs, https://go.dev/doc/faq#unions) implying that Go is memory safe, but never defines what those FAQ questions mean with their statements about "memory safety". There is a 2012 presentation by Rob Pike (https://go.dev/talks/2012/splash.slide#49) where it is stated that go is "Not purely memory safe", seeming to disagree with the more recent FAQ. What is meant by "purely memory safe" is also not defined. The Go documentation for the race detector talks about whether operations are "safe" when mutexes aren't added, but doesn't clarify what "safe" actually means (https://go.dev/doc/articles/race_detector#Unprotected_global...). The git record is similarly unclear.

In contrast to the go project itself, external users of Go frequently make strong claims about Go's memory safety. fly.io calls Go a "memory-safe programming language" in their security documentation (https://fly.io/docs/security/security-at-fly-io/#application...). They don't indicate what a "memory-safe programming language" is. The owners of "memorysafety.org" also list Go as a memory safe language (https://www.memorysafety.org/docs/memory-safety/). This later link doesn't have a concrete definition of the meaning of memory safety, but is kind enough to provide a non-exaustive list of example issues one of which ("Out of Bounds Reads and Writes") is shown by the article from this post to be something not given to us by Go, indicating memorysafety.org may wish to update their list.

It seems like at the very least Go and others could make it more clear what they mean by memory safety, and the existence of this kind of error in Go indicates that they likely should avoid calling Go memory safe without qualification.

1y agoHN ↗

The definition kind of changed.

At the time Go was created, it met one common definition of "memory safety", which was essentially "have a garbage collector". And compared to c/c++, it is much safer.

1y agoHN ↗

That seems contrasted by Rob Pike's statement in 2012 in the linked presentation being one of the places where it's called "not purely memory safe". That would have been early, and Go is not called memory safe then. It seems like calling Go memory safe is a more recent thing rather than a historical thing.

1y agoHN ↗

Keep in mind that the 2012 presentations dates to 10 months after Rust's first release, and its version of "Memory Safety" was collecting quite a bit of attention. I'd argue the definition was already changing by this point. It's also possible that Go was already discovering their version of "Memory Safety" just wasn't safe enough.

If you go back to the original 2009 announcement talk, "Memory Safety" is listed as an explicit goal, with no carveouts:

"Safety is critical. It's critical that the language be type-safe and that it be memory-safe."

"It is important that a program not be able to derive a bad address and just use it; That a program that compiles is type-safe and memory-safe. That is a critical part of making robust software, and that's just fundamental."

https://youtu.be/rKnDgT73v8s?t=463

1y agoHN ↗

That's a very good point on the timing. Thanks for adding that extra info.

1y agoHN ↗

Rust's first release, and its version of "Memory Safety" was collecting quite a bit of attention

Note that this was not Rust's first stable release, but it's first public release. At the time it was still changing a lot and still had "garbage collected" types.

1y agoHN ↗

Yeah, it was the 0.1 release. I can't remember exactly when Rust entered the general "programming language discourse" on hackernews and /r/programming, but it was somewhere around here. I'm sure the people behind Go would have known about it by this point in time.

And while rust did have optional "garbage collected pointers", it's important to point out that it is not a garbage collected language. The ownership system and borrow checker were very much front-and-centre for the 0.1 release, it was what everyone was talking about.

Actually, my memory is that while the language had syntax to declare garbage collected pointers, it wasn't actually hooked up to a proper garbage collector. It was always more of a "we are reserving the syntax and we will hook it up when needed", and it turns out the ownership system was powerful enough that it was never needed.

1y agoHN ↗

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.

1y agoHN ↗

it met one common definition of "memory safety", which was essentially "have a garbage collector"

This is the first time I hear that being suggested as ever having been the definition of memory safety. Do you have a source for this?

Given that except for Go every single language gets this right (to my knowledge), I am kind of doubtful that this is a consequence of the term changing its meaning.

1y agoHN ↗

True, "have a garbage collector" was never the formal definition, it was more "automatic memory management". But this predates the work on Rust's ownership system and while there were theories of static automatic memory management, all practical examples of automatic memory management were some form of garbage collection.

If you go to the original 2009 announcement presentation for Go [1], not only is "memory-safety" listed as a primary goal, but Pike provides the definition of memory-safe that they are using, which is:

"The program should not be able to derive a bad address and just use it"

Which Go mostly achieves with a combination of garbage collection and not allowing pointer arithmetic.

The source of Go's failure is concurrency, which has a knock-on effect that invalidates memory safety. Note that stated goal from 2009 is "good support for concurrency", not "concurrent-safe".

[1] https://youtu.be/rKnDgT73v8s?t=463

1y agoHN ↗

Thanks! I added a reference to that in the blog post.

Interestingly, in 2012 Rob Pike explicitly said that Go is "not purely memory safe" because "sharing is legal": https://go.dev/talks/2012/splash.slide#49. However it is not entirely clear what he means by that (I was not able to find a recording of the talk), but it seems likely he's referring to this very issue.

"The program should not be able to derive a bad address and just use it"

My example does exactly that, so -- as you say, Go mostly achieves this, but not entirely.

Note that stated goal from 2009 is "good support for concurrency", not "concurrent-safe".

My argument is that being concurrency-unsafe implies being memory-unsafe, for the reasons laid down in the blog post. I understand that that is a somewhat controversial opinion. :)

1y agoHN ↗

Hey! Cards on the table I'm not in love w/ your post, but mostly I'm curious about what discussion or outcome you were hoping for with it. Doesn't this boil down to "bad things will happen if you have data races in your code, so don't have data races in your code". Does it really matter what those bad things are?

1y agoHN ↗

Curiously, Go itself is unclear about its memory safety on go.dev.

Yeah... I was actually surprised by that when I did the research for the article. I had to go to Wikipedia to find a reference for "Go is considered memory-safe".

Maybe they didn't think much about it, or maybe they enjoy the ambiguity. IMO it'd be more honest to just clearly state this. I don't mind Go making different trade-offs than my favorite language, but I do mind them not being upfront about the consequences of their choices.

1y agoHN ↗

Am I missing something or is that bold claim obviously wrong on its face? This seems like a Go deficiency (lack of atomicity for it pointers), not some sort of law about programming languages.

Can you violate memory safety in C# without unsafe{} blocks (or GCHandle/Marshal/etc.)? (No.)

Can you write thread-unsafe code in C# without using unsafe{} blocks etc.? (Yes, just make your integers race.)

Doesn't that contradict the claim that you can't have memory safety without thread safety?

1y agoHN ↗

I wish we had picked a better name than "thread safety". This is really more like "concurrency safety", since it applies even in the absence of hardware threads.

1y agoHN ↗

Threads aren’t hardware, they are OS. Multihreading != multiprocessing.

1y agoHN ↗

Other than in the sense of SMT (Hyper-Threading)? I don't think so. Threads are a software concept.

One can distinguish between native (OS) threads and green (language-runtime) threads which may use a different context-switching mechanism. But that's more of a spectrum in terms of thread-safety; similar to how running multiple threads on a single CPU core without SMT, single CPU core with SMT, multiple CPU cores, with different possible CPU cache coherency guarantees, create a spectrum of possible thread-safety issues.

1y agoHN ↗

Runtime-switched tasks cannot lead to memory unsafety unless multiple OS threads are involved, because that's the only case where torn writes are possible. And a typical configuration of Go will not be running multiple OS threads unless multiple hardware threads (aka "logical cores", "virtual processors" etc.) are available.

1y agoHN ↗

Thread-safety isn’t just about word tearing. It’s also about state invariants in your program. Even if all your threads are green threads on the same OS thread, you still have to guard critical sections that check-and-modify, or that temporarily break the invariants of state that is observable by other threads, when there’s anything in-between that may lead to a context switch. Otherwise, while you may be free of word tearing, you might still have “torn” program state.

On a single-core CPU, word tearing may similarly be absent between OS threads, but you still have to guard any critical section.

I agree with user swiftcoder that there are concurrency issues (like the above) even in the absence of hardware parallelism, or of multiple OS threads (which by themselves don’t imply hardware parallelism). I disagree that “thread-safety” isn’t an appropriate term for them. Those issues are part of what thread-safety was always about.

1y agoHN ↗

I think my contention is that a lot of programmers don't think about thread-safety in those terms - if I write a purely single-threaded program, thread-safety seems like the kind of thing that I don't have to care about (when in reality, the fact that various OS-provided features like signals exist, mean that all software has to deal with concurrency issues)

1y agoHN ↗

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.

1y agoHN ↗

And here I thought the type system and error handling were the two biggest Go warts. You’re now telling me their memory model is basically ”YOLO”?

1y agoHN ↗

I feel like I'm defending Go constantly these days. I don't even like Go!

Go can already ensure "consistency of multi-word values": use whatever synchronization you want. If you don't, and you put a race into your code, weird shit will happen because torn reads/writes are fuckin weird. You might say "Go shouldn't let you do that", but I appreciate that Go lets me make the tradeoff myself, with a factoring of my choosing. You might not, and that's fine.

But like, this effort to blow data races up to the level of C/C++ memory safety issues (this is what is intended by invoking "memory safety") is polemic. They're nowhere near the same problem or danger level. You can't walk 5 feet through a C/C++ codebase w/o seeing a memory safety issue. There are... zero Go CVEs resulting from this? QED.

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."

1y agoHN ↗

Unsafety in a language is fine as long as it is clearly demarcated. The problem with Go's approach is there no clear demarcation of the unsafety, making reasoning about it much more difficult.

1y agoHN ↗

“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 agoHN ↗

You're free to structure code in and around goroutines however you like. No one's forcing you to spray this all over your codebase. Feel free to implement work queues, or the actor pattern, or whatever. No one's forcing you to use whatever frameworks or libraries or patterns; that's all you.

So much of all of this is weirdly entitled. Languages that do things differently exist: Erlang, Pony, Rust. They all make wildly different tradeoffs than Go does, not better, not worse, different. If you think they're better, use 'em. Let your better software win in the market. These weirdo polemics just fan language flamewars.

1y agoHN ↗

Why does it segfault? Because you have not used a sufficiently clever value for the integer that wouldn't when used as an address?

Just wondering.

Realistically that would be quite rare since it is obvious that this is unprotected shared mutable access. But interesting that such a conversion without unsafe may happen. If it segfaults all the time though then we still have memory safety I guess.

The article is interesting but I wish it would try to provide ideas for solutions then.