Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Oxidizing rpm-ostree

Luca Bruno
September 26, 2022

Oxidizing rpm-ostree

Luca Bruno

September 26, 2022
Tweet

More Decks by Luca Bruno

Other Decks in Technology

Transcript

  1. 04/10/2022 Oxidizing rpm-ostree 2 $ whoami “Computer Engineer and enthusiast

    FLOSS supporter” • CoreOS / Red Hat • Principal Software Engineer • Working on Linux userland components • Previously: security researcher/engineer @lucabruno | [email protected] | github.com/lucab
  2. 04/10/2022 Oxidizing rpm-ostree 3 Overview • Introduction: ◦ RPMs and

    OSTree • Journey into C & Rust land: ◦ glib-rs ◦ ostree-rs ◦ cxx-rs • Putting everything together: ◦ Rust in rpm-ostree
  3. 04/10/2022 Oxidizing rpm-ostree 5 Problem scope Making an image-based (also

    called “hermetic”) Linux distribution. In practice, that means assembling softwares from many source projects into self-contained binary OS images. In our case: • Source tarballs RPM packages OSTree commits Image artifacts
  4. 04/10/2022 Oxidizing rpm-ostree 6 RPM Package Manager (RPM) A well-established

    format for software packages. RPM supports: • carrying binary data • declaring dependencies between packages • running pre- and post-install custom scripts It originated in 1995 at Red Hat, and it’s still used by Red Hat Enterprise Linux (RHEL). Over time it got adopted by many other distributions, like Fedora and OpenSUSE.
  5. 04/10/2022 Oxidizing rpm-ostree 7 OSTree It supports storing OS content

    in multiple local branches, incrementally pulling from remote repositories. OSTree keeps a versioned history of changes, and on each boot it can checkout a specific commit. That allows transactional upgrades and rollbacks of the OS, without multiple A/B partitions. A “git-like” manager for bootable filesystem trees.
  6. 04/10/2022 Oxidizing rpm-ostree 8 rpm-ostree A project that bridges together

    RPM and OStree, in order to build an hermetic OS. • Input: a set of binary packages (e.g. pre-built RPMs from RHEL or Fedora) • Output: an OSTree commit (e.g. a specific version of RHEL CoreOS or Fedora SilverBlue) It is made of several components: • a client/daemon part, running on the host • a standalone CLI part, running in the containerized build-pipeline
  7. 04/10/2022 Oxidizing rpm-ostree 9 Our plans as rpm-ostree developers: •

    (mostly) stop writing new code in C • implement future logic directly in Rust • gradually reduce the amount of C/C++ lines Goals
  8. 04/10/2022 Oxidizing rpm-ostree 12 let mut cfg_data = String::new(); cfg_data.read_to_string(&mut

    bufrd)?; let kf = glib::KeyFile::new(); kf.load_from_data( &cfg_data, glib::KeyFileFlags::NONE)?; let is_enabled = kf.boolean( “core”, “enabled”)?; Step 1: glib-rs glib is a multi-purpose C library offering a suite of data-structures (hashmaps, lists, etc.), OOP helpers and utilities. Both ostree and rpm-ostree heavily use glib for most of their C logic. Gnome developers have enthusiastically embraced Rust, and maintain glib-rs bindings (and many more) under the gtk-rs umbrella.
  9. 04/10/2022 Oxidizing rpm-ostree 13 /* ostree_repo_commit_modifier_new: * * @flags: Control

    options for filter * @commit_filter: (allow-none): * Function to inspect individual files * @user_data: (allow-none): User data * @destroy_notify: A #GDestroyNotify * * Returns: (transfer full): * A new commit modifier. */ OstreeRepoCommitModifier * ostree_repo_commit_modifier_new (...) { ... } Step 2: gir-rs Binding C libraries from multiple language is a tedious job. In Gnome land, developers invented an Intermediate Representation (named GIR) to describe their public APIs. Bindings can be auto-generated from these GIR descriptions. For Rust, gir-rs takes care of that.
  10. 04/10/2022 Oxidizing rpm-ostree 14 impl Repo { #[doc(alias = "ostree_repo_new")]

    pub fn new<P: IsA<gio::File>>(path: &P) -> Repo { unsafe { from_glib_full(ffi::ostree_repo_new( path.as_ref().to_glib_none().0)) } } } Step 3: ostree-rs libostree is a C library built on top of glib, hosting the core ostree logic. It comes with a GIR description of the full API surface. Through gir-rs tooling, we generate two Rust crates: • ostree-sys (low-level unsafe bindings) • ostree (ergonomic interface to ostree logic) fn new_temp_with_mode( repo_mode: ostree::RepoMode) -> Result<ostree::Repo> { let dir = tempfile::tempdir()?; let repo = ostree::Repo::new_for_path( dir.path()); repo.create(repo_mode, NONE_CANCELLABLE) }
  11. 04/10/2022 Oxidizing rpm-ostree 15 /// Given an OSTree commit and

    an IMA /// configuration, generate a new commit /// object with IMA signatures. pub fn ima_sign(repo: &ostree::Repo, ostree_ref: &str, opts: &ImaOpts) -> Result<String> { let writer = &mut CommitRewriter::new( repo, opts)?; writer.map_commit(ostree_ref) } Step 4: ostree-rs-ext Over time we started adding new optional/experimental features to ostree logic (e.g. IMA hashing). This is new pure-Rust code, without unsafe bits. ostree-ext largely relies on the vast ecosystem of crates (serde, tokio, etc.)
  12. 04/10/2022 Oxidizing rpm-ostree 16 type DnfPackage; fn get_ref(self: &mut DnfPackage)

    -> &mut FFIDnfPackage; fn get_nevra(self: &DnfPackage) -> String; fn get_name(self: &DnfPackage) -> String; fn get_evr(self: &DnfPackage) -> String; fn get_arch(self: &DnfPackage) -> String; Step 5: libdnf-sys libdnf is the external C library that provides all the logic for handling RPMs. Its API surface is quite large, but our Rust logic only uses a small subset of it. We (manually) bridged the few needed functions, and keep that internal to rpm-ostree.
  13. 04/10/2022 Oxidizing rpm-ostree 17 Step 4: cxx-rs cxx-rs allows easy

    bridging between C++ and Rust. It auto-generates glue code on both C++ and Rust sides. This automatic bridging spare us from writing a lot of (unsafe) glue. (Diagram from https://cxx.rs)
  14. 04/10/2022 Oxidizing rpm-ostree Timeline 112 releases so far: • v2014.1

    - first one - 33 commits (total) • v2022.13 - current/latest one - 5549 commits (total) 19
  15. 04/10/2022 Oxidizing rpm-ostree 20 Timeline Project started as a simple

    helper for stuffing RPM content in OSTree - written in Python. Ported to C due to increasing need of low-level primitives. Initial Rust logic added, as opt-in feature. Some core logic moved to Rust, no more opt-in. Growing amount of manual FFI glue. Ported to C++, reducing the amount of manual Rust integration through cxx-rs. • 2013 • 2014 • 2018 • 2019 • 2020
  16. 04/10/2022 Oxidizing rpm-ostree 21 Timeline Language LoC C 40598 Rust

    315 Total 40913 Code statistics as of v2018.6 (first release with mixed C and Rust)
  17. 04/10/2022 Oxidizing rpm-ostree 22 Timeline Language LoC C 44419 Rust

    4670 Total 49089 Code statistics as of v2020.8 (last release with mixed C and Rust)
  18. 04/10/2022 Oxidizing rpm-ostree 23 Timeline Language LoC C 32073 C++

    11442 Rust 5357 Total 48872 Code statistics as of v2020.9 (first release using cxx-rs)
  19. 04/10/2022 Oxidizing rpm-ostree 24 Timeline Language LoC C 927 C++

    39662 Rust 20846 Total 61435 Code statistics as of v2022.13 (latest stable release)
  20. 04/10/2022 Oxidizing rpm-ostree 25 Lessons we learned Several observations from

    the last few years of development, about: • Development velocity • Complexity of reviews • Unit testing • Dependencies (Cargo) • Minimum Supported Rust Version (MSRV) and distro-packaging
  21. 04/10/2022 Oxidizing rpm-ostree 26 Development flow Compared to the previous

    C-only flow: • we increased the usage of unit-tests, thanks to cargo test. • rustfmt made code-formatting easy, and we started enforcing it in CI (later expanded to C code too). • reviewers are able to better focus on logic issues instead of C pitfalls (thanks to richer types and static checks).
  22. 04/10/2022 Oxidizing rpm-ostree 27 OS integration and project shipping Compared

    to the previous C flow: • we had to figure how to build/ship our Rust code, in Fedora and RHEL. • we had to pay attention to newer Rust features we were using, and the minimum toolchain (MSRV) requirements. • we gained access to many high-quality crates for common tasks, decoupling from system libraries.
  23. 04/10/2022 Oxidizing rpm-ostree 29 let dirfd = nix::Dir::open( tempdir.path(), OFlag::empty(),

    nix::Mode::empty())?; let target = "link-content"; let linkpath = "relpath"; symlinkat(target, Some(dirfd), // Option<RawFd> linkpath)?; Backup: nix-rs The nix crate provides a memory-safe alternative to the unsafe APIs exposed by the libc crate. This is done by wrapping low-level functionality with types/abstractions that enforce legal/safe usage.
  24. 04/10/2022 Oxidizing rpm-ostree 30 Backup: io-safety io-safety is a Rust

    libstd feature that got stabilized in 1.63.0 (2022/08). It provides sound types to work with file descriptors.
  25. 04/10/2022 Oxidizing rpm-ostree 31 Backup: rustix and cap-std rustix is

    another safe wrapper for the low-level primitives in the libc crate. It is still undergoing stabilization, and it is fully built on top of the new io-safety primitives. cap-std provides a capability-based version of libstd primitives. This is mostly useful as a building-block to implement filesystem sandboxing. Among other things, this is meant to avoid security bugs such as “path traversals” (e.g. unexpected data leak via ../../../etc/passwd)