feat: add host-backed entropy device support and update kernel configuration

This commit is contained in:
2026-09-23 07:00:22 +05:30
parent eb6119c797
commit dca3cbde2f
9 changed files with 139 additions and 10 deletions
+16 -2
View File
@@ -490,7 +490,14 @@ The `firecracker/` package has three separate responsibilities.
2. `PUT /boot-source` 2. `PUT /boot-source`
3. `PUT /drives/rootfs` 3. `PUT /drives/rootfs`
4. `PUT /network-interfaces/eth0` 4. `PUT /network-interfaces/eth0`
5. `PUT /actions` with `InstanceStart` 5. `PUT /entropy` with `{}` (a host-backed virtio RNG device)
6. `PUT /actions` with `InstanceStart`
Every generated config includes `entropy`, including retained-disk boots.
An empty object enables the device with default settings; do not skip it with
a truthiness check. A missing/invalid configuration or an entropy API failure
must prevent `InstanceStart`. Without guest entropy, systemd's random-seed
service and OpenSSH can remain in startup while the VM process and IP respond.
The guest receives a static IP via the kernel `ip=` boot argument. The netmask The guest receives a static IP via the kernel `ip=` boot argument. The netmask
comes from `Settings.network`, not a hard-coded `/24` value. comes from `Settings.network`, not a hard-coded `/24` value.
@@ -729,7 +736,14 @@ handling, downloads, and integrity-manifest publication.
(`amd64` to `x86_64`, `arm64` to `aarch64`) and rejects unsupported architectures. (`amd64` to `x86_64`, `arm64` to `aarch64`) and rejects unsupported architectures.
The same architecture selects the Firecracker release archive, guest kernel, The same architecture selects the Firecracker release archive, guest kernel,
native rootfs packages, and architecture-specific boot arguments. The default native rootfs packages, and architecture-specific boot arguments. The default
kernel URL uses Firecracker's `img/quickstart_guide/<architecture>/kernels/vmlinux.bin`. kernel URL is pinned to Firecracker's published
`firecracker-ci/v1.15/<architecture>/vmlinux-6.1.155` artifacts. The CI artifact
series is independent of the VMM release. Both architecture configs include
`CONFIG_HW_RANDOM=y` and `CONFIG_HW_RANDOM_VIRTIO=y`; custom kernels need these
built-in drivers for UVM's entropy device. The previous ARM quickstart image
was Linux 4.14.174 without the built-in virtio RNG driver. Updating the device
configuration alone cannot repair that kernel; existing installs need a forced
asset refresh with a matching new trusted kernel checksum, then a fresh VM boot.
An empty `Settings.rootfs_url` selects the built Ubuntu 22.04 (Jammy) template. An empty `Settings.rootfs_url` selects the built Ubuntu 22.04 (Jammy) template.
Explicit kernel/rootfs URLs and trusted digests must match the host architecture. ARM64 Explicit kernel/rootfs URLs and trusted digests must match the host architecture. ARM64
uses an uncompressed Linux `Image` (PE format) and an ARM64 ext4 rootfs. uses an uncompressed Linux `Image` (PE format) and an ARM64 ext4 rootfs.
+11
View File
@@ -56,6 +56,17 @@ Spark, UVM uses ARM64 assets and packages.
`x86_64`, respectively. Custom `UVM_KERNEL_URL` and `UVM_ROOTFS_URL` overrides `x86_64`, respectively. Custom `UVM_KERNEL_URL` and `UVM_ROOTFS_URL` overrides
must point to assets compatible with the host architecture. must point to assets compatible with the host architecture.
The default guest kernel is Linux 6.1.155 from Firecracker's published `v1.15`
CI artifacts, with built-in `virtio-rng` support. UVM attaches a host-backed
entropy device before each guest boot so services such as SSH can initialize
their cryptographic random generator. Custom kernels must include
`CONFIG_HW_RANDOM=y` and `CONFIG_HW_RANDOM_VIRTIO=y` for this device.
Older installs used a Linux 4.14 ARM demo kernel and supplied no entropy
device. If SSH stays at **Starting OpenBSD Secure Shell server** while ping
works, see [SSH startup and guest entropy](USAGE.md#ssh-startup-and-guest-entropy)
for the kernel-refresh and retained-VM recovery steps.
## Trusted Installation ## Trusted Installation
Installation requires trusted SHA-256 digests by default. Supply digests from Installation requires trusted SHA-256 digests by default. Supply digests from
+59 -1
View File
@@ -169,7 +169,7 @@ UVM detects the host architecture and selects matching Firecracker and kernel
downloads and native Ubuntu packages. NVIDIA DGX Spark uses `aarch64` artifacts downloads and native Ubuntu packages. NVIDIA DGX Spark uses `aarch64` artifacts
and `arm64` packages; the `arm64` architecture alias is also accepted: and `arm64` packages; the `arm64` architecture alias is also accepted:
- Kernel: `https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide/aarch64/kernels/vmlinux.bin` - Kernel: `https://s3.amazonaws.com/spec.ccfc.min/firecracker-ci/v1.15/aarch64/vmlinux-6.1.155`
- Rootfs: Ubuntu 22.04 built with `debootstrap` from `ports.ubuntu.com/ubuntu-ports`. - Rootfs: Ubuntu 22.04 built with `debootstrap` from `ports.ubuntu.com/ubuntu-ports`.
Use the installation commands below with checksums for the ARM64 kernel Use the installation commands below with checksums for the ARM64 kernel
@@ -180,6 +180,12 @@ On ARM64, the kernel must be an uncompressed ARM64 Linux `Image` (PE format),
and the rootfs must be an ARM64 ext4 filesystem. UVM adds `keep_bootcon` to the and the rootfs must be an ARM64 ext4 filesystem. UVM adds `keep_bootcon` to the
guest boot arguments for ARM64 console output. guest boot arguments for ARM64 console output.
Both architectures default to the pinned Linux 6.1.155 kernel from the published
`v1.15` CI artifact series, independently of the Firecracker binary version.
These kernels have `CONFIG_HW_RANDOM=y` and `CONFIG_HW_RANDOM_VIRTIO=y` built in.
UVM configures Firecracker's entropy device before boot; custom kernels also
need these drivers to receive host-backed randomness.
### Scenario: Strict First Installation ### Scenario: Strict First Installation
Obtain trusted checksums independently, then run: Obtain trusted checksums independently, then run:
@@ -1222,6 +1228,58 @@ External firewall integration (including UFW, firewalld, Docker, and libvirt)
is deferred. A UVM policy readback does not prove that external rules permit is deferred. A UVM policy readback does not prove that external rules permit
traffic, or that they will not later remove UVM's rules. traffic, or that they will not later remove UVM's rules.
### SSH Startup and Guest Entropy
`running` checks the Firecracker process, and `enforced` checks the private
firewall policy. Neither probes the guest's SSH listener. If ping works but
TCP/22 returns `Connection refused`, inspect the guest boot log:
```sh
sudo ./uvm.py logs VM --tail 200
sudo ./uvm.py inspect VM --json
```
An SSH service stuck at `Starting OpenBSD Secure Shell server...`, together
with an unfinished `Load/Save Random Seed` service and no `random: crng init done`,
strongly suggests entropy starvation. A login prompt can still appear while
SSH is waiting for its cryptographic random generator. On the old ARM demo
kernel, `random: fast init done` is not full cryptographic RNG initialization.
Earlier UVM versions downloaded a Linux 4.14.174 ARM quickstart kernel without
the built-in virtio RNG driver and did not attach an entropy device. Recovery
requires **both** the updated UVM boot code and an RNG-capable kernel. A restart
with the old cached kernel is insufficient.
From the updated checkout, stop the affected VM before refreshing assets.
Stop any other VMs using the same installed Firecracker binary before the
forced reinstall as well. Use the Firecracker archive checksum for the configured
release and a new trusted checksum for the selected architecture's Linux 6.1.155
kernel; the old kernel checksum will not match:
```sh
sudo ./uvm.py stop VM
sudo env -u UVM_KERNEL_URL \
UVM_FIRECRACKER_SHA256='<trusted-firecracker-archive-sha256>' \
UVM_KERNEL_SHA256='<trusted-new-kernel-sha256>' \
./uvm.py install --force
sudo ./uvm.py start VM
sudo ./uvm.py logs VM --tail 100
sudo ./uvm.py ssh VM
```
This uses the default kernel URL and boots the retained private disk with the
updated device configuration. The VM's identity, files, credentials, and SSH
host keys are retained. `install --force` also refreshes the shared rootfs
template for future creates. If using a custom kernel, supply its URL and
matching checksum with the required drivers instead. For an explicitly
unverified development installation, retain the existing
`UVM_ALLOW_UNVERIFIED_DOWNLOADS=1` setting as described in the installation section.
Restart a running UVM API server to load the updated code before using dashboard
start/restart actions. Look for `Started OpenBSD Secure Shell server` in the new
boot log and confirm an actual SSH connection. `start` on an already-running
VM only refreshes networking; use a stopped VM or `restart` to attach the device.
## 16. Important Limitations ## 16. Important Limitations
Keep these constraints in mind when deciding whether UVM fits a scenario: Keep these constraints in mind when deciding whether UVM fits a scenario:
+4 -4
View File
@@ -27,17 +27,17 @@ class ArchitectureSettingsTests(unittest.TestCase):
settings = factory() settings = factory()
base = ( base = (
"https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide/" "https://s3.amazonaws.com/spec.ccfc.min/firecracker-ci/v1.15/"
f"{architecture}" f"{architecture}"
) )
self.assertEqual(settings.architecture, architecture) self.assertEqual(settings.architecture, architecture)
self.assertEqual(settings.kernel_url, f"{base}/kernels/vmlinux.bin") self.assertEqual(settings.kernel_url, f"{base}/vmlinux-6.1.155")
self.assertEqual(settings.rootfs_url, "") self.assertEqual(settings.rootfs_url, "")
def test_preserves_individual_environment_url_overrides_on_arm64(self) -> None: def test_preserves_individual_environment_url_overrides_on_arm64(self) -> None:
kernel = "https://example.com/custom-arm64-kernel" kernel = "https://example.com/custom-arm64-kernel"
rootfs = "https://example.com/custom-arm64-rootfs.ext4" rootfs = "https://example.com/custom-arm64-rootfs.ext4"
base = "https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide/aarch64" base = "https://s3.amazonaws.com/spec.ccfc.min/firecracker-ci/v1.15/aarch64"
for overrides in ( for overrides in (
{"UVM_KERNEL_URL": kernel}, {"UVM_KERNEL_URL": kernel},
{"UVM_ROOTFS_URL": rootfs}, {"UVM_ROOTFS_URL": rootfs},
@@ -52,7 +52,7 @@ class ArchitectureSettingsTests(unittest.TestCase):
self.assertEqual( self.assertEqual(
settings.kernel_url, settings.kernel_url,
overrides.get("UVM_KERNEL_URL", f"{base}/kernels/vmlinux.bin"), overrides.get("UVM_KERNEL_URL", f"{base}/vmlinux-6.1.155"),
) )
self.assertEqual( self.assertEqual(
settings.rootfs_url, settings.rootfs_url,
+37
View File
@@ -3,9 +3,11 @@ from __future__ import annotations
import unittest import unittest
from ipaddress import IPv4Address, IPv4Network from ipaddress import IPv4Address, IPv4Network
from pathlib import Path from pathlib import Path
from unittest.mock import patch
from uvm.config import Settings from uvm.config import Settings
from uvm.domain import VmRecord from uvm.domain import VmRecord
from uvm.errors import FirecrackerError
from uvm.firecracker.api import FirecrackerClient from uvm.firecracker.api import FirecrackerClient
from uvm.firecracker.config import build_config, vcpu_count from uvm.firecracker.config import build_config, vcpu_count
@@ -45,6 +47,10 @@ class FirecrackerConfigTests(unittest.TestCase):
self.assertEqual("keep_bootcon" in arguments, architecture != "x86_64") self.assertEqual("keep_bootcon" in arguments, architecture != "x86_64")
self.assertIn("console=ttyS0", arguments) self.assertIn("console=ttyS0", arguments)
self.assertIn("ip=10.42.0.2::10.42.0.1:255.255.255.0::eth0:off", arguments) self.assertIn("ip=10.42.0.2::10.42.0.1:255.255.255.0::eth0:off", arguments)
client = RecordingFirecrackerClient()
client.configure_and_start(config)
self.assertEqual(client.calls[-2], ("PUT", "/entropy", {}))
self.assertEqual(client.calls[-1], ("PUT", "/actions", {"action_type": "InstanceStart"}))
def test_builds_a_writable_vm_disk_configuration(self) -> None: def test_builds_a_writable_vm_disk_configuration(self) -> None:
vm = VmRecord( vm = VmRecord(
@@ -101,6 +107,7 @@ class FirecrackerConfigTests(unittest.TestCase):
"/boot-source", "/boot-source",
"/drives/rootfs", "/drives/rootfs",
"/network-interfaces/eth0", "/network-interfaces/eth0",
"/entropy",
"/actions", "/actions",
], ],
) )
@@ -110,6 +117,36 @@ class FirecrackerConfigTests(unittest.TestCase):
) )
self.assertEqual(client.calls[-1][2], {"action_type": "InstanceStart"}) self.assertEqual(client.calls[-1][2], {"action_type": "InstanceStart"})
def test_entropy_failure_prevents_guest_boot(self) -> None:
config = {
"machine-config": {"vcpu_count": 1, "mem_size_mib": 512},
"boot-source": {},
"drives": [{}],
"network-interfaces": [{}],
"entropy": {},
}
client = RecordingFirecrackerClient()
def request(method: str, path: str, body: object) -> None:
client.calls.append((method, path, body))
if path == "/entropy":
raise FirecrackerError("entropy device unavailable")
with patch.object(client, "_request", side_effect=request):
with self.assertRaisesRegex(FirecrackerError, "entropy device unavailable"):
client.configure_and_start(config)
self.assertEqual(client.calls[-1], ("PUT", "/entropy", {}))
self.assertNotIn("/actions", [path for _method, path, _body in client.calls])
for entropy in (None, [], False):
with self.subTest(entropy=entropy):
client.calls.clear()
config["entropy"] = entropy
with self.assertRaisesRegex(FirecrackerError, "no valid entropy device"):
client.configure_and_start(config)
self.assertEqual(client.calls, [])
def test_uses_the_configured_network_netmask_in_guest_boot_arguments(self) -> None: def test_uses_the_configured_network_netmask_in_guest_boot_arguments(self) -> None:
settings = Settings( settings = Settings(
network=IPv4Network("10.50.0.0/16"), network=IPv4Network("10.50.0.0/16"),
+2 -1
View File
@@ -72,7 +72,8 @@ class InstallerArchitectureTests(unittest.TestCase):
payloads = { payloads = {
"https://github.com/firecracker-microvm/firecracker/releases/download/" "https://github.com/firecracker-microvm/firecracker/releases/download/"
f"{version}/firecracker-{version}-{architecture}.tgz": archive_bytes, f"{version}/firecracker-{version}-{architecture}.tgz": archive_bytes,
f"{guest_base}/kernels/vmlinux.bin": kernel_bytes, "https://s3.amazonaws.com/spec.ccfc.min/firecracker-ci/v1.15/"
f"{architecture}/vmlinux-6.1.155": kernel_bytes,
f"{guest_base}/rootfs/bionic.rootfs.ext4": rootfs_bytes, f"{guest_base}/rootfs/bionic.rootfs.ext4": rootfs_bytes,
} }
runner = Mock(spec=CommandRunner) runner = Mock(spec=CommandRunner)
+4 -2
View File
@@ -10,7 +10,9 @@ from pathlib import Path
from .errors import ConfigurationError from .errors import ConfigurationError
DEFAULT_GUEST_ASSETS_URL = "https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide" # Pin published kernels with built-in virtio-rng support. The artifact series
# is independent of the VMM release; the old ARM quickstart kernel lacks this driver.
DEFAULT_GUEST_ASSETS_URL = "https://s3.amazonaws.com/spec.ccfc.min/firecracker-ci/v1.15"
MIN_STORAGE_MIB = 30 * 1024 MIN_STORAGE_MIB = 30 * 1024
MAX_STORAGE_MIB = (2**63 - 1) // (1024 * 1024) MAX_STORAGE_MIB = (2**63 - 1) // (1024 * 1024)
@@ -55,7 +57,7 @@ class Settings:
# Resolve defaults at construction time, while preserving individual URL overrides. # Resolve defaults at construction time, while preserving individual URL overrides.
asset_base = f"{DEFAULT_GUEST_ASSETS_URL}/{architecture}" asset_base = f"{DEFAULT_GUEST_ASSETS_URL}/{architecture}"
if not self.kernel_url: if not self.kernel_url:
object.__setattr__(self, "kernel_url", f"{asset_base}/kernels/vmlinux.bin") object.__setattr__(self, "kernel_url", f"{asset_base}/vmlinux-6.1.155")
if self.gateway not in self.network: if self.gateway not in self.network:
raise ConfigurationError( raise ConfigurationError(
+4
View File
@@ -35,12 +35,15 @@ class FirecrackerClient:
boot = config["boot-source"] boot = config["boot-source"]
drives = config["drives"] drives = config["drives"]
interfaces = config["network-interfaces"] interfaces = config["network-interfaces"]
entropy = config.get("entropy")
if not isinstance(machine, Mapping) or not isinstance(boot, Mapping): if not isinstance(machine, Mapping) or not isinstance(boot, Mapping):
raise FirecrackerError("invalid Firecracker configuration") raise FirecrackerError("invalid Firecracker configuration")
if not isinstance(drives, list) or not drives: if not isinstance(drives, list) or not drives:
raise FirecrackerError("Firecracker configuration has no root drive") raise FirecrackerError("Firecracker configuration has no root drive")
if not isinstance(interfaces, list) or not interfaces: if not isinstance(interfaces, list) or not interfaces:
raise FirecrackerError("Firecracker configuration has no network interface") raise FirecrackerError("Firecracker configuration has no network interface")
if not isinstance(entropy, Mapping):
raise FirecrackerError("Firecracker configuration has no valid entropy device")
self._request( self._request(
"PUT", "PUT",
@@ -54,6 +57,7 @@ class FirecrackerClient:
self._request("PUT", "/boot-source", boot) self._request("PUT", "/boot-source", boot)
self._request("PUT", "/drives/rootfs", drives[0]) self._request("PUT", "/drives/rootfs", drives[0])
self._request("PUT", "/network-interfaces/eth0", interfaces[0]) self._request("PUT", "/network-interfaces/eth0", interfaces[0])
self._request("PUT", "/entropy", entropy)
self._request("PUT", "/actions", {"action_type": "InstanceStart"}) self._request("PUT", "/actions", {"action_type": "InstanceStart"})
def _request(self, method: str, path: str, body: object) -> None: def _request(self, method: str, path: str, body: object) -> None:
+2
View File
@@ -70,6 +70,8 @@ def build_config(
"host_dev_name": vm.tap, "host_dev_name": vm.tap,
} }
], ],
# Supply host-backed randomness before guest services such as sshd start.
"entropy": {},
} }