2026-09-01 Waking up the Xbox 360 LEGO Dimensions portal: a libusb concurrency bug, not a security gate
A few days ago I wrote about
building a USB man-in-the-middle for a LEGO Dimensions portal,
where the Xbox 360 variant needed a full hardware relay because its security
interface, XSM3, is a real challenge-response protocol whose key material for
this specific accessory was never published. That project answers "what does
a genuine console say to a genuine portal." This post is about a different,
narrower question: can the LEGODimensions
.NET library talk to that same Xbox 360 portal (24C6:FA01) directly, the
same way it already talks to the standard and Xbox One portals? The answer
turned out to hinge on something that had nothing to do with security at all.
The starting point: everything sent, nothing received
The library and its diagnostic companion, XboxPortalProbe,
already knew how to wrap a normal 32-byte LEGO Dimensions frame for this
portal: a 2-byte prefix in front of the first 30 bytes of the frame, sent over
a plain interrupt endpoint, no GIP-style envelope like the Xbox One needs.
Sending a command and watching its effect worked immediately - set a pad to
red, and the pad turned red. But anything that depended on a reply -
the wake acknowledgement, a color readback, the tag list, an unsolicited tag
placement event - never arrived. Not a garbled reply, not a delayed one:
nothing, ever, on the read endpoint.
That's exactly what a one-directional security gate would look like: outbound commands accepted and acted on, inbound data withheld until the console proves itself via XSM3. So that's what we spent a long time chasing.
Down the XSM3 rabbit hole
The Xbox 360's security interface (interface 3, no data endpoints, control
transfers only) implements XSM3 - identity, challenge, status polling, a
phase-one response, a verify packet, and a final acknowledgement, all built on
TripleDES and MACs. We implemented the full exchange in the probe
(Xbox360Xsm3Host.cs) and validated it against a genuine Microsoft Xbox 360
wired controller: full authentication succeeded, response and MAC both
verified. That confirmed the implementation itself was correct - and also
confirmed the portal's own phase-one response couldn't be decrypted with the
same key material, because a licensed accessory like this one uses its own
category-specific root key, which has never been publicly recovered. Physical
chip extraction aside, there was no software path through XSM3 for this
device.
Which made the "reads are gated until auth completes" theory feel settled -
until we found Bart Dopheide's LEGODimensions
project, whose Python client talks to this same portal and never performs
XSM3 at all. Either that client had never actually been run against real
hardware for this code path, or our gate theory was wrong.
Breaking the deadlock with raw libusb calls
To settle it, we stopped going through LibUsbDotNet entirely and wrote a
handful of direct P/Invoke calls straight to libusb-1.0.dll - open, claim
interface 0, write, read, nothing else in between. Sent the wake command,
waited, read the endpoint.
It replied. First try, fully decoded, matching the field layout from Bart's own client byte for byte.
So the portal does reply without authentication. The gate, whatever it was, lived somewhere in our own host-side code, not in the device. A second raw test - this time going through LibUsbDotNet's normal device context, but still doing the actual transfer with the same raw calls - also got a real reply, which ruled out LibUsbDotNet's endpoint wrapper and interface-3 claiming as causes. The difference that finally mattered was structural: our probe's normal code path ran a background thread continuously polling the read endpoint, while commands were written from a different thread. Rebuild that exact shape with the raw calls, and the reply vanishes again, reliably.
The real bug: reads and writes racing on the same handle
libusb_interrupt_transfer is a synchronous convenience function - it submits
a transfer and waits for its own completion internally. Call it concurrently
from two threads on the same device handle - one endlessly polling reads, the
other issuing an occasional write - and, at least on this libusb Windows
backend, the read side can silently lose its completion notification. Writes
barely show the symptom because they complete fast and rarely overlap; a
background read loop is maximally exposed because it's always mid-call,
waiting on a slow, unpredictable device. That asymmetry - writes always
working, reads never working - is exactly what had looked like a
one-directional hardware gate for so long.
Fixing it for good: full-duration mutual exclusion
For the probe, the fix was to delete the background thread and make every
command strictly sequential: write, then read, on one thread, nothing else
touching the handle in between. LEGOPortal couldn't take that shortcut - its
public API depends on a background thread that's always listening, so
LEGOTagEvent fires the moment a tag is placed, independent of whatever the
caller happens to be doing. So instead of removing the concurrency, we made it
safe: a semaphore held for the entire duration of every read and every
write, not just around the libusb call itself.
if (Volatile.Read(ref _xbox360PendingWriters) > 0)
{
Thread.Sleep(5);
}
_xbox360IoGate.Wait(_cancelThread.Token);
try
{
error = _endpointReader.Read(readBuffer, Xbox360ReadTimeout, out bytesRead);
}
finally
{
_xbox360IoGate.Release();
}
A second gotcha: SemaphoreSlim doesn't queue fairly
That alone produced a working but occasionally infuriating portal: colors
sometimes took up to thirty seconds to change. SemaphoreSlim doesn't
guarantee FIFO ordering, and the read loop's own release-then-immediately-
reacquire cycle could keep winning that race against a write's pending
Wait() call for many consecutive iterations. The fix was a plain interlocked
counter the write side increments before waiting on the gate, which the read
loop checks first - if a write is pending, back off for a few milliseconds
instead of racing straight back in:
Interlocked.Increment(ref _xbox360PendingWriters);
try
{
_xbox360IoGate.Wait();
try
{
error = _endpointWriter.Write(bytes, ReadWriteTimeout, out bytesWritten);
}
finally
{
_xbox360IoGate.Release();
}
}
finally
{
Interlocked.Decrement(ref _xbox360PendingWriters);
}
A third gotcha: one tracked command at a time
Even with reads and writes safely serialized, ReadTag() would work the first
time and then silently fail on the next page. The remaining culprit: this
portal apparently can't reliably multiplex two in-flight "Read" commands, even
with distinct message IDs, if a second one is sent before the first's reply
arrives. LEGOPortal already has an internal auto-read that fires whenever a
tag is placed; if that raced against an explicit ReadTag() call from the
caller's own thread, one of the two replies would get corrupted or dropped.
The fix serializes every tracked request/reply cycle behind its own lock, and
the internal auto-read simply skips itself for that one event if the lock
isn't immediately free, rather than blocking and risking a deadlock against
the very call it exists to support.
Thanks, Bart
None of this would have converged nearly as fast without
Bart Dopheide's
LEGODimensions project. His
reverse engineering corrected a wire-format detail
we'd gotten wrong from an earlier capture (the frame prefix is 0B 16, not
0B 14), and his Python client was the reference that let us confirm details
like the wake command's
hardcoded message ID and the Windows-specific skip of the USB configuration
step. Real, working, previously-published client code for hardware this
under-documented is worth a lot - thank you, Bart.
Where it landed
LEGOPortal now detects vendor/product ID 24C6:FA01 and exposes the exact
same public API for it as for a standard or Xbox One portal - wake, colors,
tag events, tag reads, all of it, confirmed against real hardware. The full
protocol writeup, including the frame format and every concurrency
requirement above, lives in
Xbox360PortalProtocol.md.
The XSM3 side of this investigation isn't wasted, either: it lives on as a
diagnostic path in XboxPortalProbe, and it's the same protocol the
MITM bridge project has to relay
verbatim to a real portal, since - unlike this library - it can't just decide
authentication isn't required. And wait, I just received the Xbox 360 game.
So, game on for some MITM play to see if we can discover this key! XSM3 is
fascinating!