How Oracle saved rpm on WSL 1

Oracle Linux has been published on the Microsoft Store for WSL. Oracle Linux is compiled from the sources of Red Hat Enterprise Linux (RHEL). On WSL 2 you are going to get the Microsoft WSL 2 kernel by default. But notably Oracle Linux for bare metal and VMs is available with a choice of two Linux kernels, one that is guaranteed to be compatible with RHEL and one that is Oracle's custom kernel, called the Unbreakable Enterprise Kernel (UEK). Oracle Linux competes in the enterprise Linux space with RHEL and SUSE Enterprise Linux (SLES).

Unlike RHEL and SLES, Oracle Linux and it's package repositories are completely free, no subscription is required. Oracle even publishes a handy script to switch from CentOS directly to Oracle Linux. This makes it an option for administrators running CentOS who want to remain downstream of RHEL and not go to CentOS Stream:

Oracle Linux can then be managed alongside RHEL, Ubuntu, and SLES with SUSE Manager.

Oracle Linux officially landing on WSL reminds me of a story about Oracle and WSL from 2019 I wanted to share.

rpm, the Red Hat package manager, tool was very flaky on WSL 1. It saw frequent rpmdb corruption and would seg fault regularly, with issues reported back to early 2018.

This issue affected Red Hat Enterprise Linux and all related distributions, including CentOS, Oracle Linux, Scientific Linux, and to a lesser extent Fedora. It occurred on Windows builds 17134 (1803) through 18383 (1909).

It, ironically, did not affect openSUSE or SUSE Enterprise Linux because even though zypper is based on rpm, SUSE had a patched rpm, including the part that triggered this bug on WSL 1.

At Whitewater Foundry, we wanted to ship Fedora Remix for WSL and Pengwin Enterprise, but this rpm bug held back our progress. Pengwin Enterprise shipped with Scientific Linux in the Microsoft Store but could be custom built with RHEL for enterprise customers with valid RHEL subscriptions.

I reached out to Red Hat, who we were partners with, for assistance in isolating the rpm bug on WSL 1. We had narrowed the bug down to the database function in rpm, related to a bad mmap function.

Unfortunately our relationship with Red Hat at the time was...complicated.

While the Red Hat partnership and sales teams were excited by our partnership, prominent figures in engineering were not happy at all. Unfortunately this meant Red Hat engineering would not help us with the rpm bug, even as mutual customers asked for help.

While Red Hat still has not shipped Red Hat Enterprise Linux for WSL, they have since indicated they plan to ship a Podman app for Windows based on WSL. That is progress.

Eventually I reached out to Oracle, as a long shot, for assistance. Much to my surprise, Oracle was incredibly responsive to my inquiry. I detailed the issue and our progress on the bug.

Oracle assigned an experienced engineer to the project who replied with sample C code that reproduced the WSL 1 bug, provided below, confirming a bug in mmap handling on WSL 1.

Working with the WSL team at Microsoft, a fix for the WSL 1 bug was then tested in Windows Insider build 18890 and shipped in Windows 10 19041 (20H1).

Oracle provided their assistance no questions asked, even though we were officially partnered with Red Hat, and improved the experience of running all RHEL-based distros on WSL 1.

That is how Oracle helped save rpm on WSL 1.

Additional Reading

Sample C code from Oracle

/*
 * Author: Lauren Foutz
 * This program demonstrates a bug in extending an mmapped file in WSL, 
 * where the mappings to the extended part end up effecting mappings to
 * the beginning of the file.
 *
 * Output Should Be:
 *   Fill the first 64K bytes with 'A'.
 *   Extend the file to 1MB
 *   Now fill the extended part of the file with 'B'.
 *   First byte of the extended part of the file at address 0x7f03a50d6000, should be B: B
 *   First byte of the file at mapped address 0x7f03a50c6000, should be A: A
 *   Address 0x7f03a50d6000 is now: C, address 0x7f03a50c6000 should still be A, is now: A 
 *   Reopen the file and read the first 5 bytes.
 *   First five bytes of the file should be 'AAAAA': AAAAA
 *
 * On WSL the output is:
 *   Fill the first 64K bytes with 'A'.
 *   Extend the file to 1MB
 *   Now fill the extended part of the file with 'B'.
 *   First byte of the extended part of the file at address 0x7fe1db720000, should be B: B
 *   First byte of the file at mapped address 0x7fe1db710000, should be A: B
 *   Address 0x7fe1db720000 is now: C, address 0x7fe1db710000 should still be A, is now: C
 *   Reopen the file and read the first 5 bytes.
 *   First five bytes of the file should be 'AAAAA': CBBBB
 */

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>

#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>

int
main(argc, argv)
	int argc;
	char *argv[];
{
	    const char *io_file = "tmp_mmap4_f";
	    int fid = 0, i = 0;
	    void *addr1 = NULL;
	    int open_flags = 0, mode = 0, total_size = 0;
	    char buf[256], *caddr;

	    unlink(io_file);

	    open_flags = O_CREAT | O_TRUNC | O_RDWR;
	    mode = S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH;

	    /* Open a file. */
	    fid = open(io_file, open_flags, mode);

	    total_size = 1024 * 1024;

	    /* Fill the first 64K bytes with 'A'*/
	    printf("Fill the first 64K bytes with 'A'.\n");
	    memset(buf, (int)'A', sizeof(buf));
	    for (i = 0; i < 256; i++) {
		lseek(fid, i * 256, SEEK_SET);
		write(fid, buf, sizeof(buf));
	    }

	    /* mmap the file */
	    addr1 = mmap(NULL, total_size, PROT_READ | PROT_WRITE, MAP_SHARED, fid, 0);

	    /* Extend the file, write the last byte */
	    printf("Extend the file to 1MB\n");
	    lseek(fid, total_size - 1, SEEK_SET);
	    write(fid, buf, sizeof(buf[0]));

	    printf("Now fill the extended part of the file with 'B'.\n");
	    caddr = (char *)(addr1);
	    for (i = 256 * 256; i < total_size; i++) {
		caddr[i] = 'B';
	    }   
	    caddr = caddr + (256 * 256);
	    printf("First byte of the extended part of the file at address %p, should be B: %c\n", caddr,  caddr[0]);
	    printf("First byte of the file at mapped address %p, should be A: %c\n", addr1, *(char *)addr1);
	    caddr[0] = 'C';
	    printf("Address %p is now: %c, address %p should still be A, is now: %c \n", caddr, caddr[0], addr1, *(char *)addr1);

	    munmap(addr1, total_size);
	    fsync(fid);
	    close(fid);

	    /* Reopen the file and read the first 5 bytes. */
	    printf("Reopen the file and read the first 5 bytes.\n");
	    fid = open(io_file, O_RDWR, mode);
	    read(fid, buf, sizeof(buf[0]) * 5);
	    close(fid);
	    buf[5] = '\0';
	    printf("First five bytes of the file should be 'AAAAA': %s\n", buf);

	    return 0;
}