Skip to content

perf: switch app rootfs from ext4 to erofs with LZ4 compression#94

Merged
hiroTamada merged 5 commits intomainfrom
perf/erofs-rootfs
Feb 13, 2026
Merged

perf: switch app rootfs from ext4 to erofs with LZ4 compression#94
hiroTamada merged 5 commits intomainfrom
perf/erofs-rootfs

Conversation

@hiroTamada
Copy link
Contributor

@hiroTamada hiroTamada commented Feb 12, 2026

Summary

  • Switch default rootfs image format from ext4 to erofs with LZ4 compression, reducing disk image size by ~60% (77MB vs 193MB for node:20-alpine)
  • Auto-detect filesystem type when mounting /dev/vda in the VM init so both erofs (new default) and ext4 (legacy) images boot correctly
  • Add unit test comparing ext4 vs erofs export (size, alignment, creation time)

Details

App rootfs disks are read-only in the VM — they're mounted as the lower layer of an overlayfs. A read-only compressed filesystem (erofs) is a natural fit:

Metric ext4 (before) erofs (after)
node:20-alpine rootfs size 193 MB 77 MB
Sector-aligned yes yes

The VM kernel (ch-6.12.8-kernel-1.4) has CONFIG_EROFS_FS=y built-in with full compression support (LZ4, ZIP, LZMA).

Files changed

  • lib/images/disk.goDefaultImageFormat changed from FormatExt4 to FormatErofs
  • lib/system/init/mount.go — omit -t flag when mounting /dev/vda so kernel auto-detects fs type; backwards compatible with existing ext4 images
  • lib/images/disk_test.go — new test exercising both formats

Test plan

  • Unit test passes: TestExportRootfsFormats (ext4 + erofs both produce valid, sector-aligned output)
  • E2E tested locally: build → erofs conversion → VM boot → app execution (erofs test OK)
  • VM boot log confirms: erofs: (device vda): mounted with root inode @ nid 37.
  • Verified kernel config: CONFIG_EROFS_FS=y in ch_defconfig

🤖 Generated with Claude Code


Note

Medium Risk
Changes the default on-disk rootfs format and introduces a new image registration path that bypasses the existing conversion pipeline, so boot/mount compatibility and registry/layer-extraction edge cases could impact build readiness or runtime.

Overview
Switches the default OCI rootfs export format from ext4 to erofs with LZ4 compression, adding a fallback to ext4 when mkfs.erofs is unavailable and updating VM init mounting to auto-detect the filesystem type for backward compatibility.

Adds a prebuilt-erofs fast path for source builds: the builder agent pulls the freshly pushed image from the internal registry, extracts layers (handling whiteouts), runs mkfs.erofs, and returns the disk path in the build result; the host then extracts that disk from the source volume and registers it as a ready image via a new images.Manager.RegisterPrebuiltImage, skipping the existing push→unpack→convert pipeline when successful.

Includes supporting changes: builder image now installs erofs-utils/zstd, Docker builder image builds use --network=host, improved error reporting when image conversion fails, a new unit test covering ext4 vs erofs export/sector alignment, and an added E2E benchmark script for measuring the optimization.

Written by Cursor Bugbot for commit b0bc0ba. This will update automatically on new commits. Configure here.

hiroTamada and others added 2 commits February 12, 2026 10:05
App rootfs disks are read-only in the VM (mounted as the lower layer of
an overlayfs), so a read-only filesystem is a better fit. erofs with LZ4
compression produces 60% smaller disk images (77MB vs 193MB for
node:20-alpine) and is faster to create than ext4.

Changes:
- Set DefaultImageFormat to FormatErofs (disk.go)
- Auto-detect filesystem type when mounting /dev/vda so both erofs and
  ext4 images work (mount.go)
- Add unit test comparing ext4 vs erofs export (disk_test.go)

Verified: VM kernel (ch-6.12.8-kernel-1.4) has CONFIG_EROFS_FS=y built-in.
E2E tested: build → erofs conversion → VM boot → app execution all working.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- convertToErofs falls back to ext4 if mkfs.erofs is not installed
- buildImage now logs errors to stderr (was only storing in metadata)
- waitForImageReady propagates the actual error from image metadata
  instead of generic "image conversion failed"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
// Check if mkfs.erofs is available
if _, err := exec.LookPath("mkfs.erofs"); err != nil {
fmt.Fprintf(os.Stderr, "Warning: mkfs.erofs not found, falling back to ext4\n")
return convertToExt4(rootfsDir, diskPath)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent erofs-to-ext4 fallback masks missing tool in production

Medium Severity

convertToErofs silently falls back to convertToExt4 when mkfs.erofs is missing, writing only a warning to stderr. Since DefaultImageFormat was switched to FormatErofs in this same commit, a production host without mkfs.erofs will silently produce ext4 images while callers believe they created erofs images. The test TestExportRootfsFormats would also silently pass by testing ext4 twice, never actually validating erofs output.

Additional Locations (1)

Fix in Cursor Fix in Web

Create the erofs disk inside the builder VM after the BuildKit push
completes, eliminating the slow host-side umoci unpack + mkfs.erofs
conversion pipeline.

After buildctl pushes the image to the registry, the builder agent:
1. Pulls the manifest and layer blobs from the local registry
2. Extracts layers into a tmpfs (handles gzip/zstd decompression)
3. Processes OCI whiteout files
4. Runs mkfs.erofs with LZ4 compression
5. Writes the erofs file to the source volume

The host then mounts the source volume, copies the erofs disk, and
registers it directly as a ready image via RegisterPrebuiltImage.
This skips waitForImageReady entirely.

Every step has a graceful fallback - if any part fails, the existing
push → unpack → convert pipeline is used instead.

E2E benchmark (node:20-alpine test app, warm caches):
- Baseline: ~11,020ms total (~468ms post-build conversion)
- Optimized: ~8,341ms total (conversion bypassed)

Also adds erofs-utils and zstd to the builder Dockerfile, resizes
source volumes to 512MB minimum for erofs workspace, and includes
an e2e benchmark script.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Process OCI whiteout files after each layer extraction instead of in a
single pass after all layers. The previous approach could incorrectly
delete files added by later layers when an earlier layer contained
whiteouts targeting the same paths.

Add 4096-byte sector alignment to prebuilt erofs disks in
RegisterPrebuiltImage, matching the alignment applied by convertToErofs
in the normal pipeline. Required by hypervisors for block device I/O.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

Reject absolute paths and .. components in the erofs disk path received
from the builder VM via vsock before using it in filepath.Join. Defense
in depth — the VM is the security boundary, but untrusted input from it
should still be validated on the host side.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@hiroTamada hiroTamada merged commit 8f3e617 into main Feb 13, 2026
4 checks passed
@hiroTamada hiroTamada deleted the perf/erofs-rootfs branch February 13, 2026 00:21
@rgarcia rgarcia mentioned this pull request Feb 13, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants