Latest Posts (20 found)
Unsung Today

“Redesigned callouts to be less visually chaotic.”

Three nice moments in Buttondown’s (pretty comprehensive) release notes : There is a filter and tags to search more easily. Blog entries are linked throughout, complete with cover images – it’s a nice way to distinguish small changes from big improvements. The fixes are attributed to specific people. (This part doesn’t appear on mobile.) Particularly nice to see support for more than one person collaborating on a fix – I’ve had fun memories working at various companies of pairing up with someone else to debug a particularly tricky problem.

0 views

Pacing the Frontier, AI’s Digital Limits, AI Commissars

Dario Amodei wants to pace the frontier; it's an unrealistic proposal that seems mostly geared to political control of AI.

0 views

Slow developer experience will bottleneck fast models

Right now developer experience is measured in seconds. If your tests take a second to run, that’s good; if they take thirty seconds, that’s bad. Any faster than a second doesn’t really matter, because most of your time is spent either thinking or waiting for an AI agent to spin. Shaving milliseconds off your dev server reload time or whatever is pointless: that’s not the bottleneck. It will be. Small models are getting faster and faster, and smart models are getting smaller. I think most engineers will still want to use the smartest available model — software engineering is hard — but we will increasingly see faster models get used as subagents or for well-understood tasks. This is largely uncharted territory. Very few people have developed intuitions for what it is going to be like to work with agents that run at thousands of tokens-per-second. GPT-6-Astra can run at about sixty tokens per second. That means you spend a lot of time waiting for it to think. You work with it like you would work with another human: delegating a task and then context-switching until that task is complete. If you haven’t yet, have a play around with Jimmy , Taalas’ version of LLaMA-3.1-8B running 1 at seventeen thousand tokens per second . No matter how long the response is, it arrives in the instant of you hitting send. The model is not good enough for agentic work, but it gives a glimpse of what it would be like: you would simply get your answer instantly 2 . Well, that’s assuming the agent’s tool calls are fast. When generating tokens is not the bottleneck, it will suddenly matter a lot whether it can read a file in 100ms vs 10ms, or whether it can run your tests in 500ms vs two seconds. Fast tool calls are going to be the difference between a near-instant response and having to wait several minutes. There is thus going to be enormous pressure to do agentic coding in languages with fast compilers and tests, like Golang, and to tightly optimize the dev loop in agentic codebases. Teams focused on DevEx — developer experience — are largely a relic of the 2010s, when companies were incentivized to make their engineers happy. Most companies have cut them down to a skeleton crew or removed them entirely. But we may see a return of DevEx in the late 2020s, focused on speeding up the experience for AI agents. Like most ultra-fast inference, it relies on fitting the entire model onto a huge GPU-like chip that’s specially designed to run inference: for Taalas, it’s in the silicon itself; for Cerebras and Groq, it’s in giant embedded onboard memory units. Will AI providers simply train models to spend more time reasoning, so users would have to wait for roughly the same time? I doubt it. Most ordinary engineering problems would not be solved better by spending an extra million tokens thinking. You only need to do that when you’re pushing right up against the limits of the model. Like most ultra-fast inference, it relies on fitting the entire model onto a huge GPU-like chip that’s specially designed to run inference: for Taalas, it’s in the silicon itself; for Cerebras and Groq, it’s in giant embedded onboard memory units. ↩ Will AI providers simply train models to spend more time reasoning, so users would have to wait for roughly the same time? I doubt it. Most ordinary engineering problems would not be solved better by spending an extra million tokens thinking. You only need to do that when you’re pushing right up against the limits of the model. ↩

0 views
iDiallo Today

AI Forces You to Commit to Your Initial Belief

Have you ever watched any old hacker movie lately? The hacker can write at 180 WPM, never using the backspace or changing his mind ever. It’s like before they ever started, they already knew the last character they would type on the screen. But that’s not how it works in real life, at least not for me. The idea changes mid-sentence. That's probably how most of the code I write ends up. I start with an idea in my mind. I think it's brilliant, maybe it’s a way to restructure the code, an overall design, or a specific paradigm. But when I start writing it, something else pops up. Seeing it halfway down the page, I realize it's not quite what I was imagining, and I change direction. The same principle applies when I'm writing a blog post or a story. I feel like I have a perfect concept in my head, but the moment I start putting words down, things shift and I adjust accordingly. That's why the final text rarely matches the original vision. It’s as if writing itself is the process of refining the idea. This process is lost when you use AI. You don't write out the details of an idea as you go; instead, you just give it the gist of it. It generates an entire wall of text instantly, and now you're stuck reading through it while the tool tries to preserve that initial draft, or its own interpretation of it at least. You only discover flaws or attempt to pivot while reading the generated output. But pivoting inside a wall of text is much harder than making a 180-degree turn mid-sentence. If you spot a small part you dislike and try to remove it, it might break something 20 lines down, forcing you to step back and make sure everything still aligns. Using an LLM forces you to commit to your very first thought. When you write manually, every keystroke is a chance for the idea to evolve. When you use a large language model, you have to absorb the entire output all at once, which ends up being far more time-consuming.

0 views
Kev Quirk Yesterday

2026-09-13 10:35: Nelly the pup absolutely loving life in the field! 🤣

Nelly the pup absolutely loving life in the field! 🤣 Thanks for reading this post via RSS. RSS is ace, and so are you. ❤️ You can reply to this post by email , or leave a comment .

0 views

Why I Still Believe in Mentorship

One of the best tips I ever got was from my table tennis coach. He watched me play and said, “You are leaning forward with the wrong foot!” I’d been playing for years and thought I had a good stance. I’m left-handed, and no one had told me that I should put my right foot forward. I had copied the stance of other players without noticing the problem. It was obvious once he pointed it out. But until then, I hadn’t thought to question it, and more practice hadn’t helped me notice. That single observation improved my game more than any amount of practice because it gave me better control over my forehand play. Note that I didn’t actively seek out advice on my stance. Someone had to watch me play, observe, and then point out the single most helpful thing that would set me on the right path. Mentorship in Software In software, we are often expected to struggle alone. We’re supposed to learn systems design, architecture, testing, and refactoring largely by sitting with the problems. The times when I grew the most as a programmer were through thoughtful code reviews, but they are rare. Besides, the reviewer sees the end result but not the (sometimes flawed) reasoning that produced it. Pair programming gets closer. But two equally inexperienced programmers can reinforce each other’s bad habits. An agent can control the keyboard instead of helping you reason through a problem. It may produce better code without making you much better at writing it. A session where a machine fixes everything is productive, but being productive and growing as a programmer are two different things entirely. This is part of why I still believe in mentorship, even now that an LLM can answer so many programming questions. Going beyond the superficial questions I run a one-on-one Rust mentorship program , so I have a personal stake in this. Most of my mentees come with a concrete question: Often, answering the concrete question is beside the point. We have to look at the thought process that led to it. Before deciding whether something should be generic, for example, it helps to know what is expected to vary and why. Otherwise, we can spend a long time discussing how to build an abstraction that might be unnecessary in the first place. A mentor has enough distance from the problem to question its framing and enough experience to recognize familiar patterns. They notice which details you leave out, which patterns you reach for, and which you ignore. You may sense that something is wrong without having the words to describe it. LLMs can question assumptions, too! But I’ve found they usually work best when you already know what to ask. The difficulty is that we often don’t know which of our assumptions need challenging. Knowing what to ask can matter more than knowing the answer, and it’s hard to ask about something you haven’t noticed. You can also use an LLM to examine a decision, or you can use it as an echo chamber to justify it. The second use may feel more rewarding in the moment, but it leaves you where you started. Mentoring in Other Fields Nobody is surprised when a professional athlete has a coach. Musicians keep taking lessons after decades of playing. We don’t take this as evidence that they haven’t learned the basics. Quite the contrary: it’s an indication that they are serious about improving and that they’ve outgrown generic advice. It requires introspection to recognize that you can benefit from someone who can observe you from the outside. And in some sense, you put yourself in a vulnerable position: the wrong mentor can set you back, while the right one can dramatically accelerate your growth. Experience doesn’t remove the need for mentoring in software either. You can become an experienced developer while still carrying counterproductive habits you’ve never examined. If those habits let you get the work done, you may have little reason to suspect them and the problem gets worse because bad habits get reinforced. I could play table tennis with the wrong stance, too, just worse. The people I mentor are already experienced software engineers. They are perfectly capable of using LLMs in their work. Many are in leading positions as staff or principal engineers. And yet, they’ve realized there’s something missing that’s holding them back. They feel like they’re not progressing as quickly as they could, and their companies haven’t provided the mentorship they need. Friction Is Part of the Learning Process Another problem is that software gives us very uneven feedback. A syntax error gets your attention immediately because the code won’t compile. But a poor architectural decision can compile, pass the tests, get approved in review, and become a major problem months later when you lack the time to make changes. One way to learn architecture is to make bad decisions and sit with their consequences. That’s how I learned! The trouble is that it takes years, and it’s certainly not motivating. A mentor can streamline that process by asking questions you may not think to ask yet: There is no single “correct” answer to these questions. The answers depend on the constraints and the stage of the project. A mentor should help you examine those constraints and point out risks you’ve missed, while leaving the decision to you. If someone else chooses the design, you lose the practice of choosing it yourself. You also still have to write the code and live with all the consequences. But you go in with a better understanding of what might go wrong. And if it does, you have someone to help you work out why. That is how you develop judgment you can use on the next project. It’s a bit like climbing with a partner. Does Everyone Need a Mentor? If you’re just starting out, a book, a course, or a friendly community might be better value. If you need an answer to an isolated question, documentation or an LLM is faster and cheaper. If mentorship were only about transferring information, it would be harder to make a case for it. Personal mentorship becomes valuable when the potential payoff is a multiple of the investment. That is typically the case in a professional setting, where focused mentorship can help you land a better job or switch to a different team with a more interesting project. That doesn’t mean personal mentorship is a guarantee of success. Our busy days leave little time for deliberate practice. We have to carve out time for learning, which is hard, but the alternative is stagnation. As with going to the gym, paying for help doesn’t spare you the exercise. One hour of conversation cannot make up for a week without practice. You have to try things between sessions if you want the next conversation to be different from the last. Easier access to answers hasn’t changed any of this. What I want from mentorship is the kind of help my coach gave me: someone paying enough attention to notice what I couldn’t see for myself. That’s what I aim to offer in my Rust mentorship , at least: we work through hard problems together, explore the design space, and make deliberate choices. If I do a good job, my mentee will eventually surpass me, and I’d be proud to help them get there. Would you make this generic? Is it okay to clone here? How can I handle this error? Is this abstraction really necessary? How would an error propagate across the system? How can we test this?

0 views
Evan Hahn Yesterday

Anecdotally, programmers dislike "reduce"

