]> git.ipfire.org Git - thirdparty/dracut-ng.git/commitdiff
feat(cpio): zero device major/minor numbers
authorDavid Disseldorp <ddiss@suse.de>
Wed, 8 Oct 2025 00:59:57 +0000 (02:59 +0200)
committerNeal Gompa (ニール・ゴンパ) <ngompa13@gmail.com>
Mon, 24 Nov 2025 15:16:55 +0000 (10:16 -0500)
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.

src/dracut-cpio/src/main.rs

index 2dfa92304850523b2fd02234d059ad864d1d796c..4dacb0f938c6646fdeebbbb6aac1fd66157799ba 100644 (file)
@@ -45,16 +45,12 @@ struct HardlinkPath {
 struct HardlinkState {
     names: Vec<HardlinkPath>,
     source_ino: u64,
+    source_dev: u64,
     mapped_ino: u32,
     nlink: u32,
     seen: u32,
 }
 
-struct DevState {
-    dev: u64,
-    hls: Vec<HardlinkState>,
-}
-
 struct ArchiveProperties {
     // first inode number to use. @ArchiveState.ino increments from this.
     initial_ino: u32,
@@ -105,44 +101,25 @@ impl ArchiveProperties {
 }
 
 struct ArchiveState {
-    // 2d dev + inode vector serves two purposes:
-    // - dev index provides reproducible major,minor values
-    // - inode@dev provides hardlink state tracking
-    ids: Vec<DevState>,
+    // dev + inode provides hardlink state tracking
+    hls: Vec<HardlinkState>,
     // offset from the start of this archive
     off: u64,
     // next mapped inode number, used instead of source file inode numbers to
-    // ensure reproducibility. XXX: should track inode per mapped dev?
+    // ensure reproducibility. Inode numbers all share the same dev (major=0
+    // minor=0) namespace.
     ino: u32,
 }
 
 impl ArchiveState {
     pub fn new(ino_start: u32) -> ArchiveState {
         ArchiveState {
-            ids: Vec::new(),
+            hls: Vec::new(),
             off: 0,
             ino: ino_start,
         }
     }
 
-    // lookup or create DevState for @dev. Return @major/@minor based on index
-    pub fn dev_seen(&mut self, dev: u64) -> Option<(u32, u32)> {
-        let index: u64 = match self.ids.iter().position(|i| i.dev == dev) {
-            Some(idx) => idx.try_into().ok()?,
-            None => {
-                self.ids.push(DevState {
-                    dev: dev,
-                    hls: Vec::new(),
-                });
-                (self.ids.len() - 1).try_into().ok()?
-            }
-        };
-
-        let major: u32 = (index >> 32).try_into().unwrap();
-        let minor: u32 = (index & u64::from(u32::MAX)).try_into().unwrap();
-        Some((major, minor))
-    }
-
     // Check whether we've already seen this hardlink's dev/inode combination.
     // If already seen, fill the existing mapped_ino.
     // Return true if this entry has been deferred (seen != nlinks)
@@ -150,8 +127,6 @@ impl ArchiveState {
         &mut self,
         props: &ArchiveProperties,
         mut writer: W,
-        major: u32,
-        minor: u32,
         md: fs::Metadata,
         inpath: &Path,
         outpath: &Path,
@@ -159,22 +134,19 @@ impl ArchiveState {
         mapped_nlink: &mut Option<u32>,
     ) -> std::io::Result<bool> {
         assert!(md.nlink() > 1);
-        let index = u64::from(major) << 32 | u64::from(minor);
-        // reverse index->major/minor conversion that was just done
-        let devstate: &mut DevState = &mut self.ids[index as usize];
-        let (_index, hl) = match devstate
-            .hls
+        let (_index, hl) = match self.hls
             .iter_mut()
             .enumerate()
-            .find(|(_, hl)| hl.source_ino == md.ino())
+            .find(|(_, hl)| hl.source_ino == md.ino() && hl.source_dev == md.dev())
         {
             Some(hl) => hl,
             None => {
-                devstate.hls.push(HardlinkState {
+                self.hls.push(HardlinkState {
                     names: vec![HardlinkPath {
                         infile: inpath.to_path_buf(),
                         outfile: outpath.to_path_buf(),
                     }],
+                    source_dev: md.dev(),
                     source_ino: md.ino(),
                     mapped_ino: self.ino,
                     nlink: md.nlink().try_into().unwrap(), // pre-checked
@@ -243,8 +215,8 @@ impl ArchiveState {
                     None => md.mtime().try_into().unwrap(),
                 },
                 filesize = 0,
-                major = major,
-                minor = minor,
+                major = 0,
+                minor = 0,
                 rmajor = 0,
                 rminor = 0,
                 namesize = fname.len() + 1,
@@ -271,7 +243,7 @@ impl ArchiveState {
         // GNU cpio: if a name is given multiple times, exceeding nlink, then
         // subsequent names continue to be packed (with a repeat data segment),
         // using the same mapped inode.
-        dout!("resetting hl at index {}", index);
+        dout!("resetting hl with dev {} ino {}", hl.source_dev, hl.source_ino);
         hl.seen = 0;
         hl.names.clear();
 
@@ -319,11 +291,6 @@ fn archive_path<W: Seek + Write>(
     };
     dout!("archiving {} with mode {:o}", outpath.display(), md.mode());
 
-    let (major, minor) = match state.dev_seen(md.dev()) {
-        Some((maj, min)) => (maj, min),
-        None => return Err(io::Error::new(io::ErrorKind::Other, "failed to map dev")),
-    };
-
     if md.nlink() > u32::MAX as u64 {
         return Err(io::Error::new(
             io::ErrorKind::InvalidInput,
@@ -386,8 +353,6 @@ fn archive_path<W: Seek + Write>(
             let deferred = state.hardlink_seen(
                 &props,
                 &mut writer,
-                major,
-                minor,
                 md.clone(),
                 &inpath,
                 outpath,
@@ -455,8 +420,8 @@ fn archive_path<W: Seek + Write>(
         },
         mtime = mtime,
         filesize = datalen,
-        major = major,
-        minor = minor,
+        major = 0,
+        minor = 0,
         rmajor = rmajor,
         rminor = rminor,
         namesize = fname.len() + 1 + data_align_seek as usize,
@@ -515,27 +480,25 @@ fn archive_flush_unseen_hardlinks<W: Write + Seek>(
     mut writer: W,
 ) -> std::io::Result<()> {
     let mut deferred_inpaths: Vec<PathBuf> = Vec::new();
-    for id in state.ids.iter_mut() {
-        for hl in id.hls.iter_mut() {
-            if hl.seen == 0 || hl.seen == hl.nlink {
-                dout!("HardlinkState complete with seen {}", hl.seen);
-                continue;
-            }
-            dout!(
-                "pending HardlinkState with seen {} != nlinks {}",
-                hl.seen,
-                hl.nlink
-            );
+    for hl in state.hls.iter_mut() {
+        if hl.seen == 0 || hl.seen == hl.nlink {
+            dout!("HardlinkState complete with seen {}", hl.seen);
+            continue;
+        }
+        dout!(
+            "pending HardlinkState with seen {} != nlinks {}",
+            hl.seen,
+            hl.nlink
+        );
 
-            while hl.names.len() > 0 {
-                let path = hl.names.pop().unwrap();
-                deferred_inpaths.push(path.infile);
-            }
-            // ensure that data segment gets added on archive_path recall
-            hl.nlink = hl.seen;
-            hl.seen = 0;
-            // existing allocated inode should be used
+        while hl.names.len() > 0 {
+            let path = hl.names.pop().unwrap();
+            deferred_inpaths.push(path.infile);
         }
+        // ensure that data segment gets added on archive_path recall
+        hl.nlink = hl.seen;
+        hl.seen = 0;
+        // existing allocated inode should be used
     }
 
     if deferred_inpaths.len() > 0 {