From: David Disseldorp Date: Wed, 8 Oct 2025 00:59:57 +0000 (+0200) Subject: feat(cpio): zero device major/minor numbers X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0bdecd925fbaebebfea96af97a74257fdbf13af4;p=thirdparty%2Fdracut-ng.git feat(cpio): zero device major/minor numbers 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. --- diff --git a/src/dracut-cpio/src/main.rs b/src/dracut-cpio/src/main.rs index 2dfa92304..4dacb0f93 100644 --- a/src/dracut-cpio/src/main.rs +++ b/src/dracut-cpio/src/main.rs @@ -45,16 +45,12 @@ struct HardlinkPath { struct HardlinkState { names: Vec, source_ino: u64, + source_dev: u64, mapped_ino: u32, nlink: u32, seen: u32, } -struct DevState { - dev: u64, - hls: Vec, -} - 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, + // dev + inode provides hardlink state tracking + hls: Vec, // 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, ) -> std::io::Result { 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( }; 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( let deferred = state.hardlink_seen( &props, &mut writer, - major, - minor, md.clone(), &inpath, outpath, @@ -455,8 +420,8 @@ fn archive_path( }, 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( mut writer: W, ) -> std::io::Result<()> { let mut deferred_inpaths: Vec = 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 {