<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>TinyComputers.io (Posts about msi-x)</title><link>https://tinycomputers.io/</link><description></description><atom:link href="https://tinycomputers.io/categories/msi-x.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Contents © 2026 A.C. Jokela 
&lt;!-- div style="width: 100%" --&gt;
&lt;a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/"&gt;&lt;img alt="" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/80x15.png" /&gt; Creative Commons Attribution-ShareAlike&lt;/a&gt;&amp;nbsp;|&amp;nbsp;
&lt;!-- /div --&gt;
</copyright><lastBuildDate>Mon, 22 Jun 2026 12:54:57 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>The Doorbell That Killed the Device: Writing OpenBSD's Missing ena(4) Driver</title><link>https://tinycomputers.io/posts/the-doorbell-that-killed-the-device-an-ena-driver-for-openbsd-on-graviton.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/the-doorbell-that-killed-the-device-an-ena-driver-for-openbsd-on-graviton_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;33 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;The Doorbell That Killed the Device: Writing OpenBSD's Missing ena(4) Driver&lt;/h2&gt;
&lt;p&gt;Earlier last week, I wrote about a problem with a clean, almost funny shape: OpenBSD's arm64 kernel has no driver for the network card AWS puts in every EC2 instance, so an OpenBSD/arm64 instance boots into a billable void it can never reach. The fix in that post was to stop asking — to run OpenBSD as a KVM guest inside QEMU on a bare-metal Graviton host, hand it virtio devices it already understands, and let a Linux host own the real Amazon adapter. Put a translator on the seam, and two systems that disagree about what a network card is can both be right.&lt;/p&gt;
&lt;p&gt;That solved the build-server problem. It did not solve the itch.&lt;/p&gt;
&lt;p&gt;Because the actual missing piece is small and specific and writable: the driver. Amazon's adapter is called the Elastic Network Adapter — &lt;code&gt;ena&lt;/code&gt; — and it's a documented device with a permissively licensed reference implementation. FreeBSD has an &lt;code&gt;ena&lt;/code&gt; driver. NetBSD has one. The protocol is published. OpenBSD just doesn't have the code, because effectively nobody runs OpenBSD as a first-class EC2 guest and so nobody wrote it. "Nobody wrote it" is not the same as "can't be written." So I decided to write it.&lt;/p&gt;
&lt;p&gt;This is the story of that — and, more than the previous post, it's a story about a specific kind of failure. The driver attached on the first serious try. Then the device started killing itself, silently, a few microseconds after every bring-up, and I spent the better part of two days proposing increasingly clever reasons why. The real reason turned out to be embarrassingly dull. And because I want this to be useful and not just triumphant, the last third of the post is about the word "working": what I can actually claim, and the uncomfortable distance between that and "done."&lt;/p&gt;
&lt;h3&gt;What an ENA driver has to do&lt;/h3&gt;
&lt;p&gt;A modern NIC isn't a thing you poke registers at to send a packet. It's a small message-passing computer that shares host memory with you: rings of descriptors in DMA memory, doorbell registers to announce new work, an interrupt path for completions. ENA has three kinds of queue, and all three matter to this story.&lt;/p&gt;
&lt;p&gt;The admin queue is how the host configures the device — a submission ring and a completion ring in shared memory. Write a command (read attributes, set a feature, create an IO queue), ring the admin doorbell, wait for a completion. OpenBSD's driver polls for that completion rather than taking an interrupt, which keeps bring-up simple.&lt;/p&gt;
&lt;p&gt;The AENQ — Asynchronous Event Notification Queue — is the device's back-channel: it announces link up/down and, critically, posts a keep-alive event about once a second. The keep-alive is a heartbeat; the host is expected to drain these and, by draining them, prove it's still paying attention.&lt;/p&gt;
&lt;p&gt;The IO queues are the actual network — a submission ring you fill with buffers, a completion ring the device writes back. You create them with admin commands (&lt;code&gt;CREATE_CQ&lt;/code&gt;, then &lt;code&gt;CREATE_SQ&lt;/code&gt;), and once they exist and the link is up, you can move traffic.&lt;/p&gt;
&lt;p&gt;Get all three right, in the right order, and the card works. Get the order subtly wrong and — as I'd learn — the card decides you're not a real driver and quietly bricks itself.&lt;/p&gt;
&lt;p&gt;The reference is Amazon's &lt;code&gt;ena-com&lt;/code&gt;, a hardware-abstraction layer shared across the Linux and FreeBSD drivers. Its BSD-licensed parts — &lt;code&gt;ena-com&lt;/code&gt; itself and FreeBSD's driver on top — are fair to read and port; the Linux driver is GPL, kept strictly read-only, a thing to consult for intent and never to copy. Writing the OpenBSD version means rewriting all of it in OpenBSD's idiom anyway — &lt;code&gt;bus_dma(9)&lt;/code&gt; for the rings, &lt;code&gt;pci(9)&lt;/code&gt; for attachment, &lt;code&gt;ifnet&lt;/code&gt;/&lt;code&gt;ifq&lt;/code&gt; for the stack. The protocol is the spec; the code is yours.&lt;/p&gt;
&lt;h3&gt;Phase zero: it attaches&lt;/h3&gt;
&lt;p&gt;The first milestone was just attachment, and it went well enough that I'd half-convinced myself the hard work was behind me. The driver resets the device, sets up the admin submission and completion rings, and issues commands. &lt;code&gt;GET_FEATURE(DEVICE_ATTRIBUTES)&lt;/code&gt; comes back with the real MAC and maximum MTU — proof the admin queue works end to end, DMA is coherent, the device is listening. The console prints the line I'd been chasing: &lt;code&gt;ena0 ... ENA ver 0.10 ... address 12:xx:xx:xx:xx:xx&lt;/code&gt;, on a real Graviton instance. The card was talking.&lt;/p&gt;
&lt;p&gt;And then, every single time, a few seconds later, it stopped talking. The first attempt to create an IO queue — &lt;code&gt;CREATE_CQ&lt;/code&gt;, the command that begins turning a configured device into a working network interface — would sit there and time out. No completion. No error. The admin queue that had just answered four commands flawlessly had gone silent.&lt;/p&gt;
&lt;p&gt;When I finally added code to read the device's status register at each step, the shape of it came into focus and got worse. &lt;code&gt;DEV_STS&lt;/code&gt; reads &lt;code&gt;0x1&lt;/code&gt; — ready — through reset, through the admin handshake, through reading device attributes. Then, somewhere shortly after, it reads &lt;code&gt;0x21&lt;/code&gt;. Bit five is set. &lt;code&gt;FATAL_ERROR&lt;/code&gt;. The device had, of its own accord, entered a fault state and was now refusing all further work. That's why &lt;code&gt;CREATE_CQ&lt;/code&gt; vanished: you can't drive a device that's already decided it's dead.&lt;/p&gt;
&lt;p&gt;A healthy card, healthy through every step I could see, that turns to stone the moment I look away. That's the wall.&lt;/p&gt;
&lt;h3&gt;Five wrong theories&lt;/h3&gt;
&lt;p&gt;Here is the part I'm telling on myself, because it's the honest center of the whole thing.&lt;/p&gt;
&lt;p&gt;When you don't know why a device faults, the device gives you almost nothing — a single bit that says "something is wrong" and not a syllable about what. So you reason from the reference code: what does the working driver do that mine doesn't? And the trouble with that question, on hardware this unfamiliar, is that it has too many plausible answers. Every difference between my driver and &lt;code&gt;ena-com&lt;/code&gt; looks like it could be the one that matters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Theory one: the keep-alive watchdog.&lt;/strong&gt; The device sends heartbeats; the host must drain them; if the host stops, surely the device fences off the unresponsive driver and faults. My driver had no periodic task draining the AENQ — it relied on a single interrupt that, I could see, fired exactly once and went quiet. This was a beautiful theory. It explained the timing. It matched a real mechanism FreeBSD implements. I built a proper one-second timer to drain the queue, mirroring FreeBSD's &lt;code&gt;ena_timer_service&lt;/code&gt;, complete with a mutex to keep the timer and the interrupt from racing on a multi-core guest. It was clean, correct code. It fixed nothing. The device faulted on exactly the same schedule, and the diagnostic I'd added showed the AENQ had processed zero events — there was nothing to drain. I had carefully solved a problem that wasn't happening.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Theory two: the MMIO response region.&lt;/strong&gt; ENA has a "readless" register mode backed by a small DMA region; maybe the device faulted without it. I added it. It didn't help — and worse, I'd added it before I started reading the status register, so for an embarrassingly long time I was carrying an unvalidated change that could have been the cause itself. (It wasn't.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Theory three: host attributes.&lt;/strong&gt; Both reference drivers register a 4 KB "host info" page right after admin init — &lt;code&gt;SET_FEATURE(HOST_ATTR_CONFIG)&lt;/code&gt;, a "yes, a real driver lives here" handshake. My driver skipped it. This had to be what the device validated before deciding I was legitimate. I implemented it properly. The status register read &lt;code&gt;0x1&lt;/code&gt; right after it succeeded — and faulted anyway, at the same point as always.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Theory four: a stale completion.&lt;/strong&gt; Maybe the admin queue was reading the wrong completion slot, so the feature data showing the device supporting zero AENQ event groups — a suspicious value — was garbage from an uninitialized ring. I instrumented the completion path down to the command IDs and phase bits. It was reading the right slot. The suspicious value was real.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Theory five: interrupt ordering.&lt;/strong&gt; I'd unmasked the device's interrupt after a particular doorbell write; the reference does it before. I swapped the order. The device faulted one line later than before, which felt like progress and was not.&lt;/p&gt;
&lt;p&gt;Somewhere in the middle I did what I increasingly do with a problem that has too many branches: I handed it to a fleet of AI agents, one per hypothesis, reading the reference trees in parallel, and had them synthesize a ranked root cause. The synthesis came back confident, specific, and wrong. What saved it was the same harness's adversarial step — three more agents told to refute the conclusion, and all three did, pointing out the timing didn't fit and the real anomaly was being hand-waved. The machine talked itself out of its own clever answer. That's the part worth remembering: not "the AI solved it," but "the AI proposed something plausible and the only thing that caught it was forcing a second pass that tried to tear it down."&lt;/p&gt;
&lt;p&gt;What none of the five theories were was boring enough.&lt;/p&gt;
&lt;h3&gt;The doorbell&lt;/h3&gt;
&lt;p&gt;I gave up on theories and did the dumb, mechanical thing I should have done first. I made the driver poll the status register in a tight loop after every single register write in the bring-up, printing the exact moment the fault bit flipped. Not "is it healthy at the end" — which write kills it.&lt;/p&gt;
&lt;p&gt;The answer came back in one line, and it was not ambiguous. Healthy after writing the queue's base address. Healthy after writing its size. Healthy after the feature commands. Then:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;STS&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;FATAL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;0x21&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;AENQ_HEAD_DB&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The fault appears about 150 microseconds after one specific write: the AENQ head doorbell — the register that tells the device "I've made the event ring's slots available; the queue is live." Not the interrupt unmask, which I'd swapped order on. Not any feature command. The doorbell that activates the event queue. Ring it, and the device dies.&lt;/p&gt;
&lt;p&gt;The doorbell value was correct — I'd checked it against the reference a dozen times. The ring's address was correct. Its size was correct. So why would activating a correctly-configured queue kill a healthy device?&lt;/p&gt;
&lt;p&gt;Because the queue wasn't really configured. It only looked like it was.&lt;/p&gt;
&lt;h3&gt;The most boring possible cause&lt;/h3&gt;
&lt;p&gt;I lined my driver's bring-up sequence up against &lt;code&gt;ena-com&lt;/code&gt;'s, write for write, and the difference was finally visible because I now knew exactly which write to care about.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ena-com&lt;/code&gt; registers the AENQ — writes its base address and size registers — inside &lt;code&gt;ena_com_admin_init&lt;/code&gt;, in the same breath as the admin submission and completion queues, before the device is ever told initialization is finished. All three rings get registered together, as one atomic-feeling handshake, while the device is still in its "setting up" phase.&lt;/p&gt;
&lt;p&gt;My driver registered the admin queues during admin init, exactly like the reference. But it registered the AENQ much later, in a separate function that ran after host attributes, after reading device attributes, after feature negotiation — long after the admin handshake had closed and the device considered itself up and running.&lt;/p&gt;
&lt;p&gt;And here's the thing: writing those AENQ registers late worked, in the sense that the device accepted the writes and stayed healthy. The register values landed. The status bit stayed green. Everything looked fine. The device had quietly noted the address and size of a ring it had never actually wired into its event subsystem, because that wiring only happens during the init handshake I'd already finished. The AENQ was a ghost: registered on paper, uninitialized in the device's mind.&lt;/p&gt;
&lt;p&gt;Then I rang the doorbell. "The event queue is live; start using it." The device went to use a subsystem that was never set up, and faulted. A hundred and fifty microseconds later, the bit flipped.&lt;/p&gt;
&lt;p&gt;The fix is four lines moved earlier. I pulled the AENQ registration out of its late function and into admin init, immediately after the admin queues, exactly where &lt;code&gt;ena-com&lt;/code&gt; does it. The later function kept only the parts that genuinely belong late — subscribing to event groups, and the final doorbell-and-unmask that says "go."&lt;/p&gt;
&lt;p&gt;I rebuilt, booted, and watched the status register stay &lt;code&gt;0x1&lt;/code&gt; straight through the doorbell. &lt;code&gt;CREATE_CQ&lt;/code&gt; succeeded. &lt;code&gt;CREATE_SQ&lt;/code&gt; succeeded. The link came up. The driver enqueued a hand-built DHCP DISCOVER, the device transmitted it, and a 590-byte IPv4 packet — the DHCP OFFER, a real reply from AWS's network — came back up the receive ring. Transmit and receive, on real silicon, for the first time.&lt;/p&gt;
&lt;p&gt;There was one more gift in the logs, the kind that tells you a fix is right and not just lucky. Remember theory four — the device reporting zero supported AENQ event groups, the value I'd half-dismissed as a possible misread? With the AENQ now registered during init, that same query on the same hardware came back reporting all the groups supported. The zero had never been a VF limitation or a misread. It was the device telling me, in the one channel it had, that its event subsystem wasn't initialized — because I hadn't initialized it yet. One root cause had been wearing five costumes. The watchdog that had nothing to drain, the missing handshake, the suspicious zero, the doorbell fault — all of it was the single fact that I'd set up a queue in the wrong order, refracted through a device that can only ever tell you "something is wrong."&lt;/p&gt;
&lt;p&gt;This is the same lesson the last post ended on, and I clearly didn't learn it hard enough the first time. Out here, far off the beaten path, every failure presents as exotic, because the strange explanation announces itself and the boring one doesn't. An out-of-disk error wears the costume of a dependency that won't compile. A queue initialized in the wrong order wears the costume of a keep-alive watchdog, a missing security handshake, and a device that lies about its capabilities. The further out you are, the more deliberately you have to rule out the dull thing first — and "I did the steps in the wrong order" is about as dull as it gets.&lt;/p&gt;
&lt;h3&gt;The loop that made it bearable&lt;/h3&gt;
&lt;p&gt;I have to mention the iteration speed, because for most of this saga it was the actual bottleneck, and fixing it is what turned a slog into something tractable.&lt;/p&gt;
&lt;p&gt;The previous post's build server bakes a disk image, snapshots it, registers an AMI, boots a real EC2 instance, and reads the serial console — about eighteen minutes a turn. That's fine for building a binary. It is agony for debugging a driver, where you want to change one line and see what the hardware does. Eighteen minutes times the number of wrong theories above is a number I'd rather not compute.&lt;/p&gt;
&lt;p&gt;So I built a faster loop, and it leans on the same bare-metal host the build server already needs. That &lt;code&gt;a1.metal&lt;/code&gt; host has a real ARM SMMU — the IOMMU that makes device passthrough safe — and Linux's VFIO framework can hand a physical PCI device straight to a QEMU guest. So I attached a second network interface to the metal instance, bound it to &lt;code&gt;vfio-pci&lt;/code&gt; on the Linux host (leaving the primary NIC alone, so I didn't saw off the SSH branch I was sitting on), and passed it through to the OpenBSD guest. Now the OpenBSD VM sees a real ENA device — actual Amazon silicon, vendor &lt;code&gt;1d0f&lt;/code&gt;, product &lt;code&gt;ec20&lt;/code&gt; — on its virtual PCI bus, and my driver attaches to that. No AMI bake. Build the kernel, reboot the guest, watch &lt;code&gt;ena0&lt;/code&gt; come up against real hardware. Two minutes a turn instead of eighteen.&lt;/p&gt;
&lt;p&gt;There was one gotcha worth writing down, because the error is opaque: VFIO refused with &lt;code&gt;failed to set iommu for container: Operation not permitted&lt;/code&gt;. ARM's SMMU here can't remap interrupts, so the kernel blocks passthrough by default — fixed with the module parameter &lt;code&gt;allow_unsafe_interrupts=1&lt;/code&gt;, entirely fine for a trusted device on a machine I own by the hour.&lt;/p&gt;
&lt;p&gt;That loop is also the honest reason the fix arrived when it did. The "poll after every write" instrumentation only became practical once I could run it and read it in two minutes. The clever theories flourished in the eighteen-minute dark; the boring method won the moment I could see in real time.&lt;/p&gt;
&lt;h3&gt;What "working" means&lt;/h3&gt;
&lt;p&gt;Here is where I have to be careful, because "I wrote a working ENA driver for OpenBSD" is a sentence that can mean five very different things, and only one of them is true today.&lt;/p&gt;
&lt;p&gt;What is true: on a real EC2 Graviton2 instance, an OpenBSD/arm64 kernel with my driver attaches to the real ENA adapter, completes the full bring-up — admin queue, host attributes, device attributes, AENQ, IO queue creation — brings the link up, transmits a packet the device really puts on the wire, and receives the reply. The send and receive paths through the driver are the real ones: the transmit path is the same &lt;code&gt;ifq&lt;/code&gt; enqueue the network stack uses, and the receive path is the same completion handler that would feed packets up to IP. A DHCP DISCOVER went out and an OFFER came back. The device protocol — the genuinely hard, genuinely undocumented-in-OpenBSD-idiom part — works, and it works on the production hardware, not just the passthrough rig.&lt;/p&gt;
&lt;div style="text-align: center; margin: 30px 0;"&gt;
&lt;img src="https://tinycomputers.io/images/ena_4_first_boot_success.png" alt="OpenBSD/arm64 console output on a Graviton2 EC2 instance showing the ena(4) driver successfully attaching to the real ENA adapter, completing device bring-up, and exchanging a DHCP DISCOVER and OFFER over the wire" style="max-width: 100%; border: 1px solid #ddd; border-radius: 8px;"&gt;
&lt;p style="color: #666; font-size: 12px; margin-top: 10px;"&gt;First boot. OpenBSD/arm64 on a real Graviton2 EC2 instance, ena(4) attached, link up, DHCP round-trip completed against the real device.&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;What is not yet true, and I want to be exact about each one:&lt;/p&gt;
&lt;p&gt;I drove that DHCP exchange from a kernel thread, not from userland. The test harness builds a DISCOVER packet in the kernel, hands it to the transmit ring, and watches the rings directly, because the minimal RAM-disk environment I'm booting doesn't have room for a real &lt;code&gt;dhclient&lt;/code&gt;. The packets and the ring mechanics are real; the thing wrapping them is a debug scaffold, not &lt;code&gt;ifconfig ena0 up; dhclient ena0; ping&lt;/code&gt;. I have not yet typed those three commands at a shell and watched them work. That's the next milestone, and until I've done it I won't claim the interface works "from userland," only that the driver's data paths do.&lt;/p&gt;
&lt;p&gt;I am booting the OpenBSD install ramdisk, &lt;code&gt;bsd.rd&lt;/code&gt;, which runs entirely from RAM. I have not done a full disk install and booted a persistent OpenBSD that comes up multiuser with &lt;code&gt;ena0&lt;/code&gt; as its only network interface and lets me SSH in over it. That — a normal OpenBSD instance you log into over the network it sees natively — is the milestone that would let me retire the QEMU-shim build server from the last post. I'm not there. I've proven the hard part is possible; I haven't assembled it into a system you'd actually run.&lt;/p&gt;
&lt;p&gt;The driver is full of scaffolding. The status-register polling, the packet-injection thread, a dozen diagnostic &lt;code&gt;printf&lt;/code&gt;s, the keep-alive timer I built for a problem that didn't exist — all still in the tree, behind a debug flag. None of it belongs in code anyone else should read. Before this is a contribution rather than a demo, that all comes out, the real fixes get separated from the detritus, and the whole thing gets the kind of review OpenBSD's tree rightly demands. It has had none of that. No OpenBSD developer has looked at a line of it. It is not submitted, not reviewed, and would not survive &lt;code&gt;tech@&lt;/code&gt; in its current state, nor should it. The work-in-progress tree, scaffolding and all, lives at &lt;a href="https://baud.rs/yJXRRr"&gt;github.com/ajokela/openbsd-ena&lt;/a&gt; — open for reading, not for trusting.&lt;/p&gt;
&lt;p&gt;And the testing is thin. One DHCP round-trip is not throughput, not stability under load, not days of uptime, not the dozen edge cases — checksum offload, multi-queue, MTU changes, link flaps — a NIC driver has to handle before anyone trusts it. I've shown the path is real. I have not shown it's robust.&lt;/p&gt;
&lt;p&gt;So: working in the sense that the central, doubted, genuinely difficult thing — does the device protocol function, correctly implemented from scratch, on real hardware — is now answered yes. Not working in the sense of something you'd deploy, or even the sense of something you'd &lt;code&gt;ifconfig&lt;/code&gt; by hand yet. Both halves of that sentence are true and I don't want the exciting half to eat the honest one.&lt;/p&gt;
&lt;h3&gt;What it was about, again&lt;/h3&gt;
&lt;p&gt;The last post put a translator between two systems that disagreed, so each could be right without meeting. This one is the opposite move: no shim, just teaching one system to speak the other's language directly — a driver doing the actual work of turning OpenBSD's idea of a network interface into ENA's, register by register and ring by ring.&lt;/p&gt;
&lt;p&gt;But the deeper rhyme isn't the architecture, it's the failure. Both times the headline problem had a one-sentence answer — "run it as a guest," "register the queue during init" — and both times that sentence was the easy part, with the real work in a gap where everything looked exotic and the truth was mundane. And both times the trap was the same: a plausible, faintly flattering, wrong explanation is far more available than the boring one underneath it — especially with a tireless machine happy to generate plausible explanations on demand. The machine is genuinely useful; it read three reference drivers in parallel and caught its own bad guess on the second pass. But it has no instinct for "you probably just did the steps out of order," because it has never spent an afternoon being humiliated by exactly that.&lt;/p&gt;
&lt;p&gt;What I have now is a driver that makes OpenBSD see the network on a cloud that, a month ago, OpenBSD couldn't see at all. It is not done. It is, for the first time, possible — proven on the hardware, by the most boring fix in the file. The doorbell rings, and the device lives.&lt;/p&gt;</description><category>aarch64</category><category>arm64</category><category>aws</category><category>bsd</category><category>device driver</category><category>dma</category><category>ec2</category><category>ena</category><category>ena-com</category><category>graviton</category><category>iommu</category><category>kernel</category><category>msi-x</category><category>networking</category><category>nic</category><category>openbsd</category><category>pci</category><category>smmu</category><category>vfio</category><guid>https://tinycomputers.io/posts/the-doorbell-that-killed-the-device-an-ena-driver-for-openbsd-on-graviton.html</guid><pubDate>Mon, 22 Jun 2026 01:30:00 GMT</pubDate></item></channel></rss>