HNHacker News
TopNewBestAskShowJobs

oconnor663

7,566 karma · joined September 4, 2012

[ my public key: https://keybase.io/oconnor663; my proof: https://keybase.io/oconnor663/sigs/6u7ycNDOZvuO8qNrg69nGayUCyATOnCNq6H9tBV0BsM ]
submissionscomments
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.

oconnor663··on Futurelock: A subtle risk in async Rust
There's nothing `select!` could do here to force `future1` to drop, because it doesn't receive ownership of `future1`. If we wanted to force this, we'd have to forbid `select!` from polling futures by reference, but that's a pretty fundamental capability that we often rely on to `select!` in a loop for example. The blanket `impl<F> Future for &mut F where F: Future ...` isn't a Tokio thing either; that's in the standard library.
oconnor663··on Futurelock: A subtle risk in async Rust
The author clearly understands these details. I think it's just a question of wording: did we "drop a reference (which has no effect)" or did we "not drop anything (because references don't implement Drop)"?
oconnor663··on Futurelock: A subtle risk in async Rust
> FAQ: doesn’t future1 get cancelled?

I guess cancellation is really two different things, which usually happen at the ~same time, but not in this case: 1) the future stops getting polled, and 2) the future gets dropped. In this example the drop is delayed, and because the future is holding a guard,* the delay has side effects. So the future "has been cancelled" in the sense that it will never again make forward progress, but it "hasn't been cancelled yet" in the sense that it's still holding resources. I wonder if it's practical to say "make sure those two things always happen together"?

* Technically a Tokio-internal `Acquire` future that owns a queue position to get a guard, but it sounds like the exact same bug could manifest after it got the guard too, so let's call it a guard.

oconnor663··on Uv is the best thing to happen to the Python ecosystem in a decade
PyO3 and Maturin are excellent. I've been maintaining a Python-module-written-in-Rust for several years now, and it's been quite smooth.
oconnor663··on RFC 9861: KangarooTwelve and TurboSHAKE
If you want to try an optimized AVX-512 implementation of KangarooTwelve on the command line, you can `cargo install k12sum`. On my machine it's neck-and-neck with `b3sum --no-mmap` (which does not use threads).

Edit: Oh it looks like another option, `KeccakSum`, was released a couple months ago? https://github.com/XKCP/K12/commit/5271b58c990c1ac33c1097b4e...

← PreviousPage 2 of 34Next →