Skip to content

Commit a192370

Browse files
committed
ostree-ext: Serialize xattrs into tar stream as well
We really want this for coreos/rpm-ostree#5222 to be able to rebuild images from their container-synthesized rootfs. Really, the only xattr we don't want to emit in to the tar stream is security.selinux for now. Eventually we should try to switch to putting that into the tar stream too, but it needs more validation. Signed-off-by: Colin Walters <[email protected]>
1 parent 8534450 commit a192370

File tree

3 files changed

+171
-15
lines changed

3 files changed

+171
-15
lines changed

ostree-ext/src/fixture.rs

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ use crate::container::{Config, ExportOpts, ImageReference, Transport};
88
use crate::objectsource::{ObjectMeta, ObjectSourceMeta};
99
use crate::objgv::gv_dirtree;
1010
use crate::prelude::*;
11+
use crate::tar::SECURITY_SELINUX_XATTR_C;
1112
use crate::{gio, glib};
1213
use anyhow::{anyhow, Context, Result};
14+
use bootc_utils::CommandRunExt;
1315
use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
1416
use cap_std::fs::Dir;
1517
use cap_std_ext::cap_std;
@@ -25,6 +27,7 @@ use ocidir::oci_spec::image::ImageConfigurationBuilder;
2527
use once_cell::sync::Lazy;
2628
use regex::Regex;
2729
use std::borrow::Cow;
30+
use std::ffi::CString;
2831
use std::fmt::Write as _;
2932
use std::io::{self, Write};
3033
use std::ops::Add;
@@ -46,12 +49,19 @@ enum FileDefType {
4649
Directory,
4750
}
4851

52+
#[derive(Debug)]
53+
struct Xattr {
54+
key: CString,
55+
value: Box<[u8]>,
56+
}
57+
4958
#[derive(Debug)]
5059
pub struct FileDef {
5160
uid: u32,
5261
gid: u32,
5362
mode: u32,
5463
path: Cow<'static, Utf8Path>,
64+
xattrs: Box<[Xattr]>,
5565
ty: FileDefType,
5666
}
5767

@@ -66,9 +76,21 @@ impl TryFrom<&'static str> for FileDef {
6676
let name = parts.next().ok_or_else(|| anyhow!("Missing file name"))?;
6777
let contents = parts.next();
6878
let contents = move || contents.ok_or_else(|| anyhow!("Missing file contents: {}", value));
69-
if parts.next().is_some() {
70-
anyhow::bail!("Invalid filedef: {}", value);
71-
}
79+
let xattrs: Result<Vec<_>> = parts
80+
.map(|xattr| -> Result<Xattr> {
81+
let (k, v) = xattr
82+
.split_once('=')
83+
.ok_or_else(|| anyhow::anyhow!("Invalid xattr: {xattr}"))?;
84+
let mut k: Vec<u8> = k.to_owned().into();
85+
k.push(0);
86+
let r = Xattr {
87+
key: CString::from_vec_with_nul(k).unwrap(),
88+
value: Vec::from(v.to_owned()).into(),
89+
};
90+
Ok(r)
91+
})
92+
.collect();
93+
let xattrs = xattrs?.into();
7294
let ty = match tydef {
7395
"r" => FileDefType::Regular(contents()?.into()),
7496
"l" => FileDefType::Symlink(Cow::Borrowed(contents()?.into())),
@@ -80,6 +102,7 @@ impl TryFrom<&'static str> for FileDef {
80102
gid: 0,
81103
mode: 0o644,
82104
path: Cow::Borrowed(name.into()),
105+
xattrs,
83106
ty,
84107
})
85108
}
@@ -165,6 +188,7 @@ static OWNERS: Lazy<Vec<(Regex, &str)>> = Lazy::new(|| {
165188
("usr/lib/modules/.*/initramfs", "initramfs"),
166189
("usr/lib/modules", "kernel"),
167190
("usr/bin/(ba)?sh", "bash"),
191+
("usr/bin/arping", "arping"),
168192
("usr/lib.*/emptyfile.*", "bash"),
169193
("usr/bin/hardlink.*", "testlink"),
170194
("usr/etc/someconfig.conf", "someconfig"),
@@ -184,6 +208,7 @@ r usr/lib/modules/5.10.18-200.x86_64/initramfs this-is-an-initramfs
184208
m 0 0 755
185209
r usr/bin/bash the-bash-shell
186210
l usr/bin/sh bash
211+
r usr/bin/arping arping-binary security.capability=0sAAAAAgAgAAAAAAAAAAAAAAAAAAA=
187212
m 0 0 644
188213
# Some empty files
189214
r usr/lib/emptyfile
@@ -206,7 +231,7 @@ m 0 0 1755
206231
d tmp
207232
"## };
208233
pub const CONTENTS_CHECKSUM_V0: &str =
209-
"acc42fb5c796033f034941dc688643bf8beddfd9068d87165344d2b99906220a";
234+
"bd3d13c3059e63e6f8a3d6d046923ded730d90bd7a055c9ad93312111ea7d395";
210235
// 1 for ostree commit, 2 for max frequency packages, 3 as empty layer
211236
pub const LAYERS_V0_LEN: usize = 3usize;
212237
pub const PKGS_V0_LEN: usize = 7usize;
@@ -267,11 +292,10 @@ impl SeLabel {
267292
}
268293

269294
pub fn xattrs(&self) -> Vec<(&[u8], &[u8])> {
270-
vec![(b"security.selinux\0", self.to_str().as_bytes())]
271-
}
272-
273-
pub fn new_xattrs(&self) -> glib::Variant {
274-
self.xattrs().to_variant()
295+
vec![(
296+
SECURITY_SELINUX_XATTR_C.to_bytes_with_nul(),
297+
self.to_str().as_bytes(),
298+
)]
275299
}
276300
}
277301

@@ -286,7 +310,7 @@ pub fn create_dirmeta(path: &Utf8Path, selinux: bool) -> glib::Variant {
286310
} else {
287311
None
288312
};
289-
let xattrs = label.map(|v| v.new_xattrs());
313+
let xattrs = label.map(|v| v.xattrs().to_variant());
290314
ostree::create_directory_metadata(&finfo, xattrs.as_ref())
291315
}
292316

