Running SunOS 4 in QEMU (SPARC)

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.

Installation media

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

Preparing the SunOS mini-root

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.

  • The internal HDD must have SCSI target 3, and the internal CD-ROM must have SCSI target 6.
  • SunOS expects the CD-ROM to have a physical block size of 512 bytes[1].
  • Although a real SPARCstation 5 supports up to 256 MiB of RAM, we'll be giving it only 64 MiB to simplify the installation process[2]

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.

Installing SunOS itself

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[3].

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.

Installing a web browser (Netscape)

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" "$@"

Appendix A: recv.c

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;
}

Appendix B: X11 forwarding

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[4]) then you can proxy its socket directly to TCP and then connect to it from the VM. This will let you copy-paste big blobs of text such as recv.c.

# # 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.


  1. 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.

  2. 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.

  3. 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.

  4. 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.

Change Feed