In short: from my experience, people like and , but not . I use functions like and all the time. When I put that code up for review, my peers rarely complain. I get plenty of feedback about other decisions, but not about my use of and . I cannot say the same for . Often, when I’ve submitted a patch with inside, I get a comment like, “this part is hard to read.” And I see way less than , , , and so on. Anecdotally, I have come to believe that programmers don’t like as much. I don’t know why, but I have a few theories: I usually just change to something else and move on. Even though I prefer it, I don’t usually care much. But it’s a little social phenomenon I’ve observed, and I thought I’d document it. I’ve also noticed this less recently, possibly because code review is less thorough nowadays. Do you notice this? Do you like ? Please tell me . is harder to read. is less familiar. can have worse performance compared to other options. is less elegant in languages I use, like JavaScript, Python, and Swift. In my blissful stint as a Clojure developer, I did not get this feedback. I’m wrong, and I’m seeing a trend that’s not real.

0 views
Sean Goedecke Yesterday

AI is breaking our proxies for expertise

Mathematicians are broadly not anti-AI. They’re more culturally open to using AI as a tool than, say, artists or writers 1 . However, now that more and more genuinely prestigious problems have fallen to AI, that might be changing. Almost five thousand mathematicians (including twenty-five Fields medalists) have signed a declaration called A Severe Misalignment of AI in Mathematics . The core argument goes something like this: In recent months, the success of AI in solving major mathematical problems has made headlines even outside mathematical circles. But solving problems is only a tool and proxy for achieving the primary goal of conceptual understanding and insight. Forgetting this in the world of AI may turn the tool against the primary goal. Indeed, the mass production at faster and faster pace of “true/false” statements could destroy fertile ground instead of breathing life into new ideas. A lot of people online have interpreted this as the expected complaint from any field that gets automated: translators did it, artists and programmers have been doing it, and now it’s the turn of the mathematicians. I think this is too dismissive. Understanding the concrete problem mathematicians are upset about can help us better understand the impact of AI on our own fields, and what we’ll have to do about it. There are two types of mathematics. Most people are familiar with the first, which we might call “puzzle-solving”: you take a problem and try to find a solution to it. When you’re a student, these problems are typically easy, like simplifying some algebraic expression. When you’re a researcher, these problems can be nearly impossible, like proving Fermat’s Last Theorem . Puzzle-solving is easy to understand but hard to do, which makes it impressive to non-mathematicians, which makes it highly prestigious. In other words, puzzle-solving is legible . The second type of mathematics is “idea-generating”: coming up with new ways of thinking about mathematics, and thus new terms or concepts. For examples of these, just glance down the list of arXiv mathematics papers . “Hardy spaces”, “Schatten exponent”, “Banach lattices” and so on are all concepts someone thought was interesting. This work is largely unimpressive to non-mathematicians, because nobody really knows if the concepts you come up with are particularly difficult or insightful. For instance, I have just generated the concept of a “Goedecke set”, which is the set of all natural numbers whose digits add up to a prime number. Who cares? The categories we want are the “natural kinds” of mathematics — the concepts that “carve nature at its joints” — and it’s almost impossible to tell what those are without years or decades of hard work. How are the two types of mathematics related? We might say 2 that generating ideas is the real intellectual work of mathematics. Puzzle-solving is important instrumentally: to identify which ideas can be used to answer longstanding questions, and thus which ideas are worthwhile. Over time, those worthwhile ideas become better understood and easier to use, until they reach the point where they can be used to advance science in general. Eventually the ideas become so well-understood that they can be taught to children: “zero”, “negative numbers”, “imaginary numbers” and “calculus” were all once rarefied mathematical ideas, but are now concepts we’d expect any precocious twelve-year-old to grasp. There’s another, more prosaic purpose of puzzle-solving: to make mathematical skill and progress legible to outsiders. I can’t appreciate Terence Tao’s mathematical work, but I know what a Fields Medal is. I don’t have a good intuitive sense of what a Galois representation is, but I know about the proof of Fermat’s Last Theorem . We might say that puzzles like this have served as a way to indirectly reward skilled mathematicians for their more important idea-generating work (or for conclusively demonstrating 3 that the ideas used in the proof are useful). AI proofs undercut both of these purposes. I can now lay out precisely why I think mathematicians are so unhappy: This is kind of like Goodhart’s Law . Puzzles were a useful, impossible-to-game measure for mathematical progress. But now that AI companies can game that measure (by solving them in a way that’s inaccessible 4 to humans), the whole point of those puzzles disappears. Are the mathematicians right? I think it’s broadly unclear whether (3) is true: i.e. whether frontier AI models aren’t generating or can’t generate new mathematical ideas. We’re still in the very early days of AIs solving our hardest mathematical problems. Who knows what they’re going to be capable of? I give basically zero credence to the idea that AIs are incapable of this because of some intrinsic feature of how LLMs work. For the last three years, we’ve seen people claim that LLMs are intrinsically incapable of X, only to have LLMs excel at X a few months later. Even granted that (3) is true, there’s still work to be done for human mathematicians in building the conceptual machinery that can make AI-generated proofs accessible to humans: i.e. in generating a “human proof” to go alongside the existing “AI proof”. In fact, I’d expect the existence of an AI proof to help with this. If you know proposition X is true, it’s easier to figure out why, because you’re not constantly worried you’re wasting your time. For more on this, I recommend Gwern’s blog On Really Trying , where he quotes a series of instances where simply being told that a solution exists is enough of a clue to help people find it. Of course, there’s a prestige and motivation problem. “I’m the first person to solve Navier-Stokes” is a much more compelling target than “I figured out a better way to explain the AI solution to Navier-Stokes”, and it’s much easier to award prizes for. Will mathematicians bother to work on problems that have already been solved? I think so. To see why, we can look at other domains where AI has come in and outcompeted the best humans, such as chess or video game speedrunning. I can run a chess program on my phone that will beat Magnus Carlsen 100-0. Computer programs — called “tool-assisted speedruns” or “TAS” — can finish any video game much faster than even the fastest human. But in both of these areas, humans still compete in human-only leagues, and there’s still prestige attached to the most capable humans. It’s possible that mathematics ends up in this kind of state, where “human mathematics” and “AI mathematics” exist in largely separate spheres, and the first “human” solution to a mathematical problem can still earn acclaim. In fact, in both of those areas, the presence of inhumanly strong computer players has improved the human game. Despite many computer chess moves being basically incomprehensible to humans, top chess players have learned from the computer “style”. In speedrunning, many moves once considered “TAS-only” are now performed by humans. AI mathematics might likewise improve human mathematics. I am not a mathematician. I did major in mathematics during undergrad, and I have fond memories of proofs from real and complex analysis, but it’s not even close to my field. However, I am watching the effects of powerful AI on mathematics very closely, since my own field — software engineering — is being colonized by AI agents in the same way. The field of software engineering does not have the same structure as mathematics. We write code to make money, not to earn prestige or advance the frontier of human knowledge. But AI is undercutting the traditional avenues for prestige in software engineering as well. It used to be that you could put a meaty project on your GitHub — say, an emulator, or a toy OS — and people would know you were a skilled engineer. But now projects like that are worthless, because everyone just assumes they’re vibe-coded. We used to tell stories about engineers who would disappear and rewrite a system over the weekend, or produce thousands of lines of code a day. Now anyone can do that with an OpenAI subscription. Like mathematics, software engineers are going to have to rebuild our cultural sense of the kind of work we value. We are either going to have to silo “AI work” off from “human work” like chess, or to find some legible human skills to recognize that can’t be easily counterfeited by AI. In the meantime, a lot of people who were successful in the old world are going to be very unhappy. Possibly because current AI models are much better at mathematics than at art or writing. Again, I am not a mathematician: here I am interpreting what I’ve read from Terence Tao and other mathematicians. This kind of idea-sharpening or idea-validating is part and parcel of idea-generation, and just as important. It’s interesting to compare mathematics to, say, philosophy, which has the same idea-generating task without the corresponding puzzles to validate the ideas. That’s one reason why philosophy has less prestige than mathematics. A thousand-page Lean proof is theoretically understandable by humans, but if no mathematician can hold the entire idea in their head it doesn’t matter. Puzzles serve as a high-legibility, high-reward target for mathematicians To solve these puzzles, new ideas must typically be generated; the puzzle’s solution serves as evidence that the ideas are useful But now AI can solve many of these targets “the hard way”, without generating intuitive new ideas This undercuts both ways puzzle-solving supports idea-generation: AI companies claim the prestige while not meaningfully advancing mathematical progress This is bad for mathematics as a whole, because puzzle-solving is ancillary to the real goal of mathematics Possibly because current AI models are much better at mathematics than at art or writing. ↩ Again, I am not a mathematician: here I am interpreting what I’ve read from Terence Tao and other mathematicians. ↩ This kind of idea-sharpening or idea-validating is part and parcel of idea-generation, and just as important. It’s interesting to compare mathematics to, say, philosophy, which has the same idea-generating task without the corresponding puzzles to validate the ideas. That’s one reason why philosophy has less prestige than mathematics. ↩ A thousand-page Lean proof is theoretically understandable by humans, but if no mathematician can hold the entire idea in their head it doesn’t matter. ↩

0 views

I Missed One TLB Shootdown and Somehow Ended Up Controlling a Page Table

