This post’s ID is 6694892671569328580.
I own a few “old” game consoles. There’s demeter, a Sony
Playstation 2 with a busted Graphics Synthesizer (in such a way that she
has graphical output, it’s just completely illegible–what fun!),
merope, a Microsoft Xbox 360 with a broken disk drive, and
daphne, a Nintendo Wii that is actually in great shape,
relatively speaking.
In general, I try to put Unix-like operating systems on as many of my machines as possible:
aphrodite runs Fedora 44hestia and artemis run Rocky Linux 10athena runs macOS Tahoetamamizu runs Dragonfly BSD (will soon be replaced with
Void Linux, though!)nephele runs Alpineand the game consoles are no exception. It’s entirely doable. There was even an official Playstation 2 Linux kit! It costs quite a bit on eBay, but it can still be bought!
I also happen to maintain a highly portable, zero-allocation, zero-dependency library of classical cryptographic primitives, mostly for personal use (I have odd requirements at times). It’s called TinyCrypT. It’s nowhere near the 1.0.0 tag at time of writing (I’m still working on CI integration, soundness proofs, constant-time proofs, and additional test vectors from sources like Wycheproof and CCTV). Nevertheless, it has reached the point where I get to do some very fun things.
You see where I’m going here, right?
This is genuinely as easy as it gets. Most Wiis can be softmodded with an SD card and patience; from there, you literally just flash the officially-supported NetBSD image for the Wii to the card and can boot right from the Homebrew Channel. Recent builds even support the onboard WLAN adapter!
This is harder. There are actually
softmods these days for the 360, but unlike the Wii they’re not
persistent; you have to have the USB stick used inserted every time.
Perfect for us, though, because that same pen drive can be used to hold
the swap and root filesystem! If you’re going to do this, I would highly
recommend following this guide by the
Free60 developers, though I will note that I made some modifications
such as using Dropbear and the Runit init system instead of OpenSSH and
SystemD. Also, for whatever reason, Linux kept mounting the root
filesystem as read-only, so I honestly just passed
init=/bin/sh as a kernel parameter and would run
mount -o remount,rw / followed by
exec /sbin/init. Not a fully unattended boot, but neither
of the other consoles can boot unattended either.
It’s honestly insane that modern mainline Debian can run almost completely unmodified (with a custom kernel) on such an old console.
This was the hardest to get working. I didn’t want to use the actual Linux for Playstation 2 kit, because it’s expensive and highly outdated (I already had enough issues with trying to SSH into the Xbox 360 when I was trying out a port of Ubuntu 10.10; modern OpenSSH really does not like using RSA or DSA). That meant I was voyaging into unknown territory.
Somewhat unknown. See, there’s actually modern work on the
topic, by a man named Fredrik
Noring who maintains a downstream fork of the Linux
kernel designed specifically to run on the MIPS R5900 (also known as the
Emotion Engine: the Playstation 2’s main processor). Full disclosure: I
actually started trying to run Linux on demeter a long time
ago, but ran into a stumbling block: while remarkably mature, Noring’s
fork does not support the Playstation 2 hard disk bay or network
adaptor (yes, it is specifically spelled that way). Moreover, I
wasn’t able to get networking working with a random off-the-shelf
USB-to-Ethernet adapter (at a guess, the driver’s memory requirements
exceeded what the kernel could provide on such limited hardware). This
was all complicated by the fact that I had no actually readable text
output; I was essentially working blind. A familiar feeling to me; this
is what embedded engineering felt like before I discovered the wonderful
world of the hardware debugger.
However, I’m not going to potentially destroy demeter to
try and get UART output.
As such, I went and bought the exact USB adapter mentioned
in the fork’s wiki
page on the topic, and set up the initramfs and kernel according to
the directions on the wiki. This part was remarkably easy for me; “get
Linux running in this weird way on this weird hardware” is a large part
of my day job. I will note that, for whatever reason, I was not able to
get DHCP working; udhcpc simply didn’t work, and I had to
set a static IP (10.42.0.201, please don’t hack me).
Dropbear was finicky, but I got it working eventually.
In the end, demeter actually ran the benchmark fully
from initramfs, as I didn’t feel like setting up a proper root
filesystem (there’s a guide on the Gentoo
wiki for doing so, but it assumes that you’re running Gentoo on the
build machine and I didn’t feel like setting up a Docker container).
Finally, all of the consoles are running Unix-like operating systems. It’s time to run some code.
I will note that all of these benchmarks were performed with SIMD disabled (though the only explicitly vectorized part of TinyCrypT is the ChaCha20 implementation, which also has a scalar counterpart). The IBM Broadway chip used in the Wii has no integer SIMD (though I’m experimenting with running Ascon or ChaCha20 on its Macronix DSP chip, which is admittedly usually used for audio), and while the Emotion Engine and Xenon processors used in the Playstation and Xbox have integer SIMD, emitting it from C code (even using vector extensions) is not directly supported by GCC or LLVM (it’s listed as future work). I’m considering getting my hands dirty in raw assembly, though – stay tuned!
I was not joking when I said TinyCrypT aims to be portable. Although
the 32-bit implementations of a couple primitives had fallen a bit out
of date, I was able to wrangle
them up enough to get them building and running on the target
platforms easily enough. The current build system uses
zig cc as the compiler, which uses Clang/LLVM under the
hood (but thankfully takes care of building Musl and the compiler
runtimes and such for us). Unfortunately, Zig 0.16.0 doesn’t use Clang
24, which is the first version in several years to support the MIPS
R5900; it supports all of the other platforms, though.
I initially experimented with building upstream Clang against
upstream Musl (with this patch
from Noring), but ran into a remarkably stubborn issue: no matter how I
set up all the ABIs against each other, printf did not
work:
~ # ./benchmark-demeter
Took 2143272160.2143272120 seconds to compute 0 16KiB SHA-256 hashes
Took 2143272160.2143272120 seconds to compute 0 16KiB SHA-512 hashes
It appears to be a difference in the expected calling convention, but I genuinely after hours of work could not get it to work. If you have any idea what specifically is broken here, by all means, contact me and let me know. I’m very curious.
I eventually (after 24 [!] rebuilds of Musl) gave up and built
everything with GCC using musl-cross-make,
and it finally worked (after some back-and-forth over floating point
ABIs; I elected to use -msoft-float on everything since the
EE’s floating point ISA is not IEEE-754 compliant).
To bring everything together, I put some elbow grease into optimizing some previously-neglected parts of TinyCrypT for smaller systems, fixing 32-bit Poly1305 and scalar ChaCha20, unrolling loops, and bringing the 32-bit version of Ed25519 out of the stone age. This was also a great opportunity to do constant time proofs using BINSEC; I actually determined that BINSEC/SSE 0.11.2 will assume by default that PowerPC(32/64) is little-endian (while Broadway and Xenon are big-endian), which led to a fun little patch.
Drumroll, please. This is after all the optimization work, the final result:
On the Microsoft Xbox 360 (merope):
juniper@merope:~$ ./benchmark-merope
Took 52.680654068 seconds to compute 65536 16KiB SHA-256 hashes
Took 40.685832029 seconds to compute 65536 16KiB SHA-512 hashes
Took 92.106022690 seconds to compute 65536 16KiB ChaCha20 round-trips
Took 0.248235705 seconds to compute 64 16KiB Ed25519 signatures
On the Nintendo Wii (daphne):
daphne$ ./benchmark-daphne
Took 60.301309811 seconds to compute 65536 16KiB SHA-256 hashes
Took 88.360353267 seconds to compute 65536 16KiB SHA-512 hashes
Took 50.883900329 seconds to compute 65536 16KiB ChaCha20 round-trips
Took 1.001362156 seconds to compute 64 16KiB Ed25519 signatures
And last, but not least, on the Sony Playstation 2
(demeter):
~ # ./benchmark-demeter
Took 187.469833380 seconds to compute 65536 16KiB SHA-256 hashes
Took 981.200900719 seconds to compute 65536 16KiB SHA-512 hashes
Took 161.581046629 seconds to compute 65536 16KiB ChaCha20 round-trips
Took 3.202291443 seconds to compute 64 16KiB Ed25519 signatures
….
Okay, elephant in the room: demeter’s result was kind of
abysmal. See, the main issue (as far as I can tell) is her vanishingly
small caches; SHA-2 depends heavily on a lookup table of constants, not
to mention the fact that the data being hashed is constantly being
brought into and evicted out of the cache.
merope’s performance isn’t really a surprise; she’s the
only 64-bit system, which means she can simply do certain operations
much more efficiently (in addition to her higher clock speed).
Additionally, the current
64-bit implementation of Ed25519 (as of this writing) uses a
precomputed lookup table similar to what is described in the original
Ed25519 paper, which provides a significant speedup at the cost of
~80KiB of RAM. The 32-bit version does not, as it actually (as currently
implemented) is extremely vulnerable to cache timing attacks
(look at the code! it indexes on secrets extensively! the horror!).
That, of course, will change before tag 1.0.0.
daphne was in the middle of the road, but better than
naive implementations. There’s really not much to say. Her port of SHA-2
was able to be proven constant-time using BINSEC/SSE, though, which is
fun (if you’re into that sort of thing).
merope and the
previous tenants at my old house left demeter behind
(presumably because of the damaged GS system); my thanks (to the extent
applicable) goes to them as well.Vi doesn’t actually need my thanks. I gave her my Wii Mini
(hyale) to thank her for giving me the Xbox.
Nevertheless.