---
title: "Dynoxide 0.11.3: closing a Windows port hijack - Martin Hicks"
description: "Dynoxide 0.11.3 sets SO_EXCLUSIVEADDRUSE on the DynamoDB and MCP listeners on Windows, closing a same-user port hijack that an investigation into TIME_WAIT parity wasn't looking for, and runs the unit tests on Windows in CI for the first time."
canonical: https://martinhicks.dev/articles/dynoxide-0113-windows-listener-hardening
last-updated: 2026-07-05
---

![The Dynoxide hexagonal logo centred on a near-black background](https://martinhicks.dev/images/articles/dynoxide-0113-windows-listener-hardening.png)5th July 2026

# Dynoxide 0.11.3: closing a Windows port hijack

[Dynoxide](https://dynoxide.dev) 0.11.3 is a small one, and for once there isn't a conformance fix in it. It's about who is allowed to bind the ports.

An issue had been sitting on the tracker for a while: investigate `SO_EXCLUSIVEADDRUSE` on Windows, for parity with the `TIME_WAIT` bypass dynoxide already had on Unix. I finally picked it up this week, and the investigation came back inside out. The parity already existed. The actual gap was something else.

## The wrong assumption

On Unix, dynoxide sets `SO_REUSEADDR` on its listener so a restart isn't blocked by the previous run's `TIME_WAIT` sockets. On Windows it set nothing, because `SO_REUSEADDR` means something nastier there - it lets a second socket bind straight over a live one. Setting nothing felt like the safe default.

It isn't. Microsoft's bind tables show that a Windows socket bound with no options can be taken over by another process running as the same user, if that process asks for `SO_REUSEADDR`. Meanwhile the `TIME_WAIT` rebind I assumed Windows needed an option for works out of the box - a plain bind sails past sockets that would block a Unix bind entirely.

So the option the issue pointed at was the right one, for the opposite reason. `SO_EXCLUSIVEADDRUSE` makes a takeover bind fail instead of succeed, and Microsoft's guidance is blunt about it: every server should set it. 0.11.3 sets it on both listeners - the DynamoDB HTTP port and the MCP transport.

On severity: this needs an attacker already running as your user, and a process running as you can read the MCP token file off disk anyway. It's hardening for a local dev tool, not a drop-everything advisory. But the MCP port carries bearer tokens, so it shouldn't be the easy target either.

## The tests had never run on Windows

There was a catch to check before shipping. MSDN warns that an exclusively bound port can refuse to rebind while connections from the previous instance are still settling. For a dev server that gets restarted constantly, trading a hijack hole for broken restarts would be a bad deal.

I couldn't answer that from a Mac, and it turned out CI couldn't either. `cargo test` had only ever run on Ubuntu; the Windows job built binaries and never executed a single test. So this release also adds a Windows test job, plus a regression test that walks a real connection into `TIME_WAIT` and immediately rebinds the port. It passes on Windows - restarts survive the new option.

The first ever Windows test run also failed two existing tests, both quiet Unix assumptions: one used a directory as a stand-in for an unreadable file, the other asserted a forward-slash path substring. Both now test the same intent portably.

0.11.3 is out everywhere dynoxide ships:

-   **npm:** `npm install --save-dev dynoxide`, then `npx dynoxide`
-   **Homebrew:** `brew install nubo-db/tap/dynoxide`
-   **Cargo:** `cargo install dynoxide-rs` ([crates.io](https://crates.io/crates/dynoxide-rs), [docs.rs](https://docs.rs/dynoxide-rs))
-   **Docker:** `ghcr.io/nubo-db/dynoxide`, mirrored best-effort to Docker Hub (`nubodb/dynoxide`) and ECR Public
-   **GitHub:** [source and pre-built binaries](https://github.com/nubo-db/dynoxide/releases), plus the `nubo-db/dynoxide/action` GitHub Action for CI

If you're on Windows and a restart ever refuses the port, that's a bug worth filing - there's now a test claiming it can't happen, and I'd like to keep that test honest.

## Further reading

-    [Dynoxide 0.11.2: a stack of conformance fixes
    
    Dynoxide 0.11.2 makes the engine report the transaction capacity numbers I pinned against real DynamoDB last week - the read/write split, a ConditionCheck billed as a write, an idempotent replay that recomputes a real read - plus an UpdateTable bug that dropped table keys, and a batch of projection and validation fixes.](https://martinhicks.dev/articles/dynoxide-0112-conformance-fixes)
-    [Dynoxide 0.9.8: fixing the orphan problem
    
    Dynoxide's npm wrapper was leaving orphaned processes when backgrounded in npm scripts. Three fixes in 0.9.8 sort it out - SIGTERM handling in Rust, async spawn with signal forwarding, and PPID polling for the backgrounded case.](https://martinhicks.dev/articles/dynoxide-098-npm-process-lifecycle)
-    [Introducing Dynoxide: a fast, embeddable DynamoDB engine
    
    Dynoxide is a DynamoDB-compatible engine in Rust, backed by SQLite, that starts in milliseconds. I built it to embed in a native app, and it turned into something bigger.](https://martinhicks.dev/articles/introducing-dynoxide)

---

Source: https://martinhicks.dev/articles/dynoxide-0113-windows-listener-hardening