I used a public Linux kernel bug to make one CPU keep using a memory mapping after another CPU had removed it. I then persuaded Linux to recycle the physical page behind that stale mapping as a page table. My unprivileged process could still write to the old page, while Linux now treated the same bytes as part of its memory map. That chain produced one real flag from Google’s kernelCTF service. This was a 1/n day exploit, not a new vulnerability: the bug, reproducer and fix were already public. My work was the long and occasionally silly trip from “stale read” to a complete exploit. (It took me a too mcuh time to write this blog in a way it is useful, soemthign what I would have prefered from other blogs, Feel free to let me know if it can be improved and written more reader/begginer friendly) Authorized lab only: I tested Google’s kernelCTF target and an isolated local QEMU virtual machine with networking disabled. Do not run kernel exploits on everyday computers, shared machines, production systems or anything you do not own and have permission to test. I had some free time (again), so I started writing a kernel exploit as a hobby project. This is apparently what passes for recreation once normal hobbies stop looking complicated enough. The previous kernel exploit I had developed was during a training class, It was useful and I learned a lot, but there is a small problem with learning kernel exploitation: if you do not practice afterward, the knowledge does not remain neatly arranged in your head and mine had become a box of unlabeled dataset. I remembered the broad ideas that there are pages, page tables, allocators, races and many ways to crash a virtual machine. The precise details had quietly left the building. This work is also not my day job I do not do offensive security or exploit development professionally and learning this area takes a lot of focused free time. Reading one function often means reading five structures, three commits and a mailing list thread before realizing the first function was not important. My first instinct was to re read all the theory until I felt ready (That is a comfortable trap) There is always one more memory management sections to understand and “ready” can remain three chapters away forever. So I decided to choose a real finish line instead. I wanted an exact kernel, a public bug, an objective success condition and nowhere to hide behind “the idea should work.” If the exploit did not cross the boundary and produce the expected result, it did not work. Google kernelCTF is a continuous Linux kernel exploitation challenge. Google boots an exact kernel build inside a virtual machine and runs the submitted exploit as user ID or UID, . UID 1000 is an ordinary user, not root. The protected flag is available to root in the initial guest environment, outside the challenge process’s NsJail sandbox. The job is to begin inside that restricted process and gain enough control of the guest Linux kernel to reach the flag. That boundary: QEMU runs the guest, but I did not escape QEMU. Google’s host was not a target. The outer jail was not a target The solid line in this diagram is the entire claim. My local lab reproduced the important guest details so I could reboot, add logs, attach a debugger and compare two kernels without wasting the official service’s time. A local root shell or synthetic flag is evidence that a chain is developing. Only Google’s endpoint can return an official kernelCTF flag. The flag could not be impressed by a crash, a screenshot or a paragraph explaining why the final stage probably worked. There are also two common labels worth clearing up. A zero-day is a vulnerability not publicly known to defenders when exploitation starts. A one-day uses a bug or patch that is already public. This project was firmly a 1/n day I was learning how to exploit a known defect. Each kernelCTF submission receives an ID. Mine became An earlier submission, , already occupied the version/base slot for , so mine was not the first submission for that base That changes reward and review possibilities, but it does not change whether returned a flag. Google can separately review a submission for a novel exploitation technique and for reliability. Public evidence shows one official capture for . It does not reveal which numbered run succeeded or a success percentage, so I will not manufacture either number. “One flag” is both less exciting and more accurate. I wanted a target with a public patch and a public reproducer. That gave me somewhere concrete to begin while still leaving the exploit engineering unsolved. The target I chose was tracked as CVE-2025-37964. The vulnerable build was , at exact commit . The fixed comparison was , containing stable commit . The public report and reproducer showed stale memory access caused by an ordering problem in x86 Translation Lookaside Buffer handling. The actual vulnerable component is the target’s . That sentence is important because the eventual exploit uses enough other kernel machinery to make blame wander around. The naming became stranger than the code. Stable 6.12 had an attempted backport related to upstream commit , but operations relevant to the protocol landed in the wrong order on that branch. The stable commit corrected that order. The current public CVE records do not map the stable-only correction cleanly. CVE-2025-40174 appears in later paperwork and discussion, but I am not relabeling the exploit as definitely targeting that CVE. The code relationship between and is clear. The administrative relationship between the CVE records remains unresolved. I started by reading the patch and immediately met , , , and PCID. This was the point where my box of training-class cables tipped onto the floor. The kernel documentation helped me rebuild the picture. The kernel is the trusted part of the operating system that schedules work, manages memory, talks to hardware and enforces access between programs. My exploit begins in user space , where normal applications run with limited rights. When a program needs the kernel to do something privileged, it uses a system call , usually shortened to syscall. Calling to request memory, to release it or opening a file all cross that user/kernel boundary through syscalls. I kept one imaginary program in mind while reading I Call it Maple, Maple asks Linux for one writable page and stores there. Maple receives a virtual address, perhaps . That address belongs to Maple’s process address space , the private-looking range of addresses it can use. Inside the target kernel, the whole address space is represented by , while regions within it are represented by . Threads that share an address space share the same . When Maple requested one mapped region, Linux recorded a Virtual Memory Area or VMA. A VMA says that one continuous address range exists and has particular properties: writable or read-only, file-backed or anonymous, and so on. The official process-address documentation describes the VMA as the metadata for the range. It is not the physical memory itself. That distinction initially felt fussy, then became central to the exploit. Linux can retain a VMA while removing the page-table entry for one page inside it. The region still exists as an idea, but the route to the physical bytes is temporarily gone. Linux divides virtual and physical memory into pages. On this target, the pages I cared about were 4 KiB. Maple’s virtual page is connected to a page in physical Random Access Memory , or RAM, through a hierarchy of page tables. The Linux page-table documentation explains the real multilevel walk; my working picture was simpler: The final Page Table Entry or PTE, stores the physical page number and permission bits It is one row in the official map. The CPU follows those rows when Maple reads or writes V. Following several page table levels on every memory access would be painfully slow, so each CPU core keeps recent answers in a fast local cache called the Translation Lookaside Buffer or TLB. The page table says where V officially goes. The TLB lets a core remember “V goes to P” without walking the map again. Modern x86 processors can cache translations from multiple address spaces. A Process-Context Identifier or PCID, tags entries so the CPU can tell which address space an answer belongs to. The exact target’s and track which memory map and TLB generation a CPU believes it has loaded. Now Maple runs on CPU0 and CPU1. CPU0 has cached . CPU1 removes Maple’s PTE because Maple asked Linux to discard that page. The page table no longer contains the route, but CPU0’s hardware cache does not read minds. Linux must make CPU0 forget. That coordinated invalidation is a TLB shootdown . Usually Linux identifies processors using the address space and sends the needed notification, often through an inter processor interrupt. When the protocol works, CPU0’s next access cannot use its old shortcut. It walks the current table and sees that the page is absent. The bug was a schedule where both ways of noticing the change were missed. I found it easiest to picture two people updating a sign while passing through a doorway. CPU0 was switching from a helper task into my reader task. CPU1 was removing the reader’s PTE. Linux maintained both a generation number saying how new the address space was and a CPU mask saying which cores were using it. Each core looked at a piece of state that was reasonable at that instant. Together, those observations created a hole. CPU1 missed CPU0 when selecting remote recipients because CPU0 had not yet published the new map. CPU0 missed the newer generation locally because it had already sampled the old value. The remembered translation survived between those two decisions. The race sits around and the /TLB-generation protocol in the exact . documents the generation invariant. The stable fix restores the ordering; its compiler is not a magic cross-CPU hardware fence. I could observe stale access on . The same phased test ran two million attempts on without reporting stale access. That is a useful vulnerable-versus-fixed comparison, not a proof about every schedule on every machine. The reproducer had answered the first question. CPU0 could still read P. But P still belonged to my own mapping, and an unusual route to my own bytes is not root. Then came the useful thought: the TLB remembers a physical destination, not the reason those bytes were allocated. If Linux releases P and later reuses P as a page table, CPU0’s stale route still ends at P. My process thinks it is writing an old data page. Linux thinks those same bytes describe mappings. That was the whole exploit in one diagram. Unfortunately, there were many ways to fall between the boxes. I began in the traditional way: two threads, two CPUs and optimism. The writer on CPU1 removed the mapping while the reader ran on CPU0. Millions of attempts later, optimism had produced a very stable failure. The public reproducer showed that the bug existed, but my exploit needed one particular context switch. CPU0 had to move from a helper’s address space into the reader’s address space during CPU1’s removal. Random wakeups did not reliably create that edge. I changed the scheduler shape instead of merely increasing the attempt count. The helper and reader were pinned to CPU0; the writer was pinned to CPU1. The reader slept and the helper woke it near the measured point where CPU1 would remove the PTE. The sleep/wake mechanism was a fast userspace mutex or futex . A futex lets a thread wait efficiently in the kernel until another thread wakes it I was not controlling the scheduler, I was arranging the room so the helper to reader switch became the obvious next move. Direct wakeups and an unphased helper each completed recorded campaigns of two million attempts without a stale result. Scheduler shaping produced the stale oracle The answer was not more lottery tickets, It was changing which lottery I was entering. Most attempts still had to fail and that created the next practical problem. Every try needed to remove a PTE, test for stale access and restore the page for another try. After a win, however, I needed to stop restoring it and eventually release the physical page. I chose one page from , a Linux interface that creates memory with special secret memory handling. The useful property here was not secrecy It’s lifecycle control. During retries, with could remove the page’s PTE while the underlying memory object stayed alive. A losing read could fault the page back in and prepare another attempt After a stale win, I could take a one time path toward releasing it. Linux calls the bookkeeping object for one or more physically contiguous pages a folio . My carrier was one page, so “secret folio” mostly meant “the kernel object still owning these 4 KiB.” The local in this security_flow checkout is an older safe milestone with an eight-page oracle pool. It does not reclaim the page or write a PTE. The immutable complete source contains the one-page chain. My first success check was embarrassingly trusting: put known bytes in the page, remove the mapping and celebrate if CPU0 reads the known bytes. There was another explanation, CPU0 might take a normal page fault. Linux could rebuild the PTE and return exactly the bytes I expected The output would look successful while the stale translation had never survived. So every attempt received a changing marker Then I checked , the kernel interface that reports properties of a process’s virtual pages. An oracle is just an observable test for hidden state mine combined the marker with the present bit. The sequence I accepted was strict: pagemap said present before removal, absent after removal, CPU0 returned the current attempt’s marker, then pagemap still said absent twice. The marker proved the bytes were current. The absent checks proved a normal fault had not quietly recreated the PTE. At this point I had the stale route I had wanted, I assumed releasing and reclaiming the page would be the easier part. This was incorrect in a way the allocator was happy to demonstrate for quite some time. I won the race, unmapped the page and allocated replacements. Nothing reused it I allocated more, Still nothing my first response to an allocator mystery was, naturally, more spray. It was the exploit-development version of pressing an elevator button harder. Eventually I stopped trying to persuade the allocator and traced the page’s lifetime. Tracefs showed an invisible temporary reference in CPU0’s , a deferred batch used for locked-memory accounting. Linux still had one hand on the folio, so it could not recycle the physical page regardless of how much memory I sprayed around it. The hidden reference came from refaulting the locked secret memory VMA during retries. This was where the earlier VMA distinction stopped being vocabulary and became the answer: the mapping’s region remained valid and faulting inside it could put the folio into deferred accounting. On this exact target, a page-aligned beyond the end of reached and emptied CPU0’s deferred batch. I performed that drain before the race. A successful stale read then avoided another refault, so it did not add the hidden reference back. The allocator had not been random or stubborn. My ownership model had been wrong. Draining the reference made release possible, but release usually comes with a dangerous side effect. removes a VMA and normally participates in invalidating translations. A fresh flush would erase the stale TLB entry I had spent all this effort preserving. I needed the final unmap to complete the ownership paperwork without finding anything left to clear. The race had already removed the leaf PTE. I kept neighboring mappings alive in the same Page Middle Directory or PMD, region, then unmapped only the one page secret VMA. On this target, one PTE page contains 512 entries and covers a 2 MiB PMD region. The neighbors kept the surrounding page-table structure alive. With no leaf entry left and no enclosing table becoming empty, the final had no useful translation left to invalidate. CPU0 could then watch the physical page being cleared through its stale route. That runtime observation became the gate before reclaim. This was fragile. A context switch, unrelated mapping change, pending flush or PCID rollover could still destroy the stale entry. The exploit checked that the route survived rather than assuming the diagram had authority over the machine. Now I could release P while CPU0 retained . The next question was how to make Linux reuse exactly P as a PTE page, then identify which PTE page it had become. I reserved a sparse 1 GiB virtual arena. “Sparse” means the address range was large without immediately consuming 1 GiB of physical RAM. I created its upper page-table levels first. Later, touching one address in each new 2 MiB region mainly forced Linux to allocate PTE pages. This is allocator grooming : arranging allocations and frees to influence what receives a reusable chunk. A Page Frame Number , or PFN, identifies a physical RAM page. I wanted the old secret-page PFN to return as the PFN of one arena PTE page. PTE-shaped bytes were not enough. Random values become persuasive after a long debugging session. I needed each candidate table to identify its owner. I encoded the arena region number with pairs of PTE slots. In each pair, one populated slot represented a zero bit and the other represented a one bit. Start and commit anchors showed that construction had completed. Slots that should be empty also had to remain empty. CPU0 took two full 4 KiB snapshots through the stale alias and required them to match. The decoder checked the anchors, pair code, common zero-page frame, permissions, physical range, and required empty slots. Only then did I accept both “this is a PTE page” and “this is the PTE page for arena region N.” One PTE page maps 2 MiB, so the 1 GiB arena offered 512 candidates. On this target the exploit retained 256 conditioner tables and resumed candidate allocation at region 257. Those numbers are measured behavior for this build, not laws of the Linux allocator. Recognizing a page table was exciting, but the bytes could still have been a convincing coincidence. I wanted a harmless proof before using a system wide target. The exploit therefore has a separate mode. It allocates a canary page containing known data and reads that page’s legitimate PTE through the reclaimed stale window. Then it copies the entry into a destination PTE slot. The destination address had never been accessed, or walked , by a CPU. That detail mattered. If a CPU had already cached a translation for the destination, changing its PTE would require another invalidation and that invalidation could also destroy my stale window. An untouched slot had no old TLB answer to forget. The canary became readable through both virtual addresses. A write through one appeared through the other and the raw entry matched. The exploit then cleared the forged entry and checked cleanup. That was the page-table-control proof. It ran in mode. The official capture came from a separate mode. They were not two stages observed in one lucky official run, and I do not want a smooth story to blur that boundary. For the authorized challenge finish, I used a known technique involving . Linux consults this setting when a process crashes and generates a core dump. If the pattern begins with a pipe, the kernel launches the named helper with root privileges. This finish was prior art, not my new idea. The interesting work in my chain was reaching controlled PTEs reliably enough to use it. PTEs in my fingerprint pointed to Linux’s shared read-only zero page, which revealed the physical location of that known page. Symbols from the exact gave the fixed relationship between and the physical page containing . I installed a writable mapping for that target page in the never-walked destination slot. CPU1 wrote and read back a NUL-terminated helper pattern. A copy of the exploit was already held in memory-backed file descriptor 666. After a controlled crash, the root helper used the supplied process information to recover the required descriptors and read only the challenge flag. Before triggering it, the exploit cleared the forged PTE. The helper later made a best-effort reset of to . It did not save and restore the previous value, and a reset failure was silent. “Best effort” is the honest description. The exploit validates physical-address width, page alignment, guest RAM bounds, PTE flags, kernel physical alignment, and two equivalent symbol calculations before writing. On this target identifies the crashing process in the initial process namespace and supplies a process file descriptor. The helper uses to recover descriptors. Locally, the chain now had a harmless proof mode and a separate flag mode. I packaged the official archive and expected the race to be the unreliable part. The archive refused to race at all. My first official archive checked for an invariant Time Stamp Counter or TSC. The TSC is a fast processor clock and I used it to coordinate the two CPUs. The code required the processor feature bit promising that the counter behaved consistently. Google’s CPU profile hid that feature bit. The exploit exited before making one attempt. The check had confused a label with the property I needed. The two virtual CPUs’ counters could still be usable together even when that advertised bit was absent. I replaced the feature-bit assumption with measurement. The corrected code exchanged timestamps between CPUs 32 times, used the minimum round trip to estimate offset, bounded the result, and required at least 75 percent of samples to agree within a latency-based tolerance. A preserved local run measured an offset of cycles while reporting . The corrected archive went on to produce the one official flag capture. That proves one official capture. It does not reveal a run ordinal or establish a success rate. The stale-only local milestone still requires invariant, constant, nonstop TSC; the immutable submitted exploit contains the measured cross-CPU version. After all those detours, this is the end to end path without introducing anything new. Normal exploit logs told me what my program believed: markers, pagemap state, decoded arena region and which checks passed. They were cheap enough for repeated race attempts, but they could not explain an invisible kernel reference. The GNU Debugger or GDB, let me stop a virtual CPU inside functions such as and inspect registers and control flow. It was excellent for understanding a path and terrible for preserving race timing. A breakpoint can make the execution you wanted to observe disappear simply by observing it. Tracefs is Linux’s event tracing interface. It records selected kernel events while the system continues running. Allocation, free and drain traces answered “who still owns this page?” more directly than another breakpoint could. The two kernel files in the lab also have annoyingly similar jobs and very different forms. is the compressed bootable kernel image QEMU loads. is the uncompressed kernel executable with symbols and, when available, debug information. GDB needs the exact matching the booted ; a nearby build can produce beautifully named nonsense. Tracing and debugging both distort the experiment. GDB pauses CPUs. Tracefs changes timing and memory activity. I used them to understand mechanisms, then returned to fresh untraced boots for race evidence. The local checkout contains only the stale milestone. The complete , , and implementation and build integration are in the public PR branch. A runner means the virtual machine booted and output was captured. The security result is on or on . Networking remains disabled. Start a debug copy of the matching QEMU command with . The first option opens a local debugger port; the second pauses the guest before execution. These breakpoints are for learning the path. Remove them and use fresh boots for timing observations. Check that optional event paths exist before enabling them. This privileged diagnostic run is not the UID-1000 exploit proof, and tracing can change both timing and allocation behavior. Do not fabricate output. Do not publish the signed flag or the sensitive root screenshot. The public reproducer proved stale access but not the page lifecycle I needed. Random wakeups lost millions of attempts, which forced me to shape the helper to reader transition instead of running longer. My first marker could not separate a stale hit from a normal fault; pagemap made that claim testable. More spray could not beat ; tracefs found the hidden reference. Finally, the first official archive trusted a missing TSC feature bit and never raced; measuring the clocks fixed it. None was cinematic Together they were most of the work. The vulnerability was public, Stale-TLB exploitation after physical-page reuse was public. Generic freed-page-to-PTE conversion, zero-page anchoring, and the helper finish all had prior art. Project Zero’s stale-TLB work and page-use-after-free to PTE work are useful background. My narrower contribution was fitting a retryable one-page secret memory carrier, changing marker plus pagemap oracle, deferred-reference drain, empty final unmap, conditioned PTE allocation, pair-coded owner fingerprint, separate harmless canary proof and never-walked destination into one end-to-end chain for this exact target. I searched Google’s public kernelCTF corpus at commit from 2026-08-06. I found no match there for , , , this CVE, or the pair-coded protocol. That supports a novelty case; it cannot search private work or every unindexed experiment, and Google decides the category. The complete submission is public in Google security-research PR #418 . The PR is open and unmerged. Its immutable head is . Pinned links are available for the full exploit source , Makefile and kernelXDK integration , and technical documentation . The repository review source is 3,226 lines. The inside the submitted archive is 3,179 lines because the review copy includes the kernelXDK symbol-wrapper route. Both counts are correct for different copies. The successful archive was named in my private working tree and in the public submission layout. Its SHA-256 is: It contains exactly , , and . The earlier archive that stopped at the TSC check has different bytes and a different hash. PR #418’s checks and discussion expose the CVE mapping dispute. The technical stable correction is ; the public CNA and spreadsheet mapping remains unresolved, with CVE-2025-40174 complicating the record. Public code does not mean the PR has been reviewed, merged or that the metadata has been adjudicated. has public evidence for one official flag. already occupied the version/base slot and that fact alone does not reveal which vulnerability used. The remaining review question was the discretionary novel-technique category. A working exploit, a first slot and a reward are three different facts. The finished chain can be compressed into one sentence: one CPU kept an obsolete route, Linux recycled the destination as a page table and my ordinary process edited the map. Building it felt nothing like one sentence. I had to relearn memory-management basics, make the scheduler produce one edge, distinguish a stale hit from a fault, find a reference I could not see, make an unmap boring, label hundreds of page tables, preserve one untouched destination and finally replace a confident clock check with an actual clock measurement. The flag was a clean finish line, but the useful part was being forced to answer each question with evidence. Rereading theory would have made me feel less rusty. Watching a supposedly free page refuse to move taught me why it was not free. The honest ending is still the scoreboard: captured one official flag; had the base slot; PR #418 is public and unmerged; the CVE metadata is unresolved. No new bug, no invented reliability number, no mysterious years of kernel wisdom. Just some free time, a lot of rebooting, and one CPU that forgot to forget. If you are still reading this, you are awesome. Thanks for sticking with me! The exact two-CPU guest starts my exploit as UID 1000 inside the challenge jail. The exploit reserves a sparse 1 GiB arena and prepares page-table candidates that can later identify themselves with pair coded region numbers. It creates one page, writes a changing marker, and drains CPU0’s deferred locked memory batch so no hidden reference keeps the folio alive. A helper and reader share CPU0 while the writer runs on CPU1. Futex wakeups shape the helper-to-reader switch around the writer’s operation. In the winning interleaving, CPU1 removes the PTE but does not include CPU0 in the shootdown. CPU0 has already read the old generation and skips its local flush. CPU0 reads the current marker through its stale TLB entry. Pagemap says the PTE remains absent before and after the read, ruling out a normal fault. The exploit unmaps the already-empty one-page VMA while keeping neighboring mappings alive. The physical page is released without an effective invalidation, and CPU0 verifies that its stale route survived. Touching conditioned arena regions makes Linux allocate PTE pages. The old secret page is recycled as one of them. Two identical snapshots plus anchors, zero-page entries, permissions, empty slots, and the pair code identify the owning arena region. In a separate run, the exploit copies an owned canary PTE into a never-walked destination and proves both aliases reach the same page. In mode, the exploit instead maps the physical page containing , writes and verifies the helper pattern, clears the forged entry, and triggers the helper. The helper reads the protected flag and attempts a best-effort reset. Google’s service records one successful flag for . Exact release and UID 1000 in QEMU. GDB stopped in with matching symbols. Pagemap showing present, absent, current marker, still absent. Tracefs showing the relevant physical page’s release and allocation. Separate harmless output. Local stale marker and fixed comparison. Redacted ledger entry containing no secret data. Write a tiny C program that opens and reads a file. Run and watch the syscalls cross from user space into the kernel. Allocate one page with , write a value, and inspect . Connect the address range to a VMA before worrying about PTE bits. Draw the path from virtual page to PTE to physical RAM to TLB. The official page-table and process-address documents are good companions once the picture is clear. Write two harmless threads, pin them to CPUs, and use a futex to change their wakeup order. Observe timing; do not begin with a kernel bug. Boot a disposable QEMU guest with networking disabled. Learn which file is , which is , and where the guest/QEMU/host boundaries sit. Attach GDB to the guest and stop at a harmless syscall. Then enable one tracefs event and connect one trace line to one action in your program. Read a public patch and reproducer. Try to state the broken invariant in one sentence before building anything. Build evidence in small rungs: controlled observation, controlled read, controlled write, harmless canary, then the authorized target outcome. Count failed attempts and preserve raw logs. A success proves exploitability; it does not retroactively make every failed run disappear. Keep vulnerable-kernel work inside an isolated system you own or are explicitly authorized to test. Linux kernel documentation kernelCTF rules Linux CNA record for CVE-2025-37964 Public CVE discussion and reproducer Original upstream ordering fix Correct 6.12.y stable fix Exact vulnerable x86 TLB source Exact target memory-management structures Exact target x86 TLB state structures Linux page-table documentation Linux process-address and VMA documentation Linux pagemap documentation documentation Project Zero stale-TLB exploitation precedent Project Zero page-UAF-to-PTE precedent Google’s public kernelCTF exploit corpus Public submission PR #418 Immutable full exploit source Immutable exploit documentation

