Kernel

UraniumOS runs the mainline Linux kernel, built from the source of the nixpkgs pin without patches and without module support. Version 0.1.0 uses Linux 7.3.0, with the release string 7.3.0-uranium. UraniumOS adds a configuration file, recipes/linux-uranium.config, and the recipe that builds with it. The kernel and the initramfs are the packages linux-uranium and initramfs-uranium, and scram boot copies them to the EFI System Partition (see boot). A frame stores the kernel it was taken with and boots with it. The runtime settings are described in security.

How it is built

The recipe recipes/linux-uranium.nix takes version and src from the nixpkgs attribute linux_latest and runs this:

make defconfig
scripts/kconfig/merge_config.sh -m .config linux-uranium.config
make olddefconfig
make -j$NIX_BUILD_CORES bzImage

The result is installed as boot/vmlinuz and boot/config (the final .config) in the package. The build needs bc bison flex perl openssl elfutils. The recipe sets hardeningDisable = [ "all" ], which turns off the compiler flags nixpkgs adds, because the kernel sets it's own. Binaries are not stripped. The license is GPL-2.0-only.

The file is a fragment on top of the default x86-64 configuration, so every option it does not mention keeps the default of that kernel version, and olddefconfig changes options whose dependencies are not met. The built configuration can therefore differ from the fragment. The tables below come from the final boot/config.

The kernel is not in the binary cache, so rod +i linux-uranium compiles it on the machine. The version follows the package index pin, so rod +s and an update moves it.

What is in the configuration

The fragment covers, besides hardening: EFI boot and the EFI framebuffer, the simple DRM driver with virtio-gpu, Bochs and i915, virtio devices, NVMe, SATA and USB storage, USB HID, Intel and Realtek Ethernet (e1000, e1000e, igb, r8169), btrfs, ext4, vfat, XFS, exFAT, NTFS3, NFS, squashfs, ISO 9660, overlayfs and FUSE, device mapper with dm-crypt, zram swap, nftables (NF_TABLES with conntrack, NAT, masquerade, log, limit and reject), WireGuard, tun, bridge, veth, macvlan and VLAN, KVM for Intel and AMD, namespaces and cgroups, Intel HDA and USB audio. Wi-Fi chip drivers for Intel, Atheros, Realtek, Mediatek and Broadcom are not set, although cfg80211 and mac80211 are present from the default configuration. Bluetooth and the AMD and Nouveau graphics drivers are not set either. See hardware and firmware.

Hardening options that are on

All of these are set to y in the built configuration.

Memory layout

RANDOMIZE_BASE, RANDOMIZE_MEMORY
The kernel image and the large memory regions (direct map, vmalloc, vmemmap) are placed at random addresses at each boot.
STRICT_KERNEL_RWX, DEBUG_WX
Kernel code and read-only data are not writable, and data is not executable. At boot the kernel checks its page tables and warns about any page that is both writable and executable.
STRICT_DEVMEM, IO_STRICT_DEVMEM
/dev/mem only gives access to memory that is not RAM, and to I/O regions that no driver has claimed. DEVMEM itself stays on.
LEGACY_VSYSCALL_NONE
The default for the legacy vsyscall page is none, so it is not mapped. The command line also has vsyscall=none.

Stacks

STACKPROTECTOR, STACKPROTECTOR_STRONG
Functions with arrays or with locals whose address is taken get a canary. A smashed canary stops the kernel.
VMAP_STACK, SCHED_STACK_END_CHECK
Kernel stacks are mapped with guard pages, and the end of the stack is checked on scheduling.
RANDOMIZE_KSTACK_OFFSET, RANDOMIZE_KSTACK_OFFSET_DEFAULT
The kernel stack gets a random offset on each system call. It is on by default, and randomize_kstack_offset=on on the command line says so again.
INIT_STACK_ALL_ZERO
Stack variables are initialized to zero.

Heap and copies

