Skip to content

Conversation

@ddiss
Copy link
Collaborator

@ddiss ddiss commented Dec 3, 2025

Sangeetha and I have been working on some changes to replace Dracut with
a rust-based initramfs / cpio image generator. The main reasons for this
are:

  • Some distros are moving away from using Dracut, so we can't assume it's
    locally available
  • We only use a small portion of Dracut functionality: base and systemd
    modules, and kernel / user dependency gathering for cpio
  • Dracut is slow: it forks many processes, stages all initramfs content
    and is mostly written in bash

The rewrite builds on my previous dracut-cpio implementation and adds:

  • elf dependency gathering using https://github.com/cole14/rust-elf
  • kernel module dependency gathering via native modules.dep (etc.)
    parsing (thanks @thackara !)
    • VMs still user regular kmod / modprobe.
  • basic rapido-vm and rapido-init programs, to run qemu and start autorun
    scripts
  • basic rapido.conf parsing

It's otherwise kept as minimal as possible, with rust-elf and the std
library the only major external dependencies. One single-file crosvm
argument parser is also bundled.

The preliminary benchmark results look good, particularly for initramfs
image generation (cut):

---------------+-------------------------------+-----------------------|
               | Before: rapido e4c6077        | After: rs_wip 9a90973 |
               | dracut-059+suse.769.g693ea004 |  rustc 1.91.0         |
---------------+-------------------------------+-----------------------|
simple-example |   2.389s +- 0.117             | 0.075s +- 0.001       |
cut            |                               |                       |
---------------+-------------------------------+-----------------------|
simple-example |   7.2942 +- 0.065             | 4.746s +- 0.003       |
cut+boot+exit  |                               |                       |
---------------+-------------------------------+-----------------------|
simple-network |   2.572s +- 0.135             | 0.098s +- 0.000       |
cut            |                               |                       |
---------------+-------------------------------+-----------------------|
simple-network |   7.460s +- 0.126             | 4.926s +- 0.011       |
cut+boot+exit  |                               |                       |
---------------+-------------------------------+-----------------------|

To try these changes yourself, check out this branch and run:

cargo build --offline --release
./rapido cut simple-example

You may need to change your rapido.conf file a little if you
use env variables or shell callouts.