0 views

Generating running routes with GPT-6 Astra and ChatGPT Work

Here's a neat thing I had ChatGPT Work with GPT-6 Astra (Max) do this morning: It worked for 27 minutes and produced exactly what I'd asked for, as both an embedded visualization and downloadable GPX file and GeoJSON files. Here's that 5K route: When I asked it how it had created the route, it replied: I used Nominatim to locate the address and Overpass to download local OpenStreetMap roads and trails , then calculated the loops locally. Frustratingly, the actual code it ran and exact details of what it did weren't visible to me in the ChatGPT UI. I see this lack of transparency is an anti-feature. By the time I thought to ask for a copy of the Python code it had used, ChatGPT was unable to provide it. This appears to be because the thread had been compacted. I think any LLM system that uses compaction needs to both preserve the pre-compacted text and make that text available via agent tool calls, to protect against this kind of problem. As for displaying the map to me, that used the visualize skill . It created a file called to embed directly into the ChatGPT UI. Here's a copy of that HTML , which starts like this: The element contains the full geometry needed to render both the running route and the map itself, using D3, which is loaded from an allow-listed CDN location described in this section of the visualize skill : You are only seeing the long-form articles from my blog. Subscribe to /atom/everything/ to get all of my posts, or take a look at my other subscription options . The CSP allows only , , , , , , and . Other origins are blocked and fail silently.

