Skip to content

Commit 2f6a8ae

Browse files
committed
wip: add test for concurrent image layer threading
1 parent 58b063d commit 2f6a8ae

1 file changed

Lines changed: 228 additions & 0 deletions

File tree

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
// Copyright (c) 2026 The image-rs Authors
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
//! Reproduces the trusted image store scenario from
6+
//! <https://github.com/kata-containers/kata-containers/issues/12721>:
7+
//! concurrent layer unpack on ext4 over LUKS2 + dm-integrity.
8+
//!
9+
//! Requires root, `cryptsetup`, `mkfs.ext4`, and loop device support.
10+
//! Run explicitly:
11+
//! `cargo test -p image-rs pull_image_on_luks2_integrity_parallel_layers -- --ignored --nocapture`
12+
13+
#[cfg(not(target_os = "linux"))]
14+
#[test]
15+
fn pull_luks_integrity_linux_not_run_on_non_linux() {}
16+
17+
#[cfg(target_os = "linux")]
18+
use std::io::Write;
19+
#[cfg(target_os = "linux")]
20+
use std::path::Path;
21+
#[cfg(target_os = "linux")]
22+
use std::process::Command;
23+
#[cfg(target_os = "linux")]
24+
use std::time::{SystemTime, UNIX_EPOCH};
25+
26+
#[cfg(target_os = "linux")]
27+
use loopdev::LoopControl;
28+
#[cfg(target_os = "linux")]
29+
use nix::mount::{mount, MsFlags};
30+
#[cfg(target_os = "linux")]
31+
use tempfile::NamedTempFile;
32+
33+
#[cfg(target_os = "linux")]
34+
const BACKING_SIZE: u64 = 512 * 1024 * 1024;
35+
#[cfg(target_os = "linux")]
36+
const PASSPHRASE: &[u8] = b"image-rs-luks-integrity-test";
37+
/// Image with multiple layers so `buffer_unordered` can run concurrently.
38+
#[cfg(target_os = "linux")]
39+
const TEST_IMAGE: &str = "ghcr.io/confidential-containers/test-container-image-rs:busybox-gzip";
40+
41+
#[cfg(target_os = "linux")]
42+
fn run_cryptsetup(args: &[&str]) -> std::io::Result<()> {
43+
let status = Command::new("cryptsetup").args(args).status()?;
44+
if !status.success() {
45+
return Err(std::io::Error::new(
46+
std::io::ErrorKind::Other,
47+
format!("cryptsetup {:?} failed: {status}", args.first()),
48+
));
49+
}
50+
Ok(())
51+
}
52+
53+
#[cfg(target_os = "linux")]
54+
fn which_cryptsetup() -> bool {
55+
Command::new("which")
56+
.arg("cryptsetup")
57+
.output()
58+
.map(|o| o.status.success())
59+
.unwrap_or(false)
60+
}
61+
62+
#[cfg(target_os = "linux")]
63+
struct LuksIntegrityMount {
64+
mount_point: tempfile::TempDir,
65+
mapper_name: String,
66+
loop_dev: loopdev::LoopDevice,
67+
}
68+
69+
#[cfg(target_os = "linux")]
70+
impl LuksIntegrityMount {
71+
fn new() -> Result<Self, String> {
72+
if !nix::unistd::Uid::effective().is_root() {
73+
return Err("must run as root".into());
74+
}
75+
if !which_cryptsetup() {
76+
return Err("cryptsetup not found in PATH".into());
77+
}
78+
79+
let backing = NamedTempFile::new().map_err(|e| e.to_string())?;
80+
backing
81+
.as_file()
82+
.set_len(BACKING_SIZE)
83+
.map_err(|e| e.to_string())?;
84+
85+
let loop_control = LoopControl::open().map_err(|e| e.to_string())?;
86+
let loop_dev = loop_control.next_free().map_err(|e| e.to_string())?;
87+
loop_dev
88+
.with()
89+
.autoclear(true)
90+
.attach(
91+
backing
92+
.path()
93+
.to_str()
94+
.ok_or_else(|| "backing path".to_string())?,
95+
)
96+
.map_err(|e| e.to_string())?;
97+
98+
let loop_path = loop_dev
99+
.path()
100+
.ok_or_else(|| "loop device path".to_string())?;
101+
let loop_path_str = loop_path
102+
.to_str()
103+
.ok_or_else(|| "loop path utf-8".to_string())?;
104+
105+
let nanos = SystemTime::now()
106+
.duration_since(UNIX_EPOCH)
107+
.map(|d| d.as_nanos())
108+
.unwrap_or(0);
109+
let mapper_name = format!("img_rs_luks_{}_{}", std::process::id(), nanos);
110+
111+
let mut keyfile = NamedTempFile::new().map_err(|e| e.to_string())?;
112+
keyfile
113+
.write_all(PASSPHRASE)
114+
.map_err(|e| e.to_string())?;
115+
keyfile.flush().map_err(|e| e.to_string())?;
116+
let key_path = keyfile
117+
.path()
118+
.to_str()
119+
.ok_or_else(|| "key path".to_string())?;
120+
121+
// Align with confidential-data-hub LUKS2 + integrity (sector 4096, hmac-sha256).
122+
run_cryptsetup(&[
123+
"luksFormat",
124+
"--type",
125+
"luks2",
126+
"--integrity",
127+
"hmac-sha256",
128+
"--sector-size",
129+
"4096",
130+
"--batch-mode",
131+
"--pbkdf",
132+
"pbkdf2",
133+
loop_path_str,
134+
"--key-file",
135+
key_path,
136+
])
137+
.map_err(|e| e.to_string())?;
138+
139+
// Match CDH `CryptActivate::NO_JOURNAL` for dm-integrity.
140+
run_cryptsetup(&[
141+
"open",
142+
"--type",
143+
"luks2",
144+
"--integrity-no-journal",
145+
loop_path_str,
146+
&mapper_name,
147+
"--key-file",
148+
key_path,
149+
])
150+
.map_err(|e| e.to_string())?;
151+
152+
let mapper_path = format!("/dev/mapper/{mapper_name}");
153+
let mkfs = Command::new("mkfs.ext4")
154+
.args(["-F", &mapper_path])
155+
.output()
156+
.map_err(|e| e.to_string())?;
157+
if !mkfs.status.success() {
158+
return Err(format!(
159+
"mkfs.ext4: {}",
160+
String::from_utf8_lossy(&mkfs.stderr)
161+
));
162+
}
163+
164+
let mount_point = tempfile::tempdir().map_err(|e| e.to_string())?;
165+
nix::mount::mount(
166+
Some(mapper_path.as_str()),
167+
mount_point.path(),
168+
Some("ext4"),
169+
MsFlags::empty(),
170+
None::<&str>,
171+
)
172+
.map_err(|e| format!("mount ext4: {e}"))?;
173+
174+
Ok(Self {
175+
mount_point,
176+
mapper_name,
177+
loop_dev,
178+
})
179+
}
180+
181+
fn work_dir(&self) -> &Path {
182+
self.mount_point.path()
183+
}
184+
}
185+
186+
#[cfg(target_os = "linux")]
187+
impl Drop for LuksIntegrityMount {
188+
fn drop(&mut self) {
189+
let _ = nix::mount::umount(self.mount_point.path());
190+
let _ = Command::new("cryptsetup")
191+
.args(["close", &self.mapper_name])
192+
.status();
193+
let _ = self.loop_dev.detach();
194+
}
195+
}
196+
197+
#[cfg(target_os = "linux")]
198+
#[tokio::test]
199+
#[serial_test::serial]
200+
#[ignore = "requires root, cryptsetup, mkfs.ext4, loop; run with: cargo test -p image-rs pull_image_on_luks2_integrity_parallel_layers -- --ignored"]
201+
async fn pull_image_on_luks2_integrity_parallel_layers() {
202+
let store = LuksIntegrityMount::new().expect("setup LUKS+integrity mount");
203+
204+
let work_dir = store.work_dir().to_path_buf();
205+
206+
let mut image_client = image_rs::builder::ClientBuilder::default()
207+
.work_dir(work_dir.clone())
208+
.max_concurrent_layer_downloads_per_image(3)
209+
.build()
210+
.await
211+
.expect("build ImageClient");
212+
213+
let bundle_dir = tempfile::tempdir().unwrap();
214+
image_client
215+
.pull_image(TEST_IMAGE, bundle_dir.path(), &None, &None)
216+
.await
217+
.unwrap_or_else(|e| panic!("pull_image on LUKS+integrity failed: {e:#}"));
218+
219+
let busybox = bundle_dir.path().join("rootfs/bin/busybox");
220+
assert!(
221+
busybox.exists(),
222+
"expected busybox at {}",
223+
busybox.display()
224+
);
225+
226+
let mounted_rootfs = bundle_dir.path().join("rootfs");
227+
let _ = nix::mount::umount(&mounted_rootfs);
228+
}

0 commit comments

Comments
 (0)