INIT_ON_ALLOC_DEFAULT_ON
Pages and heap objects are zeroed when they are allocated. Zeroing on free (INIT_ON_FREE_DEFAULT_ON) is not set.
ZERO_CALL_USED_REGS
Registers a function used are cleared when it returns.
SLAB_FREELIST_RANDOM, SLAB_FREELIST_HARDENED, SHUFFLE_PAGE_ALLOCATOR
The slab and page allocators hand out memory in a random order, and the free-list pointers of slab objects are obfuscated.
slab_nomerge (command line)
Caches of similar objects are not merged. The option SLAB_MERGE_DEFAULT is on, so the command line is what turns merging off.
HARDENED_USERCOPY, FORTIFY_SOURCE
Copies between user and kernel memory are checked against the size of the object, and string and memory functions are bounds-checked.
LIST_HARDENED, BUG_ON_DATA_CORRUPTION
Linked lists are checked, and detected corruption stops the kernel and does not continue.

The CPU

X86_KERNEL_IBT
Indirect branches in the kernel must land on marked entry points. This works on CPUs with indirect branch tracking.
X86_USER_SHADOW_STACK
Programs that opt in can use the hardware shadow stack.
MITIGATION_PAGE_TABLE_ISOLATION, MITIGATION_RETPOLINE, MITIGATION_SLS
Meltdown, Spectre variant 2 and straight-line speculation. The other MITIGATION_* options and CPU_MITIGATIONS keep their defaults and are on.

Security modules and filters

SECCOMP, SECCOMP_FILTER
A program can restrict its own system calls.
SECURITY_LANDLOCK
A program can restrict its own file access without privileges.
SECURITY_YAMA
Provides kernel.yama.ptrace_scope, which /etc/sysctl.conf sets to 2.
SECURITY_DMESG_RESTRICT
The kernel log is for root.
BPF_UNPRIV_DEFAULT_OFF
Unprivileged use of bpf() starts disabled.
CONFIG_LSM="landlock,lockdown,yama,integrity"
The order of the security modules. Only landlock and yama do anything here, because lockdown is not built and integrity has no policy.

Options that are off or not what the fragment asks for

Off on purpose

MODULES
Everything is built in and no module can be loaded. A driver that is not in the configuration is missing.
KEXEC, HIBERNATION, PROC_KCORE
A second kernel cannot be booted from a running one, memory cannot be written to disk for resume, and kernel memory is not exposed as /proc/kcore.
X86_IOPL_IOPERM, BINFMT_MISC, LEGACY_PTYS, COMPAT_BRK
The iopl and ioperm calls, registration of extra binary formats, BSD-style ptys and the legacy brk layout are not built.

In the fragment as off, but on or absent in the built configuration

DEBUG_FS=n
Built in anyway. debugfs=off on the command line keeps it from being mounted or used.
MODIFY_LDT_SYSCALL=n
On in the built kernel. The modify_ldt system call exists.
BPF_JIT_ALWAYS_ON=y
The option does not exist in the built kernel, and BPF_JIT is not set. BPF programs that root loads run in the interpreter. net.core.bpf_jit_harden does not exist either, see security.
RANDOM_KMALLOC_CACHES=y
The option does not exist in 7.3. KMALLOC_PARTITION_CACHES, the option of that kernel for seperate kmalloc caches, is not set.
ACPI_CUSTOM_METHOD=n, FB_SIMPLE=y
Neither option exists in the built configuration. DRM_SIMPLEDRM and the EFI framebuffer (FB_EFI) are on.

A fragment can ask for an option that the current version no longer has, and olddefconfig drops it without a message.

Left at the default, and wider than a hardened kernel would have

USER_NS (and NAMESPACES, PID_NS, NET_NS, UTS_NS, IPC_NS)
On. See the section on user namespaces below.
BPF_SYSCALL, BPF_EVENTS, KPROBES, FTRACE, PERF_EVENTS
On. The bpf() call exists, for root only after the sysctl.
IA32_EMULATION
On, so 32-bit programs run and the 32-bit system call entry exists. X86_X32_ABI is off.
IO_URING
On, with no sysctl to limit it.
LEGACY_TIOCSTI
On, and /etc/sysctl.conf does not set dev.tty.legacy_tiocsti. A process can push characters into the input of its own terminal. See sandboxes in security.
DEVPORT, X86_MSR, X86_CPUID, KALLSYMS, X86_VSYSCALL_EMULATION
On. Reading kernel symbol addresses needs kernel.kptr_restrict, which the sysctl file sets.
SECURITY_SELINUX, SECURITY_NETWORK, AUDIT
SELinux is compiled in from the default configuration, but it is not in CONFIG_LSM, so it does not start, and no policy is installed. AppArmor, Smack, Tomoyo, LoadPin and SafeSetID are not built.