0 views
Unsung Yesterday

Nova’s menu wayfinding

From its earliest days , Macs established an interesting convention – whenever you press a keyboard shortcut to an action that’s somewhere in the app menu, the matching top menu label blinks quickly. Here, I am pressing ⌘A (Edit > Select All), followed by ⌘+ (Format > Font > Bigger), and then ⌘B (Format > Font > Bold): I believe this is meant to help you connect those things better. While you might not need a map to an action that you already know a shortcut for, it might be helpful to tell you where to find other actions like it. I imagine it also helps whenever you press a wrong shortcut – or the right shortcut under the wrong circumstances – and you want to deduce what happened or what was meant to happen. Knowing roughly where a command “lives” makes it easier to open the menu and look for it, even if you might still have to dig through all the submenus. Recently, I spotted the programming editor Nova use a parallel technique. In its command palette, commands show their keyboard shortcuts, but if they’re hiding in submenus – also their menu path: = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/novas-menu-wayfinding/2.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/novas-menu-wayfinding/2.1600w.avif" type="image/avif"> = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/novas-menu-wayfinding/3.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/novas-menu-wayfinding/3.1600w.avif" type="image/avif"> I am not sure how effective either of these techniques is, and both can feel a bit… busy. But they also appear thoughtful – we’re all spatial creatures, and I imagine helping you visualize a map of the entire system of commands can make it easier for you to feel at home.

0 views
Gabe Mays 2 days ago

Making math automatic with Mathy

I finally built Mathy, a little project I’ve been thinking about for a couple years. It’s free, and no account is required if you want to try it out. I use Math Academy daily, which is the best way to learn math. But I wanted a mobile-friendly way to drill math and weak areas on the go that was more convenient/less cumbersome than Anki cards. It’s also a nice way to do something…

0 views
Brain Baking 2 days ago

What Do We Feel When We See Another Video Game Remake?

Yes, this is a blog post about the The Legend of Zelda: Ocarina of Time remake. In part inspired by Christian Donlan’s I’m trying to untangle my response to how the Ocarina of Time remake looks on EuroGamer and in part because of my huge initial distrust when I saw the first real gameplay trailers. What exactly happened in my head was something along the lines of: Since that initial shock, I’ve mostly come around. Well not really, but good enough to tolerate it’s a remake, not a remaster, and it’ll please the youngsters who don’t even know what proper Zelda gameplay is, since Breath and its direct successor don’t even feature proper dungeons. Young Link running around in the Hyrule fields. Where you can actually see stuff in the distance. Crazy. Let’s take a step back. I’ve been mulling over the purpose of a remake/remaster (let’s keep the distinction out of this discussion for now) and came to the conclusion that the reception of games like these can be categorised in the following taxonomy. On the one hand, you’ve got people who never played the game before. They are either young enough to never have witnessed the original (and have not heard enough about it to care), or people who missed the original release and are now reminded of its existence. This is exactly what a remake is for: to push the original game forward into the future, ready to win the hearts of another generation. In case of OOT , I doubt any self-proclaimed gamer will deny its presence in nearly all top of all time lists, but in the case of the average game from the nineties that since has fallen into obscurity, a remake is a sure way to try and resurrect it from the dead. On the other hand, there are folks who already played OOT . These are the critical annoying folks who’ll tell you Breath of the Wild is not a true Zelda game (look who’s talking). We can further categorise these into grumblers who want to preserve the exact experience from back then and the folks who are open to reliving its core message whilst marvelling at a completely revamped engine or even fleshed out storyline. Given my affinity with nostalgia —good or bad—I tended to side with the grumblers. That’s indeed shrewd usage of past tense. My first raw reaction when seeing Young Link in motion, built out of 3446833 polygons instead of just 10 was definitely a grumbly one. But like I said, I’ve come around. Why do I buy and play so many remakes/remasters? A non-exhaustive recent selection: Why do I buy and play so many retro-inspired new games? A non-exhaustive recent selection: Why do I buy and play so many retro collections? A non-exhaustive recent selection: Clearly, I am the ideal target audience for a game like the OOT remake. Yet my first reaction was scoffing—why? Because that’s not how Link is supposed to look like and how the field of vision is supposed to work on authentic Nintendo 64 hardware? There are a plethora of HD ports out on the PC such as Ship of Harkinian , offering modern controls, widescreen, and other quality of life stuff we’re all addicted to nowadays. Including me. Including me: the first thing I did when replaying Might & Magic VIII , a 26 year old PC game from 2000, was apply a patch that improves the field of vision, adds widescreen, and fixes some of the controls. The first thing I did after installing Ultima Underworld was patching in “modern” mouse controls. It turns out that I’m less of an authentic player than I thought I was—yet I refuse to play Serious Sam HD and stick with my Windows 98 PC for that one. Why? Because Serous Sam is supposed to be low-res that used to be enormously high-res and cutting-edge. The HD version looks too different compared to the original that in my personal context has a tight affinity with the ATI Radeons and GeForces of the early noughties. I didn’t want to alter how Ultima Underworld and Might & Magic VIII looks or sounds —merely how easily I can manipulate its world. Yet if you give me the choice to play Paper Mario 2 on the GameCube or on the Switch, I’d choose the latter in the blink of an eye. This is coming from someone who spent forty hours on both games, in 2004 and in 2024. Stuart Gipp from The Retronauts podcast recently interviewed the editor in chief of UK’s Retro Gamer magazine who was shocked to hear that some people collect PAL versions of console games as opposed to NTSC ones. We in Europe were never treated with good framerates, but why would you not want them, given you can now access everything through the internet? Because they wanted to experience a restorative nostalgic moment; to perfectly recreate their childhood memories. In practice, it’s never that black and white. I like the HD version of Age of Empires II , but I would still play the original on my old hardware if I felt like it. The “remastered” version of Warcraft III is a gigantic trainwreck. I don’t want to touch Diablo II: Resurrected . But that’s just me pretending: my hardware currently can’t handle it. I want to play it so bad: it’s recently got a new playable class—for a gameplay loop that’s twenty-six years old, that’s just crazy! Wait, the game is out on the Switch ?? Yet I’d never want to play the blurry mess that was the GameCube version of Paper Mario again if I can spin up a crispy clear version on the Switch that even comes with cleaned up and in my opinion an improved soundtrack. What about The Legend of Zelda: Link’s Awakening and the Switch remake then? I did not buy it and I have zero intentions of doing so. The aesthetics of the new version are off-putting to me: the original was a (very) pixelated 2D game. The new one technically still is, but the 3D engine full of bloom effects evoke an entirely different feeling . Yacht Club Games’ newest Mina The Hollower successfully evoked the original Link’s Awakening theme, but in doing so, it perhaps turned off a group of youngsters that are used to playing Fortnite . The fact that the engine allegedly stutters in more than a few places, dipping below 30 FPS, does not help either. Link's Awakening Rewakened: Looks like LEGO blocks to me. Where are my pixels? So where does that leave us with the upcoming Ocarina of Time remake? Just set aside that . You know you’re screwed—might as well admit it now. Related topics: / nostalgia / games / By Wouter Groeneveld on 12 September 2026.  Reply via email . This is not right, it’s not supposed to look this way! Why is this hyper realistic? Is this a From Software game ported to a finally capable Nintendo machine? Is this Ocarina of Time (OOT) or is this Breath of the Wild , where you can scope out a point in the distance from a tower, glide down and actually visit that place? Go back to point 1: it’s not supposed to look/work this way! Seventy bucks for a game that I already own on multiple platforms including the perfectly reasonable polished 3DS edition? I don’t think so. The chickens are not supposed to have more than three polygons. Now they look like plastic. Where’s the fog? Why can we see that much? Paper Mario 2: Thousand Year Door Mario RPG (remake? Debatable) Soul Reaver Anything from Nightdive Studios such as Rise of the Triad: Ludicrous Edition , DOOM (again) or the Turok trilogy Wonder Boy: The Dragon’s Trap (remake) Bio Menace Remastered Shooters like Project Warlock , DUSK , Nightmare Reaper , Wizordum Shovel Knight et al. Ruffy and the Riverside Adventure games like The Telwynium , Crimson Diamond , The Aching , Gobliins 6 , and even The Drifter Forbidden Solitaire The Castlevania collections ( Dominus , Advance , Anniversary ) The Turtles Cowabunga Collection The Turok trilogy The Duke Nukem collection 1 for Evercade

0 views
Chris Coyier 2 days ago