I'm flagging this as WIP, as there are still a few things to do:

  • boot with systemd as init, instead of only rapido-init
  • clean up interface before locking it in (parameter naming, etc.)
  • think about distro packaging / path assumptions
    • at the moment, it assumes bins are in target/release/* and conf is
      in the working directory
  • improve test coverage

I don't expect to convert remaining cut scripts before merge. Dracut and
rust based functionality should be able to live side by side, although
rapido.conf parsing is much less flexible in rust: no invocations,
currently no env var expansion, variables must be wrapped in {}.

ddiss added 30 commits November 6, 2025 10:56
Crosvm's rust argument library is very small and simple, while still
providing helpful functionality. It will be consumed by dracut-cpio in a
subsequent commit.

The unmodified, BSD licensed argument.rs source is lifted as-is from
https://chromium.googlesource.com/chromiumos/platform/crosvm
(release-R92-13982.B b6ae6517aeef9ae1e3a39c55b52f9ac6de8edb31).
The one-line crosvm.rs wrapper is needed to ensure that crosvm::argument
imports continue to work.

Signed-off-by: David Disseldorp <ddiss@suse.de>
dracut-cpio is a minimal cpio archive creation utility written in Rust.
It provides support for a minimal set of features needed to create
performant and space-efficient initramfs archives:
- "newc" archive format only
- reproducible; inode numbers, uid/gid and mtime can be explicitly set
- data segment copy-on-write reflinks
  + using Rust io::copy()'s native copy_file_range() support[1]
  + optional archive data segment alignment for optimal reflink use[2]
- hardlink support
- comprehensive tests asserting GNU cpio binary output compatibility

1. Rust io::copy() copy_file_range()
   rust-lang/rust#75272

2. Data segment alignment
   We're bending the newc spec a bit to inject zeros after the file path
   to provide data segment alignment. These zeros are accounted for in
   the namesize, but some applications may only expect a single
   zero-terminator (and 4 byte alignment). GNU cpio and Linux initramfs
   handle this fine as long as PATH_MAX isn't exceeded.

Signed-off-by: David Disseldorp <ddiss@suse.de>
This is a workaround for GRUB2's Btrfs implementation, which doesn't
correctly handle gaps between extents.

A fix has already been proposed upstream via
https://lists.gnu.org/archive/html/grub-devel/2021-10/msg00206.html

Given that this bug is severe, it makes sense to include this minimal
workaround.

Signed-off-by: David Disseldorp <ddiss@suse.de>
This will be used for future device major/minor testing. Convert the
current fifo test to use it.

Signed-off-by: David Disseldorp <ddiss@suse.de>
This tests dracut-cpio's handling of rmajor / rminor values compared to
GNU cpio. The test requires root, due to mknod invocation for block
device node creation.

Signed-off-by: David Disseldorp <ddiss@suse.de>
dev_t -> major/minor number mapping is more complicated than the
incorrect major=(dev_t >> 8) minor=(dev_t & 0xff) mapping that we
currently perform. Fix mapping to match Linux / glibc behaviour.

Fixes: dracutdevs/dracut#1695
Reported-by: Ethan Wu <ethanwu10@gmail.com>
Signed-off-by: David Disseldorp <ddiss@suse.de>
The previous dracut-cpio commits were applied verbatim from patches
generated from the dracut source via:
git format-patch 94fc50262f5e6c28d92782dc231fbb6c61855954^..94fc50262f5e6c28d92782dc231fbb6c61855954
git format-patch a9c67046431ccf5fd4f4c16c890695df388f0d38^..a9c67046431ccf5fd4f4c16c890695df388f0d38
git format-patch 0af11c5ea5018a3e1049a2207a9a671049651876^..0af11c5ea5018a3e1049a2207a9a671049651876
git format-patch 80e70f76d92b1a1c8e5cd10a06b70ef3f97d0899^..80e70f76d92b1a1c8e5cd10a06b70ef3f97d0899
git format-patch 8bd7ddf8197c14532cf05edac3203d08798af6f2^..8bd7ddf8197c14532cf05edac3203d08798af6f2
git format-patch acc629abb0d7a26f692f99e5a9cf8c8401bc6a86^..acc629abb0d7a26f692f99e5a9cf8c8401bc6a86

This change moves the nested src/dracut-cpio/ Cargo project to the
root, with source in src/main.rs . third_party is also moved under
src.

Signed-off-by: David Disseldorp <ddiss@suse.de>
warning: call to `.clone()` on a reference in this situation does
nothing
   --> src/main.rs:289:27
    |
289 |     let mut outpath = path.clone();
    |                           ^^^^^^^^
    |
    = note: the type `Path` does not implement `Clone`, so calling
`clone` on `&Path` copies the reference, which does not do anything and
can be removed
    = note: `#[warn(noop_method_call)]` on by default
help: remove this redundant call
    |
289 -     let mut outpath = path.clone();
289 +     let mut outpath = path;

Signed-off-by: David Disseldorp <ddiss@suse.de>
dracut-cpio unit tests compare binary archive output with that of GNU
cpio, for the same set of input files. A recent change to upstream GNU
cpio, commit 6a94d5e ("New option --ignore-dirnlink"), causes some tests
to fail.
The failure is due to GNU cpio `--reproducible` now hardcoding directory
nlink values to 2, instead of using the st_nlink value reported by
stat().

Fix the unit tests by dropping the GNU cpio `--reproducible` alias
parameter, and instead specify `--ignore-devno --renumber-inodes`
explicitly, matching pre-6a94d5e GNU cpio `--reproducible` behaviour.

This fix has also been submitted to upstream dracut-ng.

Signed-off-by: David Disseldorp <ddiss@suse.de>
The 'std::' namespace prefix is unnecessary so drop it.

Signed-off-by: David Disseldorp <ddiss@suse.de>
In preparation for reusing the core cpio archiving library code for
rapido.

Signed-off-by: David Disseldorp <ddiss@suse.de>
The newly bundled dracut-cpio source is GPL-2.0 licensed, with and
additional BSD licensed third_party crosvm argument parsing library.
People should rely on the per-file license headers as an indicator.

Signed-off-by: David Disseldorp <ddiss@suse.de>
The compiler warns that it's unused for the normal build.

Signed-off-by: David Disseldorp <ddiss@suse.de>
Obtained from a https://github.com/cole14/rust-elf clone via:
  $ git archive --format=tgz -o rust-elf-v0.8.0.tgz v0.8.0
  $ sha256sum rust-elf-v0.8.0.tgz
    082a203a8b47a94cff85d055b03852dc69291c0d4c51d82d7b43c65fdd2a9188

The v0.8.0 tag references c4d5222a34a97e113f863f80399284767d725e28 .

Signed-off-by: David Disseldorp <ddiss@suse.de>
Use rust-elf to recursively walk through all ELF dependencies for a
given binary.

Plenty of missing bits:
- hardcoded input: only looks for 'ls' dependencies
- directory and kernel modules handling
- cpio archive generation
- lacks proper error handling
- is written by someone who doesn't know idiomatic rust (me)
  - not sure I want to learn it; heavy abstraction and explicit
    lifetimes scare me

Signed-off-by: David Disseldorp <ddiss@suse.de>
`cargo test` runs tests in parallel by default, unless the
--test-threads=1 parameter is provided. This causes the dracut-cpio
unit tests to fail due to their working directory changes.

Add a mutex and hold it over the course of the changed directory, so
that the --test-threads=1 parameter is no longer needed.

Suggested-by: Benjamin Drung <bdrung@posteo.de>
Fixes: dracut-ng/dracut-ng#1702
Signed-off-by: David Disseldorp <ddiss@suse.de>
Drop the hardcoded 'ls' binary and allow users to provide a space
separated install list, e.g.
  ./rapido-cut --install "ls bash" my.initramfs

The actual cpio archive generation isn't hooked up yet.

Signed-off-by: David Disseldorp <ddiss@suse.de>
Archive entries are written out as the files are found, so ideally we
can reuse any file handle we have around from the elf parsing.
Next steps will be to use the rust-elf file-seek API, so that we're
not buffering the entire file data.

Signed-off-by: David Disseldorp <ddiss@suse.de>
rust-elf provides a seeking file API via the "std" feature, so use it
instead of buffering the entire file for parsing.

Signed-off-by: David Disseldorp <ddiss@suse.de>
We're archiving paths immediately as they're found, so we shouldn't
need to track this separately, at least not for now.

Signed-off-by: David Disseldorp <ddiss@suse.de>
Perform the stat in the caller instead, so that rapido-cut can avoid
a double stat.

Signed-off-by: David Disseldorp <ddiss@suse.de>
This should be squashed with the previous commit to avoid breaking
bisect.

Signed-off-by: David Disseldorp <ddiss@suse.de>
The main reason to split this from archive_path is to support pre-opened
file descriptors. E.g. For rapido-cut this will allow us to reuse the
elf_deps fd instead of closing and reopening to write file data to the
cpio archive.

Signed-off-by: David Disseldorp <ddiss@suse.de>
Reuse the elf_deps fd for it.

Signed-off-by: David Disseldorp <ddiss@suse.de>
Callers must now use archive_file for files and archive_path for
anything else, to allow for open-fd reuse when writing file data.
Convert dracut-cpio and hardlink callers.
A typo that I made in the initial dracut-cpio implementation sees the
stat()-reported major number used for both major and minor number in the
cpio archive.

Device major / minor numbers (as opposed to rmajor / rminor numbers) are
mostly ignored by initramfs, with the exception of hardlink association.
I've not seen any bug reports from users hitting this in the wild, but
theoretically cross-device archives could carry incorrectly colliding
major/minor/inode triplets which erroneously trigger initramfs hardlink
handling.

Signed-off-by: David Disseldorp <ddiss@suse.de>
initramfs / cpio allow for the tracking of hardlinks for nlink >= 2
entries using a combination of the inode, device major and minor
numbers.

dracut-cpio uses unique inode numbers within an archive via the global
state.ino counter. Device major/minor numbers are also renumbered, with
each unique source device obtaining a major/minor number mapped from the
index within dev_seen()/DevState array.

With archive-unique inode numbers, device major/minor mapping is
unnecessary. This change sees dracut-cpio behave the same as GNU
cpio --ignore-devno, where archive device major/minor numbers are
hardcoded to zero.

Hardlink tracking is simplified, replacing per-device HardlinkState
arrays with a global state.hls array. A hash could be used for faster
source inode+dev -> archive HardlinkState mapping, but the extra
size and complexity isn't worth it IMO, given that hardlinks should be
rare.

Signed-off-by: David Disseldorp <ddiss@suse.de>
Inode numbers are unique (for non-hardlinks) within the archive, so
device ID mapping is unnecessary. Confirm that dracut-cpio behaves like
GNU cpio --ignore-devno. Check this by archiving the /tmp directory
alongside a working-directory nested file; despite differing source
device IDs, the archived major/minor numbers should be zero.

The test is skipped if stat(/tmp) fails, or working-directory and /tmp
device ids match.

Signed-off-by: David Disseldorp <ddiss@suse.de>
This is currently all my work (at SUSE), including dracut-cpio. Using
(GPL-2.0 OR GPL-3.0) instead of GPL-2.0 only makes the rust codebase a
little more compatible with other projects, while remaining copyleft.

Signed-off-by: David Disseldorp <ddiss@suse.de>
Likely only useful for rapido.rs, but I've split it out as a separate
project at https://github.com/ddiss/kv-conf . This source corresponds to
commit 1f8d1448c613f87ed681a928c61b686914273b6e.

Signed-off-by: David Disseldorp <ddiss@suse.de>
ddiss added 29 commits December 18, 2025 16:58
Ubuntu 24.04.3 LTS can run rust-based rapido using the distro kernel.
However, simple-example isn't an option due to a lack of zram, so use
simple-network instead.

Signed-off-by: David Disseldorp <ddiss@suse.de>
Signed-off-by: David Disseldorp <ddiss@suse.de>
It appears to be present on the Github VM, so see whether we can use it.
Some additional changes split out the cut / boot tests, so that we can
inspect the initramfs image before boot.

Signed-off-by: David Disseldorp <ddiss@suse.de>
Signed-off-by: David Disseldorp <ddiss@suse.de>
On Ubuntu, initramfs-tools-core carries an lsinitramfs script, which is
a wrapper for unmkinitramfs --list, which is a wrapper for GNU cpio.

Signed-off-by: David Disseldorp <ddiss@suse.de>
Use something a little shorter. I've left the src/bin/kmod/main.rs
example code as-is.

Signed-off-by: David Disseldorp <ddiss@suse.de>
Pass the mutable line buffer to kv_process, so that it can clear any
processed data and retain unprocessed multi-line portions. A (now
non-mut) map still needs to be provided for ${} variable substitution.

This should put us in a better position to support callback based kv
parsing or (perhaps?) iter().

Signed-off-by: David Disseldorp <ddiss@suse.de>
Signed-off-by: David Disseldorp <ddiss@suse.de>
Like bash, we should return an error if we hit EOF before the closing
quote of a (multi-line assumed) key/val pair.
Also add an extra test for comment parsing, where unquoted # without a
preceding space should be handled as part of the value string.

Signed-off-by: David Disseldorp <ddiss@suse.de>
All errors are InvalidInput here, so it's simpler if we just return a
Result<..., &'static str>.

Signed-off-by: David Disseldorp <ddiss@suse.de>
All errors are InvalidInput here, so it's simpler if we just return a
Result<..., &'static str>.
We might want to change over the kv_conf_process[_append] return types
in future too, but Err mapping in callers would probably make it a bit
ugly.

Signed-off-by: David Disseldorp <ddiss@suse.de>
This function is the same as kv_conf_process_append() except variable
lookup is done from @varmap, while kv entries are stored separately in
@Map. It will (hopefully) be useful for rapido-cut manifest file
support.

To avoid too much duplication, the rdr.read_line() call and error
handling is moved into a separate helper.

Signed-off-by: David Disseldorp <ddiss@suse.de>
In preparation for reusing some of these parameters for manifest files.

Signed-off-by: David Disseldorp <ddiss@suse.de>
Manifest files carry key=value entries, where "key" can only be one of:
  install, try_install, include, kmods, autorun
These keys map directly to the corresponding rapido-cut command line
parameter; parsing logic for manifest keys and cli parameters is shared.

Manifest file values may include rapido.conf keys as variables, e.g.
  include="${VM_NET_CONF} /net"

Signed-off-by: David Disseldorp <ddiss@suse.de>
Signed-off-by: David Disseldorp <ddiss@suse.de>
The new manifest/net.fest file carries all of the dependencies that were
automatically added via --net.

Pros:
- drop a workload specific parameter
- easy to change at runtime instead of build-time only

Cons:
- slight cut slowdown expected, due to kv-conf file parsing
- confusing:
  - no net specific autorun; it's handed by rapido-init
  - rapido-vm's rapido-rsc parsing optimization seems more fragile with
    this, as control over the order of archiving is more difficult

Given the cons above, this change may be reverted in future (for net
*only* - I think generic manifest support should stay).

Signed-off-by: David Disseldorp <ddiss@suse.de>
When determining VM cpu/mem/net resources, rapido-vm breaks cpio
traversal when moving past /rapido-rsc/* paths as an optimization.
Therefore we need to ensure that these paths are grouped together.
Do this by prepending any include destination path that starts with
"/rapido-rsc" to the data.items list.
Sangeetha helpfully pointed out that Path::starts_with() works fine
even if multiple path separators are used.

One subtle change in behaviour here is that rapido-rsc paths will
be ordered in the reverse of where they're specified, which makes
a difference for resource parameter override. E.g:
  --include "X /rapido-rsc/mem/4G" --include "X /rapido-rsc/mem/5G"
Prior to this change, rapido-vm would boot with 5G while now it'll
boot with 4G.
This could be easily fixed by using a state.rsc_insert_off variable
but I don't think it's necessary for now.

One further note regarding /rapido-rsc/net subdirectories: these are
discovered as part of gather_archive_data() directory traversal and
won't be prepended, but that doesn't matter; rapido-vm only checks for
/rapido-rsc/net parent path presence.

Signed-off-by: David Disseldorp <ddiss@suse.de>
s.parse::<u32>() is called twice. Not sure whether the compiler
optimizes out the second call, but cleaning it up is quicker than
checking.

Add a couple of extra rsc parsing test cases to ensure double-unit
mem values don't slip through.

Signed-off-by: David Disseldorp <ddiss@suse.de>
network kmods are now covered by the net.fest manifest file, so change
rapido-init to explicitly add virtio_net and af_packet to the modprobe
list if needed.

Signed-off-by: David Disseldorp <ddiss@suse.de>
kcli_parse() is fed the entire /proc/cmdline buffer, and only splits on
b' ' match. Ignore any newline to ensure that it doesn't get picked up
by e.g. the hostname (via vm_num).

Also, sprinkle some lifetime syntax based on following compiler
suggestion:
  --> src/bin/rapido-init.rs:71:25
   |
71 | fn kcli_parse(kcmdline: &[u8]) -> io::Result<KcliArgs> {
   |                         ^^^^^                ^^^^^^^^ the same lifetime is hidden here
   |                         |
   |                         the lifetime is elided here
   |
   = help: the same lifetime is referred to in inconsistent ways, making the signature confusing
   = note: `#[warn(mismatched_lifetime_syntaxes)]` on by default
help: use `'_` for type paths
   |
71 | fn kcli_parse(kcmdline: &[u8]) -> io::Result<KcliArgs<'_>> {
   |                                                      ++++

I *think* we might be able to avoid this lifetime syntax by moving
(instead of borrowing) @kcmdline for the function call, but I don't know
how to do that without an extra type to wrap the array <shrug>.

Signed-off-by: David Disseldorp <ddiss@suse.de>
Check the hosts stty size when booting and pass to the VM via
rapido.stty=<rows>,<columns>. This allows us to avoid using the xterm
provided resize command for TIOCSWINSZ.

Link: rapido-linux#259
Signed-off-by: David Disseldorp <ddiss@suse.de>
For install binaries, we currently only check for elf dependencies if
any execute mode bits are set. This is not the case for libraries, where
mode flags are ignored.

In preparation for combining bin and lib gathering, drop the exec mode
flag check and collect elf dependencies unconditionally. I don't expect
performance to take much of a hit from this change, as we have a file
handle ready for archiving. Besides, include / data gathering can be
used for non-elf types.

It may make sense to revert this change if support for recursive
directory tree --install support is added. E.g. consider xfstests where
some ELF binaries are scattered amongst a large number of non-ELF
files, where many (but non-all) non-ELF files don't carry the exec mode
flag.

Signed-off-by: David Disseldorp <ddiss@suse.de>
This should allow us to more easily combine lib and bin gathering.
Search path determination is moved from the caller into path_stat, and
based on the GatherEnt type.

Signed-off-by: David Disseldorp <ddiss@suse.de>
These functions have always been very similar. With separate Lib and Bin
(Name) GatherEnt items it's a relatively straightforward merge.

Signed-off-by: David Disseldorp <ddiss@suse.de>
gather_archive_elfs() is now the only caller.

Signed-off-by: David Disseldorp <ddiss@suse.de>
archive_path already handles zero-length files fine, so fallback to it
if md.len is zero.

Signed-off-by: David Disseldorp <ddiss@suse.de>
The caller ignores all errors, so move error messaging closer to where
the errors occur and relocate some comments too.

Signed-off-by: David Disseldorp <ddiss@suse.de>
Commit 88fdb59 ("rapido-cut: drop exec mode req for bin elf_deps()")
removed this optimization, but the new merged lib/bin gathering
functionality makes it easy to add back cleanly.

Signed-off-by: David Disseldorp <ddiss@suse.de>
Manifests are never added as NameTry types.

Signed-off-by: David Disseldorp <ddiss@suse.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants