More from anderegg.ca
I really enjoy John Gruber’s podcast The Talk Show. However, I’ve noticed that it’s been a while since it was in my feed. I decided to chart previous episode dates to check some of my assumptions about release frequency. Cutting to the chase, here’s a link to The Talk Show Timeline. You can click/tap on days to get a pinned tooltip, which lets you link out to a particular day’s episode. I’ve tried to make this work well on mobile devices, but you’ll likely find it works best on desktop. A note about this: Gruber has mentioned in the past that there has been a personal situation that he’s been working through. He’s also mentioned something to this effect on the podcast at points, but hasn’t gone into detail. I don’t need to know more than has been said, and I just hope things are alright. I’ve read Gruber’s site since 2002 1 and listened to his podcasts since 2007. In that time, I’ve learned that he operates on his own timeline… and has a penchant for procrastination. No shade here! He’s set himself up with a work situation that I envy. I also don’t want to seem grumpy about the lack of podcast episodes; I’m just trying to understand how this current break compares to those in the past. Because it was easiest, and because it would answer my immediate questions, I only looked at the current iteration of The Talk Show. Happily, there’s a single page which lists all the episodes. It had a tiny bit of data weirdness, 2 but I was able to turn this into a JSON file with the details I cared about. With that, I used the calendar heatmap chart in Apache ECharts to display the episodes over time. Some things I noticed: While it’s been a while (29 days) since the last episode (on August 10), this isn’t unheard of. The average is about 12 days between episodes, but there have been 6 other times when the episode gap has been at least this long. The longest gap was 62 days between December 24, 2024 and February 24, 2025. There’s exactly one day with two episodes: July 19, 2014. It was “Cat Pictures” with Marco Arment, which got split into two “sides”. Here’s a link to side 1, and here’s side 2. I mentioned procrastination: 73% of episodes take place on the 16th of a month or later. 28% of episodes take place on either the last or second-last day of the month. Again, no shade! It’s something I chuckle about whenever I notice it. At the current rate, this year is about in line with output going back to 2023. While it has been a while since a new episode dropped, it’s not out of whack statistically. Anyway, this was a Labour Day project that was mostly for my own amusement. I expect there will likely be a new episode of The Talk Show some time fairly soon after tomorrow’s Apple event. I also don’t know if I’ll keep this chart up to date… though it’s not terribly onerous to do now, so who knows. I found Daring Fireball because I was researching the purchase of my first Mac around this time. It wasn’t until October of 2002, but I ended up buying one of the machines he wrote about in his first post that August. ↩ The following dates don’t match the main format: Sunday 30 November, 2025 Mon, 24 Feb 2020 16:05:34 EDT Tuesday 31 December 2019 Tuesday, June 25 2019 Sat, 16 Mar 2019 19:43:49 EDT Wednesday 31 December 2014 Monday, 30 Jun 2014 ↩
I mostly use my web stats service because I get a lizard-brain kick from watching numbers go up, but it also provides some fun insights. For example, it’s how I found out about bubbles.town. At first I thought this might be a Mastodon instance. I get a lot of referral traffic from Mastodon, but each instance has its own URL. You can guess at mas.to, but dice.camp doesn’t provide many clues. Bubbles, as it’s known, is a very nice service. It aggregates a curated list of indie blogs. People can sign up and vote for posts they enjoy, but they can’t submit new articles. Authentication and comments are both handled using Fediverse technologies. In my case, I was able to log in via my Mastodon instance. The idea is to highlight cool indie writers around the web. I love this sort of thing so very much. Anyway, this is all to say that you should check out Bubbles. Seeing a blog-based reaction to one of my posts on the service filled my heart with immense joy.
I’ve been following Apple news and rumours since the year 2000, and I don’t think I’ve ever been less excited for WWDC. Apple’s software story is in rough shape, and I worry that this year’s event won’t do much to help. Perhaps it’ll be the beginning of an upswing, but we’ve got quite a way to go. Before looking forward, let’s look back. WWDC 2024 focused on a number of “Apple Intelligence” features that never materialized. Which is probably for the best because the features that did ship that year were annoying at best and awful at worst. WWDC 2025 introduced a new redesign that I was initially looking forward to… until I saw it in practice. This year, the scuttlebutt is that WWDC will once again be about LLM-based features. Many of those features sound like re-announcements of the ones from 2024. I wasn’t excited about them then, and I’m still not now. If there are Apple Intelligence features announced on Monday, I hope they’re actually useful and not just shoehorned in. It’s reasonable for Apple to do something to improve Siri, but it’s been terrible for so long that I’ve found much better alternatives. I also worry that Apple risks overloading people with “AI” features. Microsoft only recently started learning their lesson about this, and it would be a real shame if Apple wasn’t taking notice. Given Apple’s recent track record, I’m not optimistic. I wish Apple would be content with its excellent place as a platform for machine learning and LLM-based work. I truly believe that local LLM use is the future, and it’s a future with far fewer downsides. I would love for Apple to focus on this, as it’s something they’re already doing well. None of the rumours have hinted at local LLM improvements, but it’s more developer- than user-focused. Maybe there will be more news at the State of the Union event after the keynote. My guess is that most AI features announced will still be sent to a server farm, and will likely involve subscription fees. There’s also been talk about tweaks to the Liquid Glass design language. I’m somewhat more hopeful about this, but still concerned. Apple’s head of design changed late last year, but that’s not much lead time. If there are changes, they won’t be drastic. Apple spent a lot of time re-architecting parts of the system to support Liquid Glass, and then asked developers to update their apps to support those changes. There’s no easy way to revert things, and we’d be in for another rough year of software quality if they tried. Instead, we’ll get minor tweaks. Here’s hoping for more design concerns to be addressed. The other thing I’ve heard is that this could be another “Snow Leopard” year. That is, Apple may have spent the last year fixing bugs and optimizing things. I’d be very happy if this was the case, but it is a bit at odds with all the AI rumours. Even if Apple has been doing a lot of polishing, it’s a difficult thing to market. Maybe Apple could brag about the number of bugs they’ve squashed? That would make me very happy, which tells you the sort of person I am. It’s definitely not exciting, but it’s the thing I’m most looking forward to. I’ve been worried about the direction of Apple’s software quality for a while. Their hardware game has never been more on-point, so it’s potentially hopeful that the head of hardware will soon be the new CEO. But this change, like the change of design lead, isn’t happening in time to affect this year’s WWDC. Maybe I’m wrong to worry. Maybe Apple will surprise me tomorrow. Sitting here today, I’m much more interested in what WWDC 2027 might look like.
No matter how you feel about AI, it’s changing the world of software. The “T” in ChatGPT was invented to improve language translation, and large language models (LLMs) are very good at this. Interestingly, translating between French and Japanese is effectively the same as translating between English and Python for these systems. As LLMs improve, we’re also finding that there’s little difference between “help me fix mistakes in this document”, and “find the flaws in this codebase”. LLMs are now great at both tasks, but the latter has much larger implications. Last month, Anthropic announced Claude Mythos, a next generation model which shows a significant leap in performance over currently available models. During testing, Anthropic noticed that the model was also much better at finding software flaws. The biggest improvements here come from identifying chains of issues which can be combined to produce a larger effect. Because of this, they decided not to release the model to the general public. Instead, they started Project Glasswing, which invited key software vendors to find issues in their code first. Some are calling this a marketing stunt. It’s at least a bit of that. Anthropic’s brand focuses on trust and safety, so holding back a model due to cybersecurity worries helps promote that ideal. However, the model’s system card shows real improvements across the board. More than that, we’re already dealing with AI systems affecting security today. Daniel Stenberg is the original author and lead developer of cURL (and the libcurl library). cURL is a command-line tool for transferring things over the internet. It’s nearly 30 years old, and it’s boring infrastructure in the best possible way. The libcurl library (which allows people to embed cURL’s functionality into their apps) is used in just about everything. Last year, Stenberg wrote about how AI submissions were making reviewing bug reports much more difficult. In that post, he links to previous thoughts about AI. He’s not been a fan, and his reasoning was understandable — other people’s use of AI was making his job worse. In January of this year, he shut down cURL’s bug bounty program to try and stem the deluge of AI bug reports. But then something changed. As the bug bounty was being wound down, people were realizing that Anthropic’s Opus 4.5 model, released in November of 2025, was a massive leap forward in terms of code generation. As more people started using this model, and similarly powerful ones from other AI vendors, the quality of the automated bug reports improved dramatically. At the beginning of April, Stenberg noted that he was seeing less “AI slop”, and was now dealing with a tsunami of reasonable bug reports. Just two weeks later, he shared a thread of charts he was preparing for a presentation. More reports than ever, less slop, and a higher quality of reports overall. It’s not just cURL seeing this, either. Mozilla, makers of Firefox, have written about the same change in bug reports from slop to helpful over the last several months. So have the Linux kernel team. Some of those issues have been around for nearly a decade, and could potentially have already been exploited. In a startlingly short amount of time, AI generated bug reports have gone from being a nuisance to being legitimately helpful. The issue is, even if these reports are helpful, they’re coming in faster than ever. There will always be more people looking for flaws than people willing or able to fix them. Finding flaws can be profitable, either through bug bounty reports or by sale to state-level actors. A troubling new pattern comes from the nature of open source: it happens in public. Security researchers can now have AI agents review all patches committed to a project. Those agents might find new issues, or even patches for high-impact vulnerabilities. In the former case, maybe they’ve found a new bug to report. 1 In the latter, maybe they have a new attack vector to leverage before anyone patches it. As frightening as this all sounds, some people are more optimistic. Mozilla recently published some initial thoughts about their work using the Claude Mythos preview. The main upshot is: yes, there are more attackers than defenders, but software defects aren’t infinite. If defenders can use systems like Mythos on their own codebases, they can potentially fix vulnerabilities before anyone has a chance to abuse them. Maybe future software could be defect-free. Personally, I think this might be a bit too hopeful. A larger organization like Mozilla will have the resources to be ever-vigilant, but not everyone will. If you run a large open source project, you can get Claude access for free… but the world is built on a pile of tiny projects. When those fall over, very bad things can happen. Even if the Mozilla take is correct, I worry that things will get worse before they get better. Whatever the future holds, I suspect that most people reading this won’t be security researchers or maintainers of software packages. Here are some concrete steps the rest of us can take: Software is forever An early lesson every developer learns is: software is never done. Even if you’re not adding new features, there’s always another security patch to apply or an API that’s being changed out from under you. This was true in the days before LLMs, and it’s even more true now. I’ve been working with more clients who have been vibe-coding apps. When used as internal tools, these apps can be genuinely helpful. However, I worry when I see they have access to important data or are hosted next to critical systems. If you have a system connected to the internet (or some other untrusted environment), you must have a plan to maintain it for as long as it will exist. Someone has to ensure the system is secure and apply patches when they become available. This has always been a good idea, but now it’s imperative. Update as soon as possible If there’s an update for your operating system or your phone, you should install it as soon as you can. Again, this has always been good advice, but it matters a lot more now. Really think before clicking things Phishing attempts powered by AI are more able to appear legitimate. They’re also easier to personalize. Recently Adobe’s customer support platform was targeted in a fairly novel phishing scheme. I suspect we’ll see more like this going forward. ⋯ We’re in an odd moment. It’s possible we’re at the start of something genuinely good, a time when security defenders can start outpacing attackers. It’s also possible that things are just about to hit the fan in ways we’ll all notice. Whatever the case, the software you depend on today will need someone watching it tomorrow. That part hasn’t changed, and probably never will. Here’s the prompt used to identify this issue. ↩
More in technology
Well, well, well, well, well, well, well, well, well, well, well, well, well, well, well. We're back. Sorry. We've been watching the onslaught of vulnerabilities flood the internet. Every man, dog, and their grandmas (apparently?) are now using LLMs to find and reproduce vulnerabilities - it’
You want less of them. That’s the reason. You may find that it’s too hard to stop people from doing the thing, literally blood, sweat, and tears trying to prosecute people, but that’s a different thing.
Solitaire Alone Together I made a new game. It's called Solitaire Alone Together. It's Windows 98 solitaire, but you can play with everyone else on the internet. Read the full post on my blog! Here's a raw link, if you need it: https://eieio.games/blog/solitaire-alone-together
This post is a living diary of all the times I messed up something with my website in a funny way. I value those who have the confidence to own their mistakes and share the learning with others, and so this is me doing just that! That Time I Accidentally Made a Tarpit That Time I Accidentally Made Really Large Headers That Time I Accidentally Made a Tarpit Back to Top A "tarpit" is an unofficial term used in computing to describe an intentionally slow response to a request. In these modern times many people are using tarpits as a way to combat the relentless theft of data by AI companies, although there's little to no evidence of that actually being in any way effective. I don't use tarpits, at least not intentionally, but there was that one time when I accidentally created a tarpit and trapped all visitors in it. As I've shared previously, I refuse connections from IP addresses that are blocked or belong to a blocked subnet, and I enforce this firewall during the TCP handshake. The logic here is straightforward: there's no reason to waste resources doing a TLS handshake, accepting an HTTP request, and then rejecting the connection if I already know I'm going to reject it at the earliest step. At the time, the code worked like this: the HTTP server would repeatedly call the Accept() function below expecting a new connection. I've added some comments to help explain the logic. func (l *firewallListener) Accept() (net.Conn, error) { // Accept the connection from the TCP listener. This blocks until there is a connection to accept or the listner was closed. conn, err := l.l.AcceptTCP() if err != nil { return conn, err } // Separate the IP address out from the remote address (which includes the port) ip := utils.SocketStringToIPAddress(conn.RemoteAddr().String()) if ip == nil { return nil, nil } // Check if it's blocked, if so close the connection and return a refuseError if IsBlocked(ip, true) { conn.Close() return nil, &refuseError{} } // Otherwise return the connection on to the HTTP server return conn, nil } If the incoming connection was from a blocked IP then I'd return a refuseError. I need to use a specific error interface because the HTTP server will halt if it encounters a non-temporary error from the call to Accept(), so I need to return an error that satisfies the definition of a temporary error. I defined refuseError like this: type refuseError struct{} func (e *refuseError) Error() string { return "." } func (e *refuseError) Timeout() bool { return true } func (e *refuseError) Temporary() bool { return true } func (e *refuseError) Is(err error) bool { return err == context.DeadlineExceeded } This did accomplish the goal of rejecting connections before the TLS handshake for blocked addresses, but it had one really unintended and difficult to track down side-effect. Accepting connections is done serially, after which servers typically then process that request on a dedicated thread (or in Go's case a goroutine). This means that any delays during the accept loop will block all incoming connection. What I had missed while reviewing the code for Go's HTTP server is that when it receives a temporary error from Accept() is that while it doesn't abort, it does sleep for up to a maximum of 1 second. This sleep blocks the entire server for all incoming connections. You can see a trimmed copy of the code that does this below, with some marks I've added which I will explain. // src/net/http/server.go // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. for { // (1) rw, err := l.Accept() if err != nil { if s.shuttingDown() { return ErrServerClosed } // (2) if ne, ok := err.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } s.logf("http: Accept error: %v; retrying in %v", err, tempDelay) // (3) time.Sleep(tempDelay) continue } return err } connCtx := ctx if cc := s.ConnContext; cc != nil { connCtx = cc(connCtx, rw) if connCtx == nil { panic("ConnContext returned nil") } } tempDelay = 0 c := s.newConn(rw) c.setState(c.rwc, StateNew, runHooks) // before Serve can return // (4) go c.serve(connCtx) } At mark 1 the server calls the Accept() function, this is the exact function that I defined above where I might return a temporary error. At mark 2 it checks if an error was returned, and if so if that error is temporary. If there was a temporary error, at mark 3 it sleeps for an increasing amount of time up-to 1 second, otherwise, at mark 4 it processes the connection on a dedicated goroutine, which allows the server to accept the next connection. I'm not entirely sure why the Go developers added this sleep delay and the change when it was introduced doesn't provide any meaningful insight. Regardless, it caused significant latency connecting to my website when a flood of rejected requests was coming in. It just goes to show how important it is to write meaningful commit messages, because you never know when somebody might come back years later wondering "why was this done?". I sure home I don't come to eat those words later. Coincidentally, you can actually see this happening if you look carefully at one of the metric graphs I shared in my first post about my server's security model: Securing My Web Infrastructure. This is the graph I shared in that blog post and while I didn't know it at the time, the fact that these request spikes all cap-out at around 60 requests per minute was not a coincidence. These requests were not being made with a limit in mind, attackers rarely ever care about things like that, instead it the accidental tarpit I had created. The downside to this was that while the malicious requests were being rate-limited, all requests were being rate-limited, up to a point of taking so long they timed out. The Fix Fixing the issue was relatively straightforward enough. Instead of returning a temporary error to the HTTP server during the accept loop, just don't return anything at all and wait for the next valid connection. func (l *firewallListener) Accept() (net.Conn, error) { for { conn, err := l.l.AcceptTCP() if err != nil { return conn, err } ip := utils.SocketStringToIPAddress(conn.RemoteAddr().String()) if ip == nil { return nil, nil } if IsBlocked(ip, true) { conn.SetLinger(0) conn.Close() continue } return conn, nil } } Now, when the HTTP server calls Accept(), the only time it returns is with a connection from an IP that isn't blocked, or if there genuinely is an error. No more sleep delays, no more excessive timeouts. That Time I Accidentally Made Really Large Headers Back to Top For about 10 years now all major browsers have support for a security feature known as a Content Security Policy or CSP. A CSP is an HTTP header provided by the server that instructs the browser on where it can load assets from, this could be scripts, images, stylesheets, fonts, etc. The objective of using a CSP is to prevent against injected HTML that tries to load assets, such as a malicious Javascript file, from a remote source. With so much user-provided content being available online, it's very possible for this to happen without an attacker compromising the entire web server. CSP protects against that by saying "scripts can only be loaded from these domains". That's a really simplified way of looking at it, anyways. My web server supports injecting the CSP header automatically, but before I go on I need to explain a little bit about the structure of my web server. When an incoming HTTP request is accepted (having passed all firewall checks and assertions), we look at the destination host for the request. This can either be the value of the Host header or as specified during the TLS handshake. We then look at a map of hosts to apps. Apps are just an interface that accept a few methods: type App interface { Cleanup() ReloadConfig() ServeHTTP(rw http.ResponseWriter, r *http.Request) Setup(dataDir string) error Shutdown() } One of the apps is the Proxy app, which is a reverse proxy - it accepts the incoming HTTP request and then proxies it on to another host. This is a very common design, especially with increasingly complex TLS setups. Because each app is unique to a host, and different hosts have different requirements for CSP rules, the proxy app includes a CSP preset that we use to build the header value, or skip it entirely. When the proxy app was going to copy an HTTP request to the downstream host, it would build the CSP header, however there was a slight bug... func (a *App) ServeHTTP(rw http.ResponseWriter, inRequest *ht2.Request) { // --snip -- if a.CSP != nil { a.CSP.ConnectSrc += " " + inRequest.Origin } CopyHttpRequest(inRequest, outRequest, rw, CopyHttpRequestOptions{ Origin: inRequest.Origin, Csp: a.CSP, Cors: a.CORS, AddHeaders: !a.SkipHeaders, UseHTTP3: a.UseHTTP3, InsecureTLS: a.InsecureTLS, }) } I'm really unsure as to what I was doing with the line to append to the ConnectSrc, but the impact is that I'm appending to a variable that lives on the App, rather than a variable that is per-request. This meant that every time there was a request to the app, any request at all, the origin would be appended to the header value. This went on for quite a long time unnoticed and unresolved, largely because I am constantly tweaking and tinkering with my web server, after all, it's how I made having a website fun again. Each time I restarted the server process, the header value would be reset, but only for it to continue to grow and grow. Eventually, after a period of being busy with other matters, the server process stayed running for long enough that the header value grew too large and HTTP clients began to reject it. There is no defined maximum for an HTTP header value, however most HTTP clients use 100KiB, which is perfectly reasonable, and this header value would continue to grow well beyond that. Diagnosing this issue turned out to be difficult as tools like Curl would fail with errors relating to entities being too large, but stopped short of saying what specifically. I eventually used openssl s_client to send an HTTP request by hand and observed my terminal window being filled with a domain name repeated thousands of times. Looking at the commit history, it was really unclear why I added the culprit lines of code. The commit message just says "Improved CSP support". It just goes to show how important it is to write - hey look it's those words I'm now having to eat! The Fix The fix was to just delete those three lines of code. Yup, it really was that simple, and fixing this bug actually made a larger positive impact than I had expected, as it was immediately clear when I fixed the bug by looking at outbound network bytes: So much traffic was being wasted on excessive header sizes. You might look at these mistakes I've made and think "wow, Ian, these are some obvious mistakes, I never would have made them!" to which I say "good for you!" with the utmost sarcasm and disdain. I enjoy making and refining software, and making anything means making mistakes along the way. Each time I make mistakes such as the ones above, I improve my skills of investigation, diagnosing, and repair. Skills that, judging by my peers in the industry, seemingly everyone is quickly willing to throw away because a robot does it "better" than you. Header Image: "Car accident on the Ffestiniog to Bala road. Nobody was hurt" by Geoff Charles, CC BY-SA 4.0, via Wikimedia Commons.