Hardening options that are not set

These are common in hardened configurations and are not set here: INIT_ON_FREE_DEFAULT_ON, SECURITY_LOCKDOWN_LSM, KFENCE, UBSAN, PAGE_TABLE_CHECK, STATIC_USERMODEHELPER, KSTACK_ERASE, IMA, SECURITY_LOADPIN, SECURITY_SAFESETID, RESET_ATTACK_MITIGATION. Kernel lockdown is not built, so nothing stops root from using /dev/mem within the limits above, /dev/port and the MSR interface. Secure Boot is not supported, so the kernel and limine on the EFI System Partition are not signed.

User namespaces and BPF

Both are on, and both matter for the trade-off in security.

USER_NS is on because hazmat and silo run bubblewrap, and bubblewrap is not setuid here, since the only setuid file in the base system is doas. Without unprivileged user namespaces bubblewrap cannot create the sandbox. The cost is attack surface. In a user namespace a process gets capabilities over its own namespaces, for example CAP_NET_ADMIN in a new network namespace, and can reach kernel code that is otherwise restricted to root, such as the nftables code built into this kernel. Local privilege escalations in nftables have been exploited through user namespaces. The mainline kernel has no switch to allow user namespaces for root only. user.max_user_namespaces set to 0 turns them off for everyone and breaks the two tools, and /etc/sysctl.conf does not set it.

BPF_SYSCALL is on and unprivileged use is off, from BPF_UNPRIV_DEFAULT_OFF and from kernel.unprivileged_bpf_disabled = 1 in the sysctl file, which cannot be reverted without a reboot. A user namespace does not give the right to load BPF programs.

Kernel arguments

scram boot builds the command line of every menu entry as root=LABEL=uranium rw console=tty0 console=ttyS0,115200 followed by the single line of /etc/cmdline, and uranium.frame=ID for a frame entry. The file as installed:

slab_nomerge vsyscall=none debugfs=off randomize_kstack_offset=on
slab_nomerge
Do not merge slab caches of similar objects.
vsyscall=none
Do not emulate the legacy vsyscall page.
debugfs=off
debugfs is not available, though it is built.
randomize_kstack_offset=on
Randomize the kernel stack offset on each system call.

The file is read when scram boot runs, so run it after editing and reboot. It has to stay a single line, and a new file replaces the hardening arguments. To add an argument, append to the first line:

# sed -i '1s/$/ quiet/' /etc/cmdline
# scram boot

The arguments of the initramfs (uranium.shell, uranium.luks and so on) are in boot.

The initramfs

The package initramfs-uranium builds a gzip-compressed cpio archive with the script recipes/uranium-init.sh as /init, btrfs and cryptsetup, and some busybox applets. It has no kernel modules, no firmware and no CPU microcode. CONFIG_MICROCODE is on and late microcode loading is not.

Update

A new kernel comes with the package. rod +u builds the update in a copy of the system and leaves it waiting, and scram commit --boot schedules it for the next boot. scram boot copies the kernel of the profile to the EFI System Partition, and rod runs it after its own operations. Frames keep the kernels they were taken with. The boot partition holds one copy of each kernel, and at most three besides the current one (SCRAM_KERNELS). A frame that would need a fourth boots with the current kernel.

# rod +u
# scram commit --boot
# reboot

The configuration is /rod/recipes/linux-uranium.config, and the kernel in use has its final configuration in /rod/profile/boot/config. geiger compares the version in it with the running kernel.

Your own kernel

A recipe of the same name in /rod/recipes replaces the system one, see recipes. Copy linux-uranium.nix and the configuration, change the configuration and run rod +r linux-uranium +i linux-uranium. If the new kernel does not boot, choose a frame in the boot menu.

Modules

Drivers are built in and modules are not loaded. A driver that is not in the configuration is missing, and the fix is a rebuilt kernel.