@@ -632,7 +656,18 @@ impl Fixture {
632656
} else {
633657
None
634658
};
635-
let xattrs = label.map(|v| v.new_xattrs());
659+
let mut xattrs = label.as_ref().map(|v| v.xattrs()).unwrap_or_default();
660+
xattrs.extend(
661+
def.xattrs
662+
.iter()
663+
.map(|xattr| (xattr.key.as_bytes_with_nul(), &xattr.value[..])),
664+
);
665+
let xattrs = if xattrs.is_empty() {
666+
None
667+
} else {
668+
xattrs.sort_by(|a, b| a.0.cmp(b.0));
669+
Some(xattrs.to_variant())
670+
};
636671
let xattrs = xattrs.as_ref();
637672
let checksum = match &def.ty {
638673
FileDefType::Regular(contents) => self
@@ -724,6 +759,21 @@ impl Fixture {
724759
gio::Cancellable::NONE,
725760
)?;
726761

762+
// Verify that this is what is expected.
763+
let commit_object = self.srcrepo.load_commit(&commit)?.0;
764+
let content_checksum = ostree::commit_get_content_checksum(&commit_object).unwrap();
765+
if content_checksum != CONTENTS_CHECKSUM_V0 {
766+
// Only spew this once
767+
static DUMP_OSTREE: std::sync::Once = std::sync::Once::new();
768+
DUMP_OSTREE.call_once(|| {
769+
let _ = Command::new("ostree")
770+
.arg(format!("--repo={}", self.path.join("src/repo")))
771+
.args(["ls", "-X", "-C", "-R", commit.as_str()])
772+
.run();
773+
});
774+
}
775+
assert_eq!(CONTENTS_CHECKSUM_V0, content_checksum.as_str());
776+
727777
Ok(())
728778
}
729779

ostree-ext/src/tar/export.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,23 @@ use ostree::gio;
1313
use std::borrow::Borrow;
1414
use std::borrow::Cow;
1515
use std::collections::HashSet;
16+
use std::ffi::CStr;
1617
use std::io::BufReader;
1718

1819
/// The repository mode generated by a tar export stream.
1920
pub const BARE_SPLIT_XATTRS_MODE: &str = "bare-split-xattrs";
2021

22+
/// The SELinux xattr. Because the ostree xattrs require an embedded NUL, we
23+
/// store that version as a constant.
24+
pub(crate) const SECURITY_SELINUX_XATTR_C: &CStr = c"security.selinux";
25+
/// Then derive a string version (without the NUL) from the above.
26+
pub(crate) const SECURITY_SELINUX_XATTR: &str = const {
27+
match SECURITY_SELINUX_XATTR_C.to_str() {
28+
Ok(r) => r,
29+
Err(_) => unreachable!(),
30+
}
31+
};
32+
2133
// This is both special in the tar stream *and* it's in the ostree commit.
2234
const SYSROOT: &str = "sysroot";
2335
// This way the default ostree -> sysroot/ostree symlink works.
@@ -379,6 +391,32 @@ impl<'a, W: std::io::Write> OstreeTarWriter<'a, W> {
379391
Ok(true)
380392
}
381393

