Skip to content

Commit 2aed31d

Browse files
authored
Merge pull request #1032 from cgwalters/export-xattrs
ostree-ext: Serialize xattrs into tar stream as well
2 parents e950142 + a192370 commit 2aed31d

File tree

3 files changed

+178
-18
lines changed

3 files changed

+178
-18
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: 46 additions & 3 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.
@@ -347,9 +359,13 @@ impl<'a, W: std::io::Write> OstreeTarWriter<'a, W> {
347359
Ok(())
348360
}
349361

350-
/// Export xattrs to the tar stream, return whether content was written.
362+
/// Export xattrs in ostree-container style format, which is a .xattrs file.
363+
/// This is different from xattrs which may appear as e.g. PAX metadata, which we don't use
364+
/// at the moment.
365+
///
366+
/// Return whether content was written.
351367
#[context("Writing xattrs")]
352-
fn append_xattrs(&mut self, checksum: &str, xattrs: &glib::Variant) -> Result<bool> {
368+
fn append_ostree_xattrs(&mut self, checksum: &str, xattrs: &glib::Variant) -> Result<bool> {
353369
let xattrs_data = xattrs.data_as_bytes();
354370
let xattrs_data = xattrs_data.as_ref();
355371

@@ -375,6 +391,32 @@ impl<'a, W: std::io::Write> OstreeTarWriter<'a, W> {
375391
Ok(true)
376392
}
377393

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+
378420
/// Write a content object, returning the path/header that should be used
379421
/// as a hard link to it in the target path. This matches how ostree checkouts work.
380422
fn append_content(&mut self, checksum: &str) -> Result<(Utf8PathBuf, tar::Header)> {
@@ -397,7 +439,8 @@ impl<'a, W: std::io::Write> OstreeTarWriter<'a, W> {
397439
// The xattrs objects need to be exported before the regular object they
398440
// refer to. Otherwise the importing logic won't have the xattrs available
399441
// when importing file content.
400-
self.append_xattrs(checksum, &xattrs)?;
442+
self.append_ostree_xattrs(checksum, &xattrs)?;
443+
self.append_tarstream_xattrs(&xattrs)?;
401444

402445
if let Some(instream) = instream {
403446
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)