SunOS is a historical UNIX operating system widely used from the mid 80s into the early/mid 90s. Older versions of QEMU struggled to emulate the SPARC platform that SunOS ran on, but QEMU v7.2 supports SPARC well enough to install and run SunOS without any unusual workarounds.
The installation CD-ROM for SunOS 4.1.4 (also branded Solaris 1.1.2) is available on the Internet Archive:
You might also want a dump of the SparcStation 5 boot PROM. QEMU's bundled OpenBIOS is capable of booting SunOS, but the original PROM is useful for people who want a more authentic emulation experience.
shasum -a 256 *
559c8455918029ffdaaf9890caf9f791c3a3604d2f2158793751b770593c0a3c SunOS-v4.1.4.iso
e7f40845504c65f4011278aa3e97a9810aa36775e6c199b715839fbc25eec45e ss5.bin
The first stage of the SunOS installation process is to prepare a minimal bootable environment.
SunOS is designed to run on Sun's hardware, so it's relatively fussy about device layout and configuration compared to an OS intended for consumer hardware. The SPARCstation 5 Service Manual is a useful reference.
Leave off the -bios ss5.bin line to use QEMU's built-in OpenBIOS.
qemu-system-sparc -version
QEMU emulator version 7.2.1
Copyright (c) 2003-2022 Fabrice Bellard and the QEMU Project developers
qemu-img create -f qcow2 sunos-hdd.img 2G
Formatting 'sunos-hdd.img', fmt=qcow2 cluster_size=65536 extended_l2=off compression_type=zlib size=2147483648 lazy_refcounts=off refcount_bits=16
qemu-system-sparc \
-machine SS-5 \
-m 64 \
-bios ss5.bin \
-drive file=sunos-hdd.img,bus=0,unit=3,media=disk \
-device scsi-cd,channel=0,scsi-id=6,id=cdrom,drive=cdrom,physical_block_size=512 \
-drive if=none,file=SunOS-v4.1.4.iso,media=cdrom,id=cdrom
Once at the firmware prompt, type boot cdrom (or boot cdrom:d for OpenBIOS).
In the disk formatter, select disk type 13 (SUN2.1G), write the label to disk, then quit the formatting utility.
The installation script will prep the disk for the main installer, then prompt for a reboot.
If using OpenBIOS, the VM might not boot into mini-root by itself. Type boot disk0:b -sw at the firmware prompt to continue.
After rebooting, you should see some logspam and a root prompt. Run suninstall to continue the installation process.
There's no complicated decisions to make here, so I just went with the quick install of the full system.
After the installation is finished the VM will reboot and you'll be back at the firmware prompt. Type boot disk (or boot disk3:a for OpenBIOS) to boot.
In its original environment, a new SunOS workstation would have received its network configuration from RARP (the predecessor of DHCP) and NIS (sort of a proto-LDAP). Since we don't have a lab of 100 workstations to provision, manual data entry is fine.
The default IP address for QEMU's usermode networking is 10.0.2.15, for which SunOS will assign a netmask of 0xFF000000
A password should be six to eight characters long
🔒.
Almost done. The last step is to configure the gateway router, and then the VM will have working networking. Just log in as root, set the gateway address, and write it to /etc/defaultrouter so it'll persist across reboots.
Log in as a non-root user to launch the native graphical UI of SunOS, OpenWindows.
The final version of SunOS was released when the Web was in its infancy, and therefore does not have a bundled web browser (or any sort of HTTP-related utilities). Luckily for us SunOS/SPARC was a popular platform and Netscape published binaries for it. Actually finding those binaries was a bit of a slog, but I eventually located a copy of Netscape Communicator v4.61 on the delightfully retro page The Solbourne Solace @ Floodgap Retrobits (archive).
In the least surprising twist ever, the tarball itself is only available via Gopher, at gopher://gopher.floodgap.com/9/archive/sunos-4-solbourne-os-mp/communicator-v461-us.sparc-sun-sunos4.1.3_U1.tar.gz. I have mirrored it to archive.org at Netscape Communicator 4.61 [SunOS 4.1.3].
In any case, once you've obtained a copy of the Netscape installation package you'll find that it needs gzip, which at the time was a GNU-specific technology. I recommend following the manual installation instructions from README.install on your host machine to produce a plain tarball.
shasum -a 256 communicator-v461-us.sparc-sun-sunos4.1.3_U1.tar.gz
c667feb3a73721872d60ffd4aab24e39be8d5a48761397b4dd2184b4dd2bb5de communicator-v461-us.sparc-sun-sunos4.1.3_U1.tar.gz
tar -xf communicator-v461-us.sparc-sun-sunos4.1.3_U1.tar.gz
cd communicator-v461.sparc-sun-sunos4.1.3_U1/
mkdir -p netscape-v4.61/java/classes
mv *.nif netscape-v4.61/
mv *.jar netscape-v4.61/java/classes/
cd netscape-v4.61/
gzip -dc netscape-v461.nif | tar -xf -
gzip -dc nethelp-v461.nif | tar -xf -
gzip -dc spellchk-v461.nif | tar -xf -
cd ..
tar -cf ../netscape-v4.61.tar netscape-v4.61/
Getting that tarball into the VM is also a little tricky due to the lack of common network protocols between 1994 and 2023. I ended up writing a helper (recv.c) that will connect to a TCP socket and stream any data it receives to a file.
# host (Linux, BSD, and most others)
nc -Nl 127.0.0.1 5000 < netscape-v4.61.tar
# host (macOS)
nc -l 127.0.0.1 5000 < netscape-v4.61.tar
# VM
cc -o recv recv.c
./recv 10.0.2.2:5000 netscape-v4.61.tar
Unpack that tarball, write a wrapper script and a stub /etc/resolv.conf, and Netscape is ready to go.
cat /etc/resolv.conf
domain sunos.local
nameserver 10.0.2.3
cat ~/netscape.sh
#!/bin/sh
XNLSPATH="${HOME}/netscape-v4.61/nls"
XKEYSYMDB="${HOME}/netscape-v4.61/XKeysymDB"
export XNLSPATH XKEYSYMDB
exec "${HOME}/netscape-v4.61/netscape_dns" "$@"
This should be fairly readable despite being written in K&R C; the BSD sockets API hasn't changed much.
If you don't want to type the whole thing in by hand, see the next section about X11 forwarding.
#include <arpa/inet.h>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
int split_server_address(server_address, server_ip, server_port)
char *server_address;
unsigned long *server_ip;
unsigned short *server_port;
{
char *port_str, *port_extra;
long port_raw;
port_str = strchr(server_address, ':');
if (port_str == NULL) {
return -1;
}
*(port_str++) = 0;
*server_ip = inet_addr(server_address);
if (*server_ip == -1) {
return -1;
}
port_raw = strtol(port_str, &port_extra, 10);
if (port_raw < 1 || port_raw > 65535) {
return -1;
}
if (*port_extra != 0) {
return -1;
}
*server_port = port_raw;
return 0;
}
int recv_file(server_ip, server_port, output_path)
unsigned long server_ip;
unsigned short server_port;
char *output_path;
{
int socket_fd, output_fd;
struct sockaddr_in server;
char buffer[2048];
socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd == -1) {
return -1;
}
memset(&server, 0, sizeof server);
server.sin_family = AF_INET;
server.sin_addr.s_addr = server_ip;
server.sin_port = htons(server_port);
if (connect(socket_fd, (struct sockaddr*)&server, sizeof server) == -1) {
return -1;
}
output_fd = open(output_path, O_WRONLY | O_CREAT, 0600);
if (output_fd == -1) {
return -1;
}
while (1) {
int n = read(socket_fd, buffer, sizeof buffer);
if (n == -1) {
close(output_fd);
return -1;
}
if (n == 0) {
return close(output_fd);
}
write(output_fd, buffer, n);
}
}
int main(argc, argv)
int argc;
char **argv;
{
unsigned long server_ip;
unsigned short server_port;
if (argc < 3) {
fprintf(stderr, "Usage: %s <server_address> <output_path>\n", argv[0]);
return 1;
}
if (split_server_address(argv[1], &server_ip, &server_port) == -1) {
fprintf(stderr, "Invalid server address \"%s\"\n", argv[1]);
return 1;
}
if (recv_file(server_ip, server_port, argv[2]) == -1) {
perror("Error receiving file");
return 1;
}
return 0;
}
The experience of interacting with a GUI from 1994 via QEMU's console is not great, so I recommend running an X11 server on your host and having the VM connect to it.
If you're already running an X11-based desktop (BSD, older Linux, macOS with XQuartz
# host
socat TCP-LISTEN:6001,fork,bind=127.0.0.1 UNIX-CONNECT:/tmp/.X11-unix/X0
# VM
setenv DISPLAY 10.0.2.2:1
xterm
Alternatively, use a nested X11 server such as Xnest or Xephyr. You'll be able to run the OpenWindows window manager, so it feels a bit like using VNC.
# host
Xephyr -ac -listen tcp -screen 2048x1536 :1
# VM
setenv DISPLAY 10.0.2.2:1
olwm
If olwm segfaults on startup, make sure that the host machine has the legacy X11 fonts installed. In Ubuntu 22.04 I had to install the xfonts-100dpi package.
Nowadays the physical block size for CD-ROMs is 2048 bytes, but in the 90s this value wasn't standardized yet. Consumer CD-ROM drives had a physical jumper on the back that could select the block size, and some OSes (including SunOS) would encounter read errors if the jumper wasn't set to what they expected.
SunOS requires a swap partition that is at least as large as machine memory, and the default swap partition size for SUN2.1G is 100 MiB. Using 64 MiB lets us avoid fiddling with the disk geometry in the formatting tool.
SunOS pre-dates CIDR, so it thinks of all 10.x.x.x addresses as belonging to the 10.0.0.0/8 "Class A" network. This is technically wrong for QEMU, which by default uses a netmask of 0xFFFFFF00, but it doesn't really matter as long as you don't try to do anything too complicated with multi-VM networking.
Note that the default socket path for XQuartz may contain a colon, which will make socat unhappy because it uses colons as part of its option syntax. You can work around this with a symlink.