394+
/// Append all xattrs to the tar stream *except* security.selinux, because
395+
/// that one doesn't become visible in `podman run` anyways, so we couldn't
396+
/// rely on it in some cases.
397+
/// https://github.com/containers/storage/blob/0d4a8d2aaf293c9f0464b888d932ab5147a284b9/pkg/archive/archive.go#L85
398+
#[context("Writing tar xattrs")]
399+
fn append_tarstream_xattrs(&mut self, xattrs: &glib::Variant) -> Result<()> {
400+
let v = xattrs.data_as_bytes();
401+
let v = v.try_as_aligned().unwrap();
402+
let v = gvariant::gv!("a(ayay)").cast(v);
403+
let mut pax_extensions = Vec::new();
404+
for entry in v {
405+
let (k, v) = entry.to_tuple();
406+
let k = CStr::from_bytes_with_nul(k).unwrap();
407+
let k = k
408+
.to_str()
409+
.with_context(|| format!("Found non-UTF8 xattr: {k:?}"))?;
410+
if k == SECURITY_SELINUX_XATTR {
411+
continue;
412+
}
413+
pax_extensions.push((format!("SCHILY.xattr.{k}"), v));
414+
}
415+
self.out
416+
.append_pax_extensions(pax_extensions.iter().map(|(k, v)| (k.as_str(), *v)))?;
417+
Ok(())
418+
}
419+
382420
/// Write a content object, returning the path/header that should be used
383421
/// as a hard link to it in the target path. This matches how ostree checkouts work.
384422
fn append_content(&mut self, checksum: &str) -> Result<(Utf8PathBuf, tar::Header)> {
@@ -402,6 +440,7 @@ impl<'a, W: std::io::Write> OstreeTarWriter<'a, W> {
402440
// refer to. Otherwise the importing logic won't have the xattrs available
403441
// when importing file content.
404442
self.append_ostree_xattrs(checksum, &xattrs)?;
443+
self.append_tarstream_xattrs(&xattrs)?;
405444

406445
if let Some(instream) = instream {
407446
ensure!(meta.file_type() == gio::FileType::Regular);

ostree-ext/tests/it/main.rs

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use camino::Utf8Path;
55
use cap_std::fs::{Dir, DirBuilder, DirBuilderExt};
66
use cap_std_ext::cap_std;
77
use containers_image_proxy::oci_spec;
8+
use gvariant::aligned_bytes::TryAsAligned;
9+
use gvariant::{Marker, Structure};
810
use oci_image::ImageManifest;
911
use oci_spec::image as oci_image;
1012
use ocidir::oci_spec::image::{Arch, DigestAlgorithm};
@@ -221,6 +223,7 @@ struct TarExpected {
221223
path: &'static str,
222224
etype: tar::EntryType,
223225
mode: u32,
226+
should_have_security_capability: bool,
224227
}
225228

226229
#[allow(clippy::from_over_into)]
@@ -230,6 +233,19 @@ impl Into<TarExpected> for (&'static str, tar::EntryType, u32) {
230233
path: self.0,
231234
etype: self.1,
232235
mode: self.2,
236+
should_have_security_capability: false,
237+
}
238+
}
239+
}
240+
241+
#[allow(clippy::from_over_into)]
242+
impl Into<TarExpected> for (&'static str, tar::EntryType, u32, bool) {
243+
fn into(self) -> TarExpected {
244+
TarExpected {
245+
path: self.0,
246+
etype: self.1,
247+
mode: self.2,
248+
should_have_security_capability: self.3,
233249
}
234250
}
235251
}
@@ -244,7 +260,7 @@ fn validate_tar_expected<T: std::io::Read>(
244260
let mut seen_paths = HashSet::new();
245261
// Verify we're injecting directories, fixes the absence of `/tmp` in our
246262
// images for example.
247-
for entry in entries {
263+
for mut entry in entries {
248264
if expected.is_empty() {
249265
return Ok(());
250266
}
@@ -265,6 +281,21 @@ fn validate_tar_expected<T: std::io::Read>(
265281
header.entry_type(),
266282
entry_path
267283
);
284+
if exp.should_have_security_capability {
285+
let pax = entry
286+
.pax_extensions()?
287+
.ok_or_else(|| anyhow::anyhow!("Missing pax extensions for {entry_path}"))?;
288+
let mut found = false;
289+
for ent in pax {
290+
let ent = ent?;
291+
if ent.key_bytes() != b"SCHILY.xattr.security.capability" {
292+
continue;
293+
}
294+
found = true;
295+
break;
296+
}
297+
assert!(found, "Expected security.capability in {entry_path}");
298+
}
268299
}
269300
}
270301

@@ -312,6 +343,9 @@ fn common_tar_contents_all() -> impl Iterator<Item = TarExpected> {
312343
]
313344
.into_iter()
314345
.map(Into::into)
346+
.chain(std::iter::once(
347+
("sysroot/ostree/repo/objects/b0/48a4f451e9fdfaaec911c1fe07d5d1d39be02f932b827c25458d3b15ae589e.file", Regular, 0o755, true).into(),
348+
))
315349
}
316350

317351
/// Validate metadata (prelude) in a v1 tar.
@@ -335,6 +369,7 @@ fn test_tar_export_structure() -> Result<()> {
335369
let fixture = Fixture::new_v1()?;
336370

337371
let src_tar = fixture.export_tar()?;
372+
std::fs::copy(fixture.path.join(src_tar), "/tmp/test.tar").unwrap();
338373
let mut src_tar = fixture
339374
.dir
340375
.open(src_tar)
@@ -932,7 +967,7 @@ async fn test_container_chunked() -> Result<()> {
932967
.created_by()
933968
.as_ref()
934969
.unwrap(),
935-
"8 components"
970+
"9 components"
936971
);
937972
}
938973
let import = imp.import(prep).await.context("Init pull derived").unwrap();
@@ -991,9 +1026,9 @@ r usr/bin/bash bash-v0
9911026
assert!(second.0.commit.is_none());
9921027
assert_eq!(
9931028
first.1,
994-
"ostree export of commit fe4ba8bbd8f61a69ae53cde0dd53c637f26dfbc87717b2e71e061415d931361e"
1029+
"ostree export of commit 6e6afc49d902daa2456c858818e0ad8bf9afe79cdcca738c5676c0e175c1def1"
9951030
);
996-
assert_eq!(second.1, "8 components");
1031+
assert_eq!(second.1, "9 components");
9971032

9981033
assert_eq!(store::list_images(fixture.destrepo()).unwrap().len(), 1);
9991034
let n = store::count_layer_references(fixture.destrepo())? as i64;
@@ -1785,6 +1820,36 @@ async fn test_container_xattr() -> Result<()> {
17851820
_ => unreachable!(),
17861821
};
17871822

1823+
// Verify security.capability is in the ostree commit
1824+
let arping = "/usr/bin/arping";
1825+
{
1826+
let ostree_root = fixture
1827+
.srcrepo()
1828+
.read_commit(fixture.testref(), gio::Cancellable::NONE)?
1829+
.0;
1830+
let arping_ostree = ostree_root.resolve_relative_path(arping);
1831+
assert_eq!(
1832+
arping_ostree.query_file_type(
1833+
gio::FileQueryInfoFlags::NOFOLLOW_SYMLINKS,
1834+
gio::Cancellable::NONE
1835+
),
1836+
gio::FileType::Regular
1837+
);
1838+
let arping_ostree = arping_ostree.downcast_ref::<ostree::RepoFile>().unwrap();
1839+
let arping_ostree_xattrs = arping_ostree.xattrs(gio::Cancellable::NONE)?;
1840+
let v = arping_ostree_xattrs.data_as_bytes();
1841+
let v = v.try_as_aligned().unwrap();
1842+
let v = gvariant::gv!("a(ayay)").cast(v);
1843+
assert!(v
1844+
.iter()
1845+
.find(|entry| {
1846+
let k = entry.to_tuple().0;
1847+
let k = std::ffi::CStr::from_bytes_with_nul(k).unwrap();
1848+
k.to_str().ok() == Some("security.capability")
1849+
})
1850+
.is_some());
1851+
}
1852+
17881853
// Build a derived image
17891854
let derived_path = &fixture.path.join("derived.oci");
17901855
oci_clone(basepath, derived_path).await?;
@@ -1832,6 +1897,8 @@ async fn test_container_xattr() -> Result<()> {
18321897
)
18331898
.read()?;
18341899
assert!(out.contains("'user.foo', [byte 0x62, 0x61, 0x72]"));
1900+
let out = cmd!(sh, "ostree --repo=dest/repo ls -X {merge_commit} {arping}").read()?;
1901+
assert!(out.contains("'security.capability'"));
18351902

18361903
Ok(())
18371904
}

0 commit comments

Comments
 (0)