HNHacker News
TopNewBestAskShowJobs

oconnor663

7,580 karma · joined September 4, 2012

[ my public key: https://keybase.io/oconnor663; my proof: https://keybase.io/oconnor663/sigs/6u7ycNDOZvuO8qNrg69nGayUCyATOnCNq6H9tBV0BsM ]
submissionscomments
oconnor663··on Games Workshop bans staff from using AI
It's important to think about the exact procedure you want to use for verifying something. Running with your thought experiment, let's say we publish "the root hash of the whole world" (not too far off from what Keybase did) each day in the Times. Now I open my phone to read some messages from Billy Bob, and my phone needs to get that hash somehow. This is just a thought experiment, so let's say for the sake of argument that it tells me to walk down to the convenience store, buy a copy of the day's paper, and scan a QR code on page 12. The problem with that arrangement (even in thought experiment land, where I'm happy to perform these steps every day) is that all the evil server needs to do to trick me is to put a doctored copy of the Times in that one newspaper stand. That's not the level of security we were hoping for. To get real security here, I'd need to do some sort of random sampling of newspaper stands distributed across the country, to build confidence that whatever QR code I'm seeing is the same one that everyone else is seeing. And the kicker is, everyone has to do this. We can't just pay one guy to sample the papers every day and tell us what the QR code was, because now our security depends on trusting that one guy, and the whole point of peer-to-peer security is avoiding that kind of centralized trust.

I think this is actually a great way to talk about the difficulty of the problem that Bitcoin solved, and why so many nerds were so interested in the whitepaper, long before all the real money got involved.

oconnor663··on The State of OpenSSL for pyca/cryptography
Another important parameter with BLAKE3 is "Do you want to use threads?" There's no one-size-fits-all answer to that question, but it's also a parameter that ~no other hash function needs.

Fwiw, I think the RustCrypto effort also tends to suffer a bit from over-abstraction. Once every year or two I find myself wanting to get a digest from something random, let's say SHAKE128. So I pull up the docs: https://docs.rs/sha3/latest/sha3/type.Shake128.html. How do you instantiate one of those? I genuinely have no idea. When I point Claude at it, it tells me to use `default` instead of `new` and also to import three different traits. It feels like these APIs were designed only for fitting into high-level frameworks that are generic over hash functions, and not really for a person to use.

There are a lot of old assumptions like "hash functions are padding + repeated applications of block compression" that don't work as well as they used to. XOFs are more common now, like you said. There's also a big API difference between an XOF where you set the length up front (like BLAKE2b/s), and one where you can extract as many bytes as you want (like BLAKE3, or one mode of BLAKE2X).

Maybe the real lesson we should be thinking about is that "algorithm agility" isn't as desirable as it once was. It used to be that a hash function was only good for a decade or two (MD5 was cracked in ~13 years, but it was arguably looking bad after just 6), so protocols needed to be able to add support for new ones with minimal friction. But aside from the PQC question (which is unlikely to fit in a generic framework with classic crypto anyway?), it seems like 21st century primitives have been much more robust. Protocols like WireGuard have done well by making reasonable choices and hardcoding them.

oconnor663··on Is Rust faster than C?
> Despite benefits, I don't actually think the memory safety really plays a role in the usage rate of parallelism.

I can see what you mean with explicit things like thread::spawn, but I think Tokio is a major exception. Multithreaded by default seems like it would be an insane choice without all the safety machinery. But we have the machinery, so instead most of the async ecosystem is automatically multithreaded, and it's mostly fine. (The biggest problems seem to be the Send bounds, i.e. the machinery again.) Cargo test being multithreaded by default is another big one.

oconnor663··on Games Workshop bans staff from using AI
> They aren't useful outside of "financial engineering."

Without disagreeing with your overall point in 99% of cases, we did actually have a good use for pinning things in the Bitcoin blockchain when I worked at Keybase. If you're trying to do peer-to-peer security, and you want to prove not only that the evil server hasn't forged anything (which you do with signatures) but also that it hasn't deleted anything legitimate, "throw a hash in the blockchain" really is the Right Way to solve that problem.

oconnor663··on C Is Best (2025)
> With C you may, if you wish, develop a big sensibility to race conditions, and stay alert. In general it is possible that C programmers have their "bugs antenna" a bit more developed than other folks.

I think there are effects in both directions here. In C you get burned, and the pain is memorable. In Rust you get forced into safe patterns immediately. I could believe that someone who has done only Rust might be missing that "healthy paranoia". But for teaching in general, it's hard to beat frequent and immediate feedback. Anecdotally it's common for experienced C programmers to learn about some of the rules only late in their careers, maybe because they didn't happen to get burned by a particular rule earlier.

> Rust may create a false sense of security, and in the unsafe sections the programmer sometimes, when reviewing the code, is falsely convinced by the mandatory SAFETY comment.

This is an interesting contrast to the previous case. If you write a lot of unsafe Rust, you will eventually get burned. If you're lucky, it'll be a Miri failure. I think this makes folks who work with unsafe Rust extremely paranoid. It's also easier to sustain an that level of paranoia with Rust, because you hopefully only have to consider small bits of unsafe code in isolation, and not thousands of lines of application logic manipulating raw pointers or whatever.

oconnor663··on LLMs Are Not Fun
Ehhhhhh hating on AI is also extremely popular on social media.
oconnor663··on The Algebra of Loans in Rust
Yes I'm especially interested in what OP thinks about the overlap (or not?) between the ideas in this post and the ideas in this part of boats' post:

> One could imagine an alternative design in which instead of places being unpinned by default and opting into pinning, places are pinned (or perhaps “immovable”) by default, and have to opt into supporting the ability to move out of them. This would make it so that by default places have the least power (can only access via shared reference) and they gain a monotonically increasing set of powers (can assign to them, can move out of them).

> In addition to places having to opt into moving, there would be three reference types instead of two: immutable, mutable, and movable references.

oconnor663··on Go ahead, self-host Postgres
You have to consider the class of problems as a whole, from the perspective of management:

- The cheap solution would be equally good, and it's just a blame shifting game.

- The cheap solution is worse, and paying more for the name brand gets you more reliability.

There are many situations that fall into the second category, and anyone running a business probably has personal memories of making the second mistake. The problem is, if you're not up to speed on the nitty gritty technical details of a tradeoff, you can't tell the difference between the first category and the second. So you accept that sometimes you will over-spend for "no reason" as a cost of doing business. (But the reason is that information and trust don't come for free.)

oconnor663··on Eurydice: a Rust to C compiler
> We recommend using Nix to easily ensure you are running the right versions of the tools and libraries.

Ooof I remember when everything used to be like this. Cargo has really spoiled me.

oconnor663··on Thoughts on Go vs. Rust vs. Zig
> What is the dreaded UB? I think the best way to understand it is to remember that, for any running program, there are FATES WORSE THAN DEATH.

I love this. I'm gonna steal this :)

> I’m not the first person to pick on this particular Github comment, but it perfectly illustrates the conceptual density of Rust:

The overall point here is fair, but I think it's important to clarify that (iiuc) this comment is talking about a "soundness hole". Soundness holes are cases where there's a bug in the compiler or in a library that allows someone to commit UB without writing `unsafe`. Given the goals of Rust, it doesn't matter how ungodly complicated and contrived the example is. If it produces UB without `unsafe`, then it's a bug that needs to be fixed. In practice, that means a lot of issue threads about soundness involve mind-numbing code samples that mash different features together in unintuitive ways.

But that's a good thing! No one's saying you'll ever need to look at code like this in the wild. They're saying that no matter how hard you (or your coworkers or your dependencies) try, Rust should never fail to protect memory safety in safe code.

oconnor663··on Thoughts on Go vs. Rust vs. Zig
Yeah it's worth emphasizing, if I spawn two threads, and both of them print a message when they finish (and don't interact with each other in any other way), that's technically a race condition. The output of my program depends on the order on which these threads complete. The question is whether it's a race that I care about.
oconnor663··on Several core problems with Rust
> If the unwrap hadn't caused an exit, the process would've run out of memory

It was trying to push an element into a full ArrayVec. The options are:

- Blindly write off the end of the array. Obviously no one wants this, despite the decades of tradition...

- Panic and unwind, as the program actually did in this case.

- Return an error.

Some folks assume that returning an error instead of unwinding would've been better. But my assumption is that the outcome would've been the same. I think the issue came up when loading configs, which isn't usually a recoverable situation. If you have an "invalid config error", you're probably just going to return that all the way up, which is effectively the same outcome as unwinding: your process exits with an error code. There are cases where the difference matters a lot, but I don't think this was one of them.

The real gap seems to be why it took hours for folks to notice that this service was crash looping. That should normally be really prominent in alerts and dashboards. (Probably part of the story is that alerts were firing all over the place. Tough day at the office.)

oconnor663··on Choosing a hash function for 2030 and beyond: SHA-2 vs. SHA-3 vs. BLAKE3
> similar to what Bao encoding is used for in BLAKE3

In my mind Sakura and Bao are doing very different things. Sakura is a general framework for defining sound hash functions, while Bao is a BLAKE3-specific interleaving of hash function input and output that let's you do partial/streaming verification.

There was an interesting issue where it turned out that the Sakura security properties (which I studied while I was working on Bao, very helpful) weren't sufficient for what Bao wanted to do. I didn't realize that until a couple colleagues pointed it out years later. Luckily it didn't turn out to be a break in practice: https://github.com/oconnor663/bao/issues/41

oconnor663··on Choosing a hash function for 2030 and beyond: SHA-2 vs. SHA-3 vs. BLAKE3
BLAKE3 does reduce the round count relative to BLAKE2, and the underlying compression functions are similar enough that it is an apples-to-apples comparison. Our rationale for doing that was described in https://eprint.iacr.org/2019/1492.pdf, which also argued that Keccak could reduce their round count even farther than they did.
oconnor663··on Choosing a hash function for 2030 and beyond: SHA-2 vs. SHA-3 vs. BLAKE3
This is a difference between BLAKE3 and most other hash functions. In the usual arrangement ("Merkle–Damgård"), each block depends on the previous one, so the only way to verify some "slice" of the input is to re-hash the whole thing. But when you arrange the input into a tree shape (a "Merkle tree") instead, suddenly the right half of the tree does not depend on the left half until the very last step at the very top. If you give me the input to that last step, I can verify that it matches the root hash that I know, now I have the hashes ("chaining values") I'd need to verify either the left half or the right half without the other. Then I do our favorite trick in computer science, which is to recursively apply that same procedure all the way down, until I have an efficient "path" to whatever part of the tree I actually care about.

For more on this see Section 6.4 of our paper: https://docs.google.com/viewer?url=https://github.com/BLAKE3...

And the Bao repo: https://github.com/oconnor663/bao

oconnor663··on APT Rust requirement raises questions
> to rewrite some feature for a tiny security benefit

For what it's worth, the zero->one introduction of a new language into a big codebase always comes with a lot of build changes, downstream impact, debate, etc. It's good for that first feature to be some relatively trivial thing, so that it doesn't make the changes any bigger than they have to be, and so that it can be delayed or reverted as needed without causing extra trouble. Once everything lands, then you can add whatever bigger features you like without disrupting things.

No comment on the rest of the thread...

oconnor663··on PHP 8.5
> They did this by not breaking the universe in these major updates

I don't think the amount of breakage per se was the problem with Python 3. I think the problem was that for a long time (until u"" strings in 3.3? four years after 3.0?) there was ~no way for a library to support both 2 and 3 in a single codebase. That meant that each project had to either maintain a fork, or do a "flag day" migration, which in practice meant you couldn't migrate until all your dependencies were migrated. Big mistake in retrospect.

oconnor663··on What Killed Perl?
When I was doing undergrad CS in 2006, we had a choice between Perl and Python for our scripting assignments. As far as I know, no one chose Perl. It's comparatively a huge pain to get ramped up on, and the promised payoff is that you get all these implicitly stateful sigils that let you write contest-winning one-liners. But by the early 2000's, the culture was getting up to speed on obvious-in-retrospect principles like "avoid global variables" and "you should give your variables names that someone else can understand", and even as students we could all see that Perl wasn't going to be the future.
oconnor663··on Giving C a superpower: custom header file (safe_c.h)
The boring answer is that standard atomics didn't exist until C++11, so any compiler older than that didn't support them. I think most platforms (certainly the popular desktop/server platforms) had ways to accomplish the same thing, but that was up to the vendor, and it might not've been well documented or stable. Infamously, `volatile` used to be (ab)used for this a lot before we had proper standards. (I think it still has some atomic-ish properties in MSVC?)
oconnor663··on Giving C a superpower: custom header file (safe_c.h)
I think it's pretty rare to do a straight up atomic load of a refcount. (That would be the `use_count` method in C++ or the `strong_count` method in Rust.) More of the time you're doing either a fetch-add to copy the pointer or a fetch-sub to destroy your copy, both of which involve stores. Last I heard the fetch-add can use the "relaxed" atomic ordering, which should make it very cheap, but the fetch-sub needs to use the "release" ordering, which is where the cost comes in.
oconnor663··on Open-source Zig book
I imagine it's kind of like "What's stopping someone from forging your signature on almost any document?" The point is less that it's hard to fake, and more that it's a line you're crossing where everyone agrees you can't say "oops I didn't know I wasn't supposed to do that."
oconnor663··on Brimstone: ES2025 JavaScript engine written in Rust
I still wouldn't call it GC in that case. It's pretty much exactly the same as std::shared_ptr in C++, and we don't usually call that GC. I don't know about the academic definition, but I draw the line at a cycle collector. (So e.g. Python is GC'd, but Rust/C++/Swift are not.)
oconnor663··on Rust in Android: move fast and fix things
Take a look at the examples in this post: https://www.microsoft.com/en-us/msrc/blog/2019/07/we-need-a-...

Large C++ codebases have the same problems that large codebases have in any language: too many abstractions, inconsistent ways of doing things, layers of legacy. It comes with the job. The difference is that in C/C++, hard-to-read code also means hard-to-guess pointer lifetimes.

oconnor663··on Rust in Android: move fast and fix things
I like to focus on the ways that C is actually quite complicated, especially the complications that directly provoke UB when you don't know about them. Integer promotion and strict aliasing are at the top of my list.
oconnor663··on Futurelock: A subtle risk in async Rust
To be clear, if you restrict yourself to `async`/`.await` syntax, you never see any of this. To await something means to poll it to completion, which is usually what you want. "Joining" two futures lets you poll both of them concurrently until they're both done, which is kind of the point of async as a concept, and this also doesn't really require you to think about scheduling. One place where things get hairy (like in this article) is "selecting" on futures, which polls them all until one of them is done, and then stops polling the rest. (Normally I'd loosely say it "drops the rest on the floor", but the deadlock in this article actually hinges on exactly what gets "dropped" when, in the Rust sense of the `Drop` trait.) This is where scheduling as you put it, or "cancellation" as Rust folks often put it, starts to become important. And that's why the article concludes "In the end, you should always be extremely careful with tokio::select!" However, `select!` is not the only construct that raises these issues. Speaking of which...

> But it seems like an absolutely absurd exercise for code running in userspace on a "real" OS like Linux

Clearly you have a point here, which is why these blog posts are making an impact. That said, one counterpoint is, have you ever wished you could kill a thread? The reason there are so many old Raymond Chen "How many times does it have to be said: Never call TerminateThread" blog posts, is that lots of real world applications really desperately want to call TerminateThread, and it's hard to persuade them to stop! The ability to e.g. put a timeout on any async function call is basically this same superpower, without corrupting your whole process (yay), but still with the unavoidable(?) difficulty of thinking about what happens when random functions give up halfway through.

oconnor663··on Futurelock: A subtle risk in async Rust
Rust futures are "just" structs with a poll() method. The poll() method is a function like any other, so it can have local variables on the stack as usual, but anything it wants to save between calls needs to be a field of the struct instead of a stack local. The magic of async/await is that the compiler figures out which of your async function's variables need to be fields on that struct, and it generated the struct and the poll method for you.

I have a blog series that goes into the concrete details if you like: https://jacko.io/async_intro.html

oconnor663··on Futurelock: A subtle risk in async Rust
> This seems like a contradiction to me. How can future1 acquire the Mutex in the first place, if it cannot run? The word "given" is really odd to me.

`future1` did run for a bit, and it got far enough to acquire the mutex. (As the article mentioned, technically it took a position in a queue that means it will get the mutex, but that's morally the same thing here.) Then it was "paused". I put "paused" in scare quotes because it kind of makes futures sound like processes or threads, which have a "life of their own" until/unless something "interrupts" them, but an important part of this story is that Rust futures aren't really like that. When you get down to the details, they're more like a struct or a class that just sits there being data unless you call certain methods on it (repeatedly). That's what the `.await` keyword does for you, but when you use more interesting constructs like `select!`, you start to get more of the details in your face.

It's hard to be more concrete than that without getting into an overwhelming amount of detail. I wrote a set of blog posts that try to cover it without hand-waving the details away, but they're not short, and they do require some Rust background: https://jacko.io/async_intro.html

oconnor663··on Futurelock: A subtle risk in async Rust
> so no virtual dispatch

Speaking of which, I'm kind of surprised we landed on a Waker design that requires/hand-rolls virtual dispatch. Was there an alternate universe where every `poll()` function was generic on its Waker?

oconnor663··on Futurelock: A subtle risk in async Rust
Kind of a tangent, but I think "systems programming" tends to bounce back and forth between three(?) different concerns that turn out to be closely related:

1. embedded hardware, like you mentioned

2. high-performance stuff

3. "embedding" in the cross-language sense, with foreign function calls

Of course the "don't use a lot of resources" thing that makes Rust/C/C++ good for tiny hardware also tends to be helpful for performance on bigger iron. Similarly, the "don't assume much about your runtime" thing that's necessary for bare metal programming also helps a lot with interfacing with other languages. And "run on a GPU" is kind of all three of those things at once.

So yeah, which of those concerns was async Rust really designed around? All of them I guess? It's kind of like, once you put on the systems programming goggles for long enough, all of those things kind of blend together?

oconnor663··on Futurelock: A subtle risk in async Rust
> I wonder if there is a similar idea possible with tokio? E.g. if you are awaiting a Mutex held by a future that "can't run", then poll that future instead.

Something like this could make sense for Tokio tasks. (I don't know how complicated their task scheduler is; maybe it already does stuff like this?) But it's not possible for futures within a task, as in this post. This goes all the way back to the "futures are inert" design of async Rust: You don't necessarily need to communicate with the runtime at all to create a future or to poll it or to stop polling it. You only need to talk to the runtime at the task level, either to spawn new tasks, or to wake up your own task. Futures are pretty much just plain old structs, and Tokio doesn't know how many futures my async function creates internally, any more than it knows about my integers or strings or hash maps.

← PreviousPage 2 of 34Next →