Underdesk Treadmill

I pulled the trigger on an underdesk treadmill. Basic research suggested GoPlus is a decent one. I was hoping it would be $300-400 USD. Turns out this one is just $119.99. So cheap it had me a little worried, like it was going to be cheap junk, but I pulled the trigger anyway. It took 2-3 days only to get here, and it’s… kinda nice? They must be trying to unload them or something cause it seems a little too to be true. Ask me in a few months I guess.

0 views
David Bushell 2 days ago

RSS Club #009: Domains

This is an RSS-only post, thank you for subscribing :) I keep buying domains! When will I ever learn? Domain prices are increasing (again) because why wouldn’t they? Sub-domains are free! Host your side project on a sub-domain! Then if you get bored, who cares? I used to know this but somewhere along the way I lost the knowledge. My old projects like Shiro and Tales and print.css live long happy lives undisturbed by man. I’ve been busy moving newer side projects to sub-domains or archiving them on my self-hosted Git forge . These are the ones I remember: expires in six months. Attic is archived. expires in a year. Croissant is now on a sub-domain. might needs to be renewed indefinitely to avoid squatters (it has juice). Is the strategy to stop redirecting and leave it blank for a year? Pikaday is now on a sub-domain. is impossible to move without severing RSS subscriptions. I thought a legacy framework like React would have died out by now; how wrong I was! is a piece of art waiting on Google to reap before it can expire. has a story behind it but I think interest has waned. Recent events transpired to nothing. The courtroom drama is glacial. turned into my second notes blog but the domain still lives. Cotton Coder was to be my brand for web education. Glad I didn’t invest there — yikes that side of the industry evaporated quickly! is expiring in 19 days, used once for a joke. I’ve lost track of how many domains I’ve bought for a year and never used. “Buying” is the wrong terminology. “Renting” or “leasing” should be legally enforced. I have a dozen dead domains I bought rented for possible company names before I settled on Valley Fold ( ). For brand names it’s difficult not to buy all the variations. I don’t own the but I did snap up the — primary, — criminal they sell that separately, and for future use like website staging. Want to guess how many TLDs and variations of I own (or owned) over the years? I let most go. It’s silly and expensive squatting them. FYI both myself and my ownership of pre-date the D-Bus project . Maybe it’s hell maybe it’s not, that’s not my beef! In hindsight though I wouldn’t have chosen if I knew it would become my freelance business. It took a year of indecisiveness before Valley Fold Ltd was born. I was kicking around other names and domains but I’m happy where I settled. I paid a three-figure transfer fee for on a whim (ouch). I quite liked the sound of it… no one else agreed. Originally I wanted (note the different spelling) but more than one “Quiet” agency exists with the same idea. Another business name candidate was “Focus Visible” inspired by Clearleft . A reference to CSS if you’re not familiar. I did like the nod towards accessibility too. The problem with this name is: do I use or with a hyphen? The hyphen fits the CSS property but hyphenated domains are lame. I bought both. All the big TLDs in fact. I was surprised they were available (not anymore lol). “Focus Visible” also sounds like a team name from The Apprentice . Which is cringe. Needless to say I have more domains waiting to expire than I’m actively using. I set up a second registrar account with notifications disabled so that I don’t have to see them! My new goal is to reach 2027 without renting a new domain. We are not in a golden age of web development and frugality is necessary to survive! Thanks for reading! Follow me on Mastodon and Bluesky . Subscribe to my Blog and Notes or Combined feeds.

0 views
Unsung 2 days ago

“Sweet, a whole website of video game menus”

The Game UI Database is a website that covers the interfaces of almost 2,000 via over 75,000 screenshots. = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/sweet-a-whole-website-of-video-game-menus/1.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/sweet-a-whole-website-of-video-game-menus/1.1600w.avif" type="image/avif"> It has meticulous information architecture, so you can jump into specific sections, for example: Even if games and productivity software seem worlds apart – the creator of the site, Edd Coates, calls himself “UI artist,” which is not a title I’ve ever seen in my line of work – I’ve long thought games do some things better, and these can be an inspiration. The database was started by Coates a few years ago. This launch article on Mashable has this interesting passage: The site is relatively short on context, with only the collections of screenshots gathered under different tags for visitors to go on. […] That’s the site working as intended. As Coates explained: “It’s useful for designers to identify recurring and pre-established patterns in successful titles when building their own interfaces.” Added context isn’t necessary because the contrasting approaches evident in each image is the whole point. Needless to say – I mean, you know this after spending 12 seconds on this blog – I disagree. But it seems the author is too, since he’s working on a book called The Game UI Bible , to be released in the first half of 2027: = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/sweet-a-whole-website-of-video-game-menus/2.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/sweet-a-whole-website-of-video-game-menus/2.1600w.avif" type="image/avif"> = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/sweet-a-whole-website-of-video-game-menus/3.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/sweet-a-whole-website-of-video-game-menus/3.1600w.avif" type="image/avif"> = 2x) and (width >= 700px)" srcset="https://unsung.aresluna.org/_media/sweet-a-whole-website-of-video-game-menus/4.2096w.avif" type="image/avif"> = 3x) or (width >= 700px)" srcset="https://unsung.aresluna.org/_media/sweet-a-whole-website-of-video-game-menus/4.1600w.avif" type="image/avif"> I recognize the volume is price’y and last time I was excited about a book , it turned out to be quite a disaster . The Game UI Bible book also touts a lot of interviews with UI artists, and I find these to be a mixed bag in practice; it’s really hard to interview people in a way that yields something interesting. But still, I am excited again – and if you’re not, the Game UI Database is worth checking out. (The title of the post comes from a different blog post about the database .) Examples of game difficulty settings Examples of game UI settings menus Modals and pop-ups Loading screens

0 views
Lalit Maganti 2 days ago

I made a build visualizer to understand Bun’s compile times

I built buildprof ( Github ), an open-source tracing tool that shows where the time goes when you compile software on Linux. Here’s a realtime video of it profiling a clean build of ripgrep: Watch the buildprof demo Sometimes, builds are slow because there is simply a lot of code to compile. But more often than not, there are fixable problems: poor parallelism, repeated work, dependency downloads or a huge compiler/linker invocation. buildprof makes all of this clearly visible, so you can see what’s worth investigating and optimizing. You run it by putting in front of any build command you already use: buildprof records every process your build command launches, including their subprocesses (and their subprocesses…), and lays them out on one timeline. Time moves from left to right, bar width shows duration, and child processes appear beneath whatever launched them. I made buildprof because this tweet from Jarred Sumner, chief architect of the Bun JavaScript runtime, was living rent free in my head: Specifically, the claim that Bun’s new Rust build was >5× faster on Linux than its old Zig build really bothered me. In my experience, Zig projects had usually compiled much faster than Rust projects of similar complexity. That intuition was enough to make me feel there was a mystery to solve. This was further compounded by another important, yet easily missed, detail in the tweet: the Zig build used Full LTO, while the Rust build used ThinLTO. Compilers normally optimize separate compilation units largely in isolation. 1 Link-time optimization (LTO) lets them optimize across those boundaries. Full LTO brings those units together into one large optimization job, while ThinLTO preserves more separation so much of the work can run in parallel. From past experience, this difference can have an enormous effect on build time. The tweet mentioned it in passing, but I wondered how much of the headline improvement it explained. I started by trying to reproduce the numbers. I checked out Bun 1.3.14 and Bun 1.4.0 and wrote some scripts to replay their Linux x64 CI builds on a 6-core, 12-thread Linux VM. The scripts preserved the build steps and their dependencies, running everything on one machine. 2 My timings were in the same ballpark as Jarred’s: OK, so the gap showed up on my machine too. But a lot had changed between the two measurements besides the language; so what was actually responsible? Was it the Zig compiler that was taking all that extra time? Or maybe it was the Full LTO link? Or perhaps there was something else in Bun’s build I hadn’t even thought to look at? This is where my profiling and developer-tools brain kicked in. Usually, when I’m trying to understand why something is slow, I want a trace: what happened, when it happened and how long it took. It would be really cool to have that for these builds, to put them on a timeline and see where their time actually went. But a build involves a lot of different tools, each with its own idea of what’s happening. What could I record that would let me see across all of them? When you type or , it feels like you are running one program. The build system works out what needs to be rebuilt, the ordering between those pieces and what can run in parallel. But generally, it does not perform all that work itself; it launches compilers, code generators, archivers, linkers and arbitrary scripts. Which can launch more programs which launch some more… Different build systems describe that work in different ways. Cargo sees crates, Ninja sees build edges and CMake generates instructions for another build system. From the operating system’s point of view, however, they (mostly) look like processes launching other processes. 3 A Rust build, for example, might contain a chain like this: If we record when each subprocess starts and ends, we can lay them out on a timeline. Here’s what that chain looks like in buildprof: There are also several nice properties to visualizing a build at this layer: This gave me a starting point for buildprof: record the process tree, then turn it into a timeline I could explore. There are plenty more details to get into, which I will do later. But once I had that working, I could finally go back to my initial question: what was Bun doing for those twenty-four minutes? I started by recording the Zig-era CI build with buildprof, using the same scripts as before : Explore in buildprof Right away we can see a huge problem: the linker invocation dominates the build time. It ran alone at the very end for over sixteen minutes, about two-thirds of the entire build. What the heck was it doing for all that time? Clicking on the linker shows its command line, which buildprof captures automatically: There’s Full LTO, just as Jarred said. Given how long the link was taking, it was now my main suspect. But the process tree alone couldn’t tell me whether LTO was actually responsible for those sixteen minutes. Thankfully, LLD records its own internal timing events, and buildprof can include them when you use . I recorded the final link again , this time with enabled: Explore in buildprof Now we can see that LTO is where almost all the time goes. The linker is running compiler passes over the program, not just combining already-compiled files. The bar alone takes just over ten minutes and includes the passes which generate machine code. 4 With so much of the Zig build spent in LTO, I wanted to see how much time the Rust build spent linking. I recorded that build too: Explore in buildprof Just 2m24s. And this time, as expected, the linker command contains : Both builds were doing LTO, but with different settings and very different link times. What if I kept Bun’s Zig code and changed Full LTO to ThinLTO? How much of the gap would that close? I switched Zig Bun’s build flags to ThinLTO and recorded another clean build, along with a fresh Full-LTO build for comparison: Explore in buildprof: Full LTO · partial ThinLTO The link got 3m40s faster in this pair of recordings, but it was still taking nearly thirteen minutes. Why was linking still so expensive? Looking back at the compiler trace, a lot of the work was on functions with in their names. That’s JavaScriptCore, the engine Bun uses to execute JavaScript. The linker was spending time compiling the JavaScript engine too. 5 Clicking on the linker invocation showed the WebKit libraries among its inputs, including : Following those inputs back through the build, I found that Bun wasn’t compiling these libraries itself. It was downloading them from a separate WebKit build. And when I checked that build’s flags , there it was again: . The Rust build used a newer WebKit revision whose build recipe selected ThinLTO . Even though I had changed how Bun compiled its own code, those downloaded libraries still contained Full-LTO inputs and so the linker still had to optimize that code and turn it into machine code. To change that, I would have to rebuild WebKit too. I checked out the historical WebKit revision and rebuilt it and its ICU dependencies with compatible ThinLTO settings. Then I replaced the downloaded libraries with the ones I had built, keeping the ThinLTO changes to Bun. Here are the recorded builds: 6 The link now took 7m22s. Still slower than the Rust build, but enough of an improvement that I wanted to look beyond the linker. The build still took fifteen minutes, and nearly eight of those passed before the linker even started. What was it waiting for? I went back to the original CI trace to follow the inputs from Bun’s own code. buildprof also records which files each process reads and writes. If a process reads a file another wrote, it links the two together under the hood. Turning on “Show on timeline” draws those links as arrows. Here, the linker reads from the C++ compilation and from Zig. Both arrive through copy steps; following those back takes us to the processes which produced them: The C++ side of the compilation finished first. The linker was waiting for , so it could not begin until the Zig branch had finished too. It was at this point I went back to the Rust build and compared against how it worked, and the main reason the Rust build was faster became obvious: Bun has been split into >90 crates, while in Zig it was all trying to compile as a single Zig module! This meant that the Zig build cannot parallelise the same way Rust can. I also suspect, though I did not prove this, that it explains the slow linking: the linker has to optimize one huge ThinLTO bitcode module instead of the same work spread across crates. It was at this point I had to stop: to go any further, I would have to split up the Zig module myself, and given that this code is all obsolete anyway, I didn’t think it was worth doing that. Summarizing: And fwiw, the traces had also turned up a few things I couldn’t resist poking at… In the middle of Bun’s CI build, I found commands asking the public internet for the machine’s IP address, inspecting running Docker containers and reading the latest Git commit message. These take well under a second altogether. Nothing to optimize but I just wasn’t expecting to find them in a build trace. The builds above reused downloaded dependencies, so I also recorded a fresh WebKit fetch . Downloading and extracting the archive took about twenty seconds. For the first twelve, all we see is Node running. Then it launches and , and we can see the extraction separately. Earlier, we followed the linker’s inputs back to Bun’s C++ compilation. We can look inside those compiler invocations too. I picked one of the last files to finish, , and replayed its Ninja command with . For Clang, buildprof enables and adds its internal timings to the process timeline. 7 The replay took about twelve seconds, split almost evenly between Clang’s frontend and backend. Zooming in further, we see , one of the phases of Clang, accounts for over four seconds of the backend’s work. The recording side of buildprof uses , the same Linux interface used by debuggers. I did consider both eBPF and ftrace, but is just straight up perfect for exactly this type of problem; eBPF tracing means and permissions and hooking into potentially unstable tracepoints/kernel functions. While with ftrace, I’d have to juggle tracing instances to avoid interfering with other users, and getting the filters perfect for just the build process and all its descendants is cumbersome. 8 With , I can launch the build and follow its children directly. Its built-in events tell buildprof when processes fork, exec a new program or exit. And for filesystem activity, buildprof uses a seccomp filter to intercept only the calls it needs. How much buildprof costs is almost entirely down to how many files the build opens. For ripgrep, recording barely changed the build time. Redis opened files much more often, and recording added about five seconds: 9 If that overhead gets in the way, you can turn off filesystem tracing with and keep the process timeline. I work on Perfetto , so it was a natural starting point for the UI; buildprof’s UI is a soft fork of the Perfetto UI. I could have just opened the recordings on ui.perfetto.dev , but I wanted control over how the process tree was laid out, which details appeared when you clicked a command, and things like those on-demand arrows between file producers and consumers. Fortunately, we’ve spent the last several years working on making the Perfetto UI extensible through plugins . Most of buildprof’s UI is reusing that infrastructure. Perfetto handles the hard stuff (parsing traces, querying events, rendering the timeline and managing workspaces) and I get to focus on what makes those things useful for builds. I plan on going into a lot more detail about the recorder and UI in a separate technical post. Subscribe if you’d like to be notified when it comes out! :) These days it’s very easy to make a tool just because you can. But that wasn’t the case here; before building buildprof, I looked long and hard for an existing tool that could give me this view. I started with ninjatracing , which I’ve used many times. It turns Ninja’s build log into a timeline showing what ran and how much ran in parallel. Here’s the Ninja log from the Zig-era build . But Ninja only sees part of Bun’s build. The scripts which invoke it are missing from its log, and commands it runs appear as single blocks even when they launch whole trees of subprocesses. There were several other tools, each covering different parts of the problem: What the Fork ( via ) came closest: it follows processes across build systems and presents a build-specific view. But as far as I could tell, it still appears to be in private beta and there don’t seem to be any plans to make it open source. buildprof already does what I wanted it to do, and I plan to keep working on it as I use it on my own builds. But there are a few things I’d like to improve. Recording overhead is one; the Redis measurements showed there’s room to improve filesystem tracing, especially for builds which open lots of files. I’d also like to support macOS where I do some of my work and maybe Windows if there’s interest. There are also more build systems and toolchains I’d like to test, including npm, Gradle and Bazel. Computing critical paths would also be a big improvement: we followed dependencies by hand in this post, but buildprof could help identify the chain of work holding up the build and automatically annotate it. I’ll probably tackle these as and when I need them. But if you try buildprof and there’s something you wish it could do, I’d be interested to hear about it . What people find useful will help me decide where to spend more time. I managed to satiate my curiosity, though I ended up spending rather more time on this than I expected. Along the way I built a tool I now want to have around whenever a build is taking too long. I know I’ll come back to buildprof the next time a slow build annoys me. If you have one of those builds too, give it a try . I’d love to hear what you find! In C and C++, a compilation unit is usually a source file together with its included headers . Rust compiles crates , which can be split into multiple code-generation units . Zig normally compiles a program’s Zig sources together as a single compilation unit . Bun’s Zig compiler fork supports splitting that into multiple LLVM modules, but its CI build explicitly selected one when LTO was enabled .  ↩︎ The Zig-era CI build ran its C++ and Zig compilation stages on separate Buildkite machines and passed their outputs to a final linking stage. My script ran those stages concurrently on one machine, waited for both outputs, copied them locally instead of transferring them over the network, then linked them. This should preserve the dependency graph, but due to the hardware differences and running both stages on one machine, resource contention would obviously be quite different. Also note that my timings are individual runs (albeit ones which were quite stable) while Bun’s reported figures are medians.  ↩︎ A process can do substantial work internally, including running multiple threads, without launching anything else. The process timeline won’t show that parallelism. To see inside a process, we need tracing from the program itself, as Clang and LLD provide in the examples below.  ↩︎ LLVM emits from its legacy pass manager, which LLD uses for code generation. The inlining and other IR optimization passes can appear before it, so this bar is not the total time spent optimizing a module.  ↩︎ In the earlier Full-LTO linker replay, 26,825 events with JSC symbols total about 209s. This is summed event time, not a measurement of JavaScriptCore’s entire contribution to the link. One example event takes 2.94s; its symbol demangles to .  ↩︎ Recording script . These timings are just for building Bun with the libraries already available; the WebKit and ICU rebuild happened beforehand and isn’t included. Of course, I could point buildprof at that build too, but that’s another rabbit hole… I did not rebuild a matching Full-LTO WebKit archive as a control, so I cannot attribute every second saved to the LTO setting alone.  ↩︎ buildprof currently supports compiler traces from Clang, LLD and nightly Rust.  ↩︎ eBPF tracing uses capabilities such as and , as described in the kernel’s capability definitions . ftrace provides separate tracing instances and PID filters , but these still need configuring and access to tracefs. also depends on the host’s security settings; containers may need additional permissions to allow tracing child processes.  ↩︎ Medians of five clean builds per mode on the same VM, with six build jobs. Measurement script .  ↩︎ It’s build-system agnostic : Cargo, Ninja, Zig, Make and most other build systems do much of their work by spawning processes, so we do not need to write a special integration for each one. It naturally includes custom scripts : This includes both scripts above the build system (repository setup, dependency fetching) and scripts underneath it (code generators, asset processors). We can follow the files between build steps : recording which files each process reads and writes lets us see which steps produce the inputs for others. This even works across build systems! The huge outlier in the initial Zig build vs the Rust build was the massive linker step which ran alone at the end of the build. Changing the LTO settings for just Bun was not sufficient as WebKit, a significant part of the build, still used Full LTO. Once I had done this, the Zig build dropped from twenty-four minutes to fifteen. Even after this, linking still took 7 minutes and the whole build 15 minutes. The overwhelming difference which remained was structural: Rust spreads compilation across >90 crates while the Zig build funnelled everything through a single module. Cargo timings works well for Cargo-managed builds, but cannot break down arbitrary work inside or see wrapper scripts above Cargo. In Bun, Cargo is only part of the build: the report I captured covered 1m51s of a 5m40s CI build. Clang’s gave us the detail inside a compiler invocation, but cannot show what the rest of the build is doing while Zig’s Tracy integration goes deeper still and is intended more for understanding the compiler itself. and can follow arbitrary processes through and , but show general process events rather than a build-oriented timeline. In C and C++, a compilation unit is usually a source file together with its included headers . Rust compiles crates , which can be split into multiple code-generation units . Zig normally compiles a program’s Zig sources together as a single compilation unit . Bun’s Zig compiler fork supports splitting that into multiple LLVM modules, but its CI build explicitly selected one when LTO was enabled .  ↩︎ The Zig-era CI build ran its C++ and Zig compilation stages on separate Buildkite machines and passed their outputs to a final linking stage. My script ran those stages concurrently on one machine, waited for both outputs, copied them locally instead of transferring them over the network, then linked them. This should preserve the dependency graph, but due to the hardware differences and running both stages on one machine, resource contention would obviously be quite different. Also note that my timings are individual runs (albeit ones which were quite stable) while Bun’s reported figures are medians.  ↩︎ A process can do substantial work internally, including running multiple threads, without launching anything else. The process timeline won’t show that parallelism. To see inside a process, we need tracing from the program itself, as Clang and LLD provide in the examples below.  ↩︎ LLVM emits from its legacy pass manager, which LLD uses for code generation. The inlining and other IR optimization passes can appear before it, so this bar is not the total time spent optimizing a module.  ↩︎ In the earlier Full-LTO linker replay, 26,825 events with JSC symbols total about 209s. This is summed event time, not a measurement of JavaScriptCore’s entire contribution to the link. One example event takes 2.94s; its symbol demangles to .  ↩︎ Recording script . These timings are just for building Bun with the libraries already available; the WebKit and ICU rebuild happened beforehand and isn’t included. Of course, I could point buildprof at that build too, but that’s another rabbit hole… I did not rebuild a matching Full-LTO WebKit archive as a control, so I cannot attribute every second saved to the LTO setting alone.  ↩︎ buildprof currently supports compiler traces from Clang, LLD and nightly Rust.  ↩︎ eBPF tracing uses capabilities such as and , as described in the kernel’s capability definitions . ftrace provides separate tracing instances and PID filters , but these still need configuring and access to tracefs. also depends on the host’s security settings; containers may need additional permissions to allow tracing child processes.  ↩︎ Medians of five clean builds per mode on the same VM, with six build jobs. Measurement script .  ↩︎

0 views
Anton Zhiyanov 2 days ago

Solod 0.4: Better C interop

Solod is a subset of Go that translates to regular C — with zero runtime, manual memory management, and source-level interop. It's designed for two main audiences: The new Solod release provides an easy way to call third-party C libraries, makes a large part of the standard library freestanding, and impoves the tooling. Automatic bindings • Freestanding packages • Type assertions • C interop • Multi-package testing • Checks and targets • Windows • Wrapping up Sobind generates bindings — stubs for calling third-party C libraries from Solod. It parses files and emits a Solod source file with necessary structs, unions, constants, variables, function pointer typedefs, and function declarations. You can then use the generated types and functions in regular Solod code: Usually, the generated bindings are good enough to use as they are, without any manual changes. I have also prepared bindings for popular C libraries like libuv , raylib , sodium , and sqlite . Unlike Go, calling C from Solod has zero overhead — Solod code is just regular C in the end. At some point I decided to make as many packages as possible freestanding — independent of any libc implementation or specific OS runtime. That went pretty well. Solod now has 37 standard library packages, and 31 of them work in freestanding mode. These packages work in freestanding mode with no restrictions: These packages work in freestanding mode with certain limitations: There's a separate post with more details if you're interested. A comma-ok type assertion is now fully supported for non-empty interfaces: Which translates to the following C code: Previously, the only two supported forms were a direct assertion like and a check-only form like . The package now supports more common C types: There's also a type, which maps to a C . You can use it where C expects a pointer: Finally, there are some useful cast functions. reads the bits of a value as another type of the same size: You can use instead of a pointer conversion such as . and return a typed pointer to the string or slice data: They replace and . can now run tests from multiple packages at once. If you use a pattern that ends with , it will select every package that has a subdirectory under its base directory: The entire run only needs one translation, one compilation, and one execution, which is much faster than running it separately for each package. The flag restricts the run to only the packages listed in a file: , , and take two new flags: and . specifies the target platform for cross-compilation. Use the same value that and accept after : enables code analysis: The default optimization level is . You can use to change it. The standard library now builds for and . All packages in the freestanding set work. Packages that require POSIX ( , , , , , ) are not supported. You can use to cross-compile for Windows: Not the first-class Windows support that Go offers, but it's better than nothing. With v0.4, Solod can work with almost any C library thanks to automatic bindings. The freestanding-aware standard library makes the language a viable option for bare metal programming. Extra interop helpers make C-calling code easy to read, and better tooling keeps tests fast. There's still a lot to do, of course. In the next release, I plan to focus on the standard library and bring over some hashing and crypto packages from Go. More C library integrations are on the way too! If you're interested, take a look at Solod's readme — it has everything you need to get started. Or try Solod online without installing anything. Go developers who want low-level control without having to learn another language. C developers who like Go's style. depends on a user-provided hook to read random bytes. depends on a user-provided hook to print formatted text. offers a working subset of features. works fully, except it can't resolve an IPv6 zone name. works on targets that support lock-free instructions. depends on a user-provided hook to print test results. reads the clock using user-provided hooks. depends on hooks from both and .

0 views

In the world

In memory of Nevan Scott, 1985–2026 I met Nevan shortly after I began A Working Library . I don’t seem to have that particular email around anymore, but I recall that he wrote the politest, most generous note to alert me to a typo. I think it was late 2008, or maybe early 2009. We would end up crossing paths online many times in the following years, over Twitter—those were the good days, for some of us, at least—but also email, which then and now remains a more humane way of talking. I made a lot of typos; we talked about books and tech and work. We stayed in touch as our various work lives changed—he moved to Europe, I moved into (and eventually out of) startupland. At some point I needed some technical counsel with what I was only just beginning to realize was much more than a blog, and it was Nevan who I reached out to. There’s a real intimacy to letting someone in to your private repo, letting them see all the embarrassing first (and second, third, fifth) drafts, but I never doubted I could trust him. He was not only generous with his support, but eager to step in as a peer and collaborator. He took my work seriously—a great gift, too great to ever fully repay—but he took his own work seriously, too; everything he did, he did well and with care. For the last decade and then some he has been a regular contributor behind the scenes, offering astute feedback and proposing different ways to set things up, showing me how to do the things I could think of but didn’t know how to pull off. And of course, catching typos. We had a long-running barter agreement: he would reach out to talk about his work and I would listen, offer counsel, help him see what he already knew; I would reach out when I broke something, always with the caveat that it wasn’t urgent, to get to it when he had the time. And inevitably I’d get a pull request an hour or two later with a proposal, plus a bug fix or upgrade he noticed while he was working. It sounds incredibly mundane to talk about it now, a friendship measured in video calls and commit messages, but then the word mundane comes from the Latin mundi, or world. To be mundane is to belong to the world, to be in the world, of it and within it. Not outside of it—not lost, the way so much technology tries to make us lose ourselves—but in the world as only the truly living can be. As we can only be with each other. Nevan was a teacher. In all his work, in building schools and publications, he never lost his love for teaching. He was most himself, he told me, when he was in class, with his students. In the last year, he began teaching how to use AI in design, a topic on which he was intensely conflicted. He worried he was doing harm. But he also knew that the people who came to his classes were going to learn about AI one way or another, and maybe if he was the one teaching them, they would learn not only how to use AI but how it would try to use them ; that this was the kind of learning that should be undertaken only with the greatest of caution. Caution is, in Samuel Johnson’s definition, “provident care”—that care which foresees and considers the future. Our friendship covered a period of years in which the future would change dramatically: from an excitement and even optimism about tech (unearned and unwarranted, in retrospect), to a dehumanized nightmare, stultifying and terrifying in equal measure. Through it all we both maintained a sense that—whatever the horrors of capitalism—there was something keenly human and joyful about putting words and type on the internet. That we did so is a credit to our collaboration, and Nevan’s beautiful legacy. Near the end of The Blue Place , as Julia is dying, she tells Aud to “stay in the world.” Aud promises to do so, but the truth is she does not know how. It is so very hard to stay in the world, to hold on to the mundane and ordinary, those fragile human connections, amidst all the forces arrayed against us. I think it is only in the effort to learn that we come close to that knowledge; and perhaps only in the effort to teach that the learning really takes hold. View this post on the web , reply via email , or become a supporter .

0 views
Farid Zakaria 2 days ago

A Nix store is three functions

While building trynix I needed somewhere to host a store-path that did not exist on cache.nixos.org . 1 I wanted to demonstrate that non-Nixpkgs store paths could be booted just as easily. The only requirement seemed to be a lenient Cross-Origin Resource Sharing (CORS) policy, , because the fetch happens in JavaScript. Turns out that GitHub Pages sets that header on every file it serves. 😈 I committed the output of to my Git repository and voilà, I have a free Nix substituter. I seem to be late to the party on this discovery. tomberek’s github-store is a cache assembled out of GitHub release assets. 2 GitHub Pages or Releases are a static file server. It has no idea what Nix is. If a humble file server can be a Nix binary cache, what else could we use? Turns out that in order to be a Nix binary cache, you must implement only three simple functions. The Nix client does not care what medium you use to implement them although HTTP is the most common and included by default in CppNix 3 . Anything that can answer those three requests can be used as a remote Nix store . 4 The reason we can be this careless about transport is that Nix does not trust it. A narinfo’s signature ( ) field covers , , and . It does not cover , , or . Once the archive is fetched, Nix decompresses it and checks that the matches. This is the special sauce of how packages that were signed by cache.nixos.org can be fetched from any other binary cache as an intermediary, and the signature still validates. The field does not even have to be on the same host as the narinfo. It can be anywhere on the internet, and it can be a different protocol than HTTP. Nix does not care. The only thing that matters is that the archive fetched from has the same as the narinfo. For protocols that are not included by default in the Nix client, you can always write an HTTP proxy that translates the three functions to whatever medium you want. In research for this post, I found a few interesting ones. gachix : puts the archives in git’s object database. Git content-addresses and delta-compresses blobs already, so the store dedupes itself; the author reports roughly 82% smaller than the equivalent plain cache. DNS : I wrote a proof-of-concept that puts the narinfo and 4 KiB slices of the archive in TXT records. The narinfo is small enough to fit on one record but the archive needs to be chunked. pastebin : a pastebin can hold the narinfo and the archive. The narinfo is small enough to fit on one paste, but the archive needs to be chunked. Many pastebins have an expiry policy which acts as a natural garbage collector. nixcache-oci : uses an OCI registry to store Nix archives. infinite storage glitch : encodes data within a video and uploads it to YouTube. “Everything is available on npm” – Some person on the internet Unsurprisingly, npm is a great binary cache and it has some interesting properties for release management we can ab use. emits a directory and npm publishes directories: a match made in heaven. 💑 Let’s walk through a small example. It is dynamically linked against glibc, so the closure is five paths and roughly ~36 MiB: We copy it to a local cache, signed with our own key, and add the one file npm needs ( ): then dutifully packages our complete closure for us: is now a real package on the public npm registry. It is now a substituter you can point Nix at directly: Note We have to use to run the binary because is a chroot store and all the are still under . If we had relocatable binaries we could run it directly. That is Nix fetching the complete closure from npm and running it. 🤯 We can distribute Nix packages to non-Nix users, let the infection spread! As an added bonus, similar to Nixpkg and NixOS we can get nice “channel” semantics by using npm’s dist-tags. The tag is mutable and points to the latest version, while each version is immutable and points to a specific store path. The major downside of this approach is that npm ahs no incremental publishing. Every version is a whole tarball, so fifty closures sharing glibc upload glibc fifty times. We could fix that by publishing each store path as a separate package, and then having a small index package that points at them. Each store path would then be uploaded exactly once. I won’t build that though as it’s not in good faith to the npm ecosystem. What other store implementations can we find? I was also waiting for @domenkozar to enable CORS on cache.nixos.org so I could use it.  ↩ In order to be a Nix binary cache, the prefix is stripped from the field in the narinfo, because GitHub releases are a flat namespace.  ↩ You can write a Nix plugin to implement a new protocol if you wanted.  ↩ We will see that that they must not all be all on the same medium, protocol or domain even!  ↩ I was also waiting for @domenkozar to enable CORS on cache.nixos.org so I could use it.  ↩ In order to be a Nix binary cache, the prefix is stripped from the field in the narinfo, because GitHub releases are a flat namespace.  ↩ You can write a Nix plugin to implement a new protocol if you wanted.  ↩ We will see that that they must not all be all on the same medium, protocol or domain even!  ↩

0 views