Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
436 changes: 436 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ tempfile = "^3.26"
uapi-version = "0.4.0"
walkdir = "2.3.2"
signal-hook-registry = "1.4.8"
zlink = { version = "0.7.0", default-features = false, features = ["smol", "service", "tracing"] }
smol = "2.0.2"

[profile.release]
# We assume we're being delivered via e.g. RPM which supports split debuginfo
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ install-grub-static:

.PHONY: install-systemd-unit
install-systemd-unit:
install -m 644 -D -t "${DESTDIR}$(PREFIX)/lib/systemd/system/" systemd/bootloader-update.service
install -m 644 -D -t "${DESTDIR}$(PREFIX)/lib/systemd/system/" systemd/bootloader-update.service systemd/bootupd-varlink.service systemd/bootupd-varlink.socket

.PHONY: install-all
install-all: install install-grub-static install-systemd-unit
2 changes: 2 additions & 0 deletions contrib/packaging/bootupd.spec
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ Conflicts: bootc < 1.14.1
%{_libexecdir}/bootupd
%{_prefix}/lib/bootupd/grub2-static/
%{_unitdir}/bootloader-update.service
%{_unitdir}/bootupd-varlink.socket
%{_unitdir}/bootupd-varlink.service

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be nice to have systemd units enforce locking.

So we have a single bootupd.service and change bootloader-update.service to actually call into the varlink API too, which would activate that service in the same way.

Alternatively, do we actually need a .socket unit? What we did in e.g. varlink for https://github.com/bootc-dev/bcvk/ is that it remains a CLI that has an interface one forks that enables varlink.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current state should work without a socket unit actually, so just bootupd varlink will run it's own daemon. I don't quite understand the suggestion for a single bootupd.service though, would you mind clarifying? If we're gonna have a service anyway why not let systemd handle the socket? I don't have a strong opinion on this and initially leaned towards a simple service at first but the PR over on fwupd assumed it was socket-activated so I shifted that direction.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking a look by the way @cgwalters


%prep
%autosetup -n %{crate}-%{version} -p1 -a1
Expand Down
2 changes: 1 addition & 1 deletion src/backend/statefile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ fn get_parent_device(root: &Dir) -> Result<Device> {

impl SavedState {
/// System-wide bootupd write lock (relative to sysroot).
const WRITE_LOCK_PATH: &'static str = "run/bootupd-lock";
pub(crate) const WRITE_LOCK_PATH: &'static str = "run/bootupd-lock";
/// Top-level directory for statefile (relative to sysroot).
pub(crate) const STATEFILE_DIR: &'static str = "boot";
/// On-disk bootloader statefile, akin to a tiny rpm/dpkg database,
Expand Down
17 changes: 17 additions & 0 deletions src/cli/bootupd.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::bootloader::Bootloader;
use crate::bootupd::{self, ConfigMode};
#[cfg(efi_arch)]
use crate::varlink;
use anyhow::{Context, Result};
use camino::Utf8Path;
use cap_std::ambient_authority;
Expand Down Expand Up @@ -42,6 +44,9 @@ pub enum DVerb {
Install(InstallOpts),
#[cfg(efi_arch)]
SetDefaultBootloader(DefaultBootloaderOpts),
#[cfg(efi_arch)]
#[clap(name = "varlink", hide = true, about = "Run the varlink service")]
Varlink,
}

#[derive(Debug, Parser)]
Expand Down Expand Up @@ -116,6 +121,8 @@ impl DCommand {
DVerb::GenerateUpdateMetadata(opts) => Self::run_generate_meta(opts),
#[cfg(efi_arch)]
DVerb::SetDefaultBootloader(opts) => Self::set_default_bootloader(opts),
#[cfg(efi_arch)]
DVerb::Varlink => Self::run_varlink_service(),
}
}

Expand Down Expand Up @@ -178,4 +185,14 @@ impl DCommand {

Ok(())
}

fn run_varlink_service() -> Result<()> {
#[cfg(efi_arch)]
{
varlink::run_varlink_service()
}

#[cfg(not(efi_arch))]
Ok(())
}
}
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ mod ostreeutil;
mod packagesystem;
mod sha512string;
mod util;
#[cfg(efi_arch)]
mod varlink;

use clap::crate_name;

Expand Down
216 changes: 216 additions & 0 deletions src/varlink.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
use anyhow::{anyhow, Context};
use cap_std::{ambient_authority, fs::Dir};
use cap_std_ext::dirext::CapStdExtDirExt;
use log::info;
use std::{
fs::create_dir_all,
os::{
fd::IntoRawFd,
unix::io::{FromRawFd, OwnedFd},
},
path::{Path, PathBuf},
};

use crate::{
bootupd::list_dev_current_root, efi::Efi, freezethaw::fsfreeze_thaw_cycle, model::SavedState,
};

const SOCKET_PATH: &str = "/run/bootupd/org.coreos.bootupd1";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above re the socket


/// Find the ESP device matching a given partition UUID
fn find_esp_by_partuuid<'a>(
devices: &'a [bootc_internal_blockdev::Device],
partuuid: &str,
) -> Option<&'a bootc_internal_blockdev::Device> {
devices.iter().find(|d| {
d.partuuid
.as_deref()
.is_some_and(|u| u.eq_ignore_ascii_case(partuuid))
})
}

#[derive(Debug, Clone, zlink::ReplyError, zlink::introspect::ReplyError)]
#[zlink(interface = "org.coreos.bootupd1")]
enum BootupdVarlinkError {
Failed { message: String },
}

impl BootupdVarlinkError {
fn new(message: String) -> Self {
Self::Failed { message }
}
}

impl From<anyhow::Error> for BootupdVarlinkError {
fn from(err: anyhow::Error) -> Self {
log::error!("varlink call failed: {err:#}");
Self::Failed {
message: format!("{err}"),
}
}
}

struct BootupdVarlinkService;

#[zlink::service(interface = "org.coreos.bootupd1")]
impl BootupdVarlinkService {
/// Sync capsule update files from a "primary" ESP to all colocated ESPs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this really be specific to capsules? Isn't the general use case here "I am some software that touched one of the ESPs, please sync the others"?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I assumed at first, but @travier wanted just the "fwupd folder" in coreos/fedora-coreos-tracker#1623 (comment)

///
/// partuuid: GPT partition UUID of the ESP containing the source capsule files.
/// capsule_dir: Path to the directory containing the source capsule files, relative
/// to the ESP root (e.g. "EFI/fedora/fw")
#[allow(clippy::unused_async)]
async fn sync_fwupd_updates(
&mut self,
partuuid: &str,
capsule_dir: &str,
) -> Result<(), BootupdVarlinkError> {
if partuuid.is_empty() {
return Err(BootupdVarlinkError::new(
"partuuid must not be empty".into(),
));
}
if capsule_dir.is_empty() {
return Err(BootupdVarlinkError::new(
"capsule_dir must not be empty".into(),
));
}

let capsule_dir = Path::new(capsule_dir);
// Must be relative
if capsule_dir.is_absolute() {
return Err(BootupdVarlinkError::new(
"capsule_dir must be a relative path".into(),
));
}
// Must not contain path traversal components
if capsule_dir
.components()
.any(|c| c == std::path::Component::ParentDir)
{
return Err(BootupdVarlinkError::new(
"capsule_dir must not contain '..' components - path traversal not allowed".into(),
));
}

let root_device = list_dev_current_root()?;
let esp_devices = root_device.find_colocated_esps()?.unwrap_or_default();

// Find the source ESP (the one fwupd wrote to).
let primary_device = find_esp_by_partuuid(&esp_devices, partuuid).ok_or_else(|| {
BootupdVarlinkError::new(format!("No ESP found with partuuid {partuuid}"))
})?;

// Avoid running at the same time as bootloader updates
let sysroot = Dir::open_ambient_dir("/", ambient_authority()).context("opening sysroot")?;
let _lock = SavedState::acquire_write_lock("/".into(), sysroot)?;

// Mount primary ESP and find capsule updates dir
let primary_efi = Efi::default();
let primary_mount =
primary_efi.ensure_mounted_esp(Path::new("/"), Path::new(&primary_device.path()))?;

let src_capsule_path = primary_mount.join(capsule_dir);
if !src_capsule_path.is_dir() {
primary_efi.unmount()?;
return Err(BootupdVarlinkError::new(format!(
"Capsule directory not found at: {src_capsule_path:?}"
)));
}

let src_dir = Dir::open_ambient_dir(&src_capsule_path, ambient_authority())
.context("opening source capsule dir")?;

// Sync to every other co-located ESP.
let mut synced_count = 0;
for esp in esp_devices.iter().filter(|dev| {
dev.partuuid
.as_ref()
.is_some_and(|u| !u.eq_ignore_ascii_case(partuuid))
}) {
let secondary_efi = Efi::default();
let dest_mount =
secondary_efi.ensure_mounted_esp(Path::new("/"), Path::new(&esp.path()))?;

let dest_capsule_path = dest_mount.join(capsule_dir);
create_dir_all(&dest_capsule_path)
.with_context(|| format!("creating {dest_capsule_path:?}"))?;

let dest_dir = Dir::open_ambient_dir(&dest_capsule_path, ambient_authority())
.context("opening destination capsule dir")?;

for entry in src_dir.entries().context("reading source capsule dir")? {
let entry = entry.context("reading dir entry")?;
let name = entry.file_name();
let contents = src_dir
.read(&name)
.with_context(|| format!("reading {name:?}"))?;
dest_dir
.write(&name, &contents)
.with_context(|| format!("writing {name:?}"))?;
}

fsfreeze_thaw_cycle(
dest_dir
.reopen_as_ownedfd()
.context("reopening dest dir as owned fd")?,
)?;
drop(dest_dir);
secondary_efi.unmount()?;

synced_count += 1;
}
info!("successfully synced {capsule_dir:?} from ESP {partuuid} to {synced_count} colocated ESP(s)");

drop(src_dir);
primary_efi.unmount()?;

Ok(())
}
}

/// Ensure the Unix socket can be created
fn get_socket() -> anyhow::Result<PathBuf> {
let socket_path = PathBuf::from(SOCKET_PATH);

// Ensure the parent directory exists.
if let Some(parent) = socket_path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("creating directory {}", parent.display()))?;
}

// Remove any stale socket from a previous run.
if let Err(e) = std::fs::remove_file(&socket_path) {
if e.kind() != std::io::ErrorKind::NotFound {
return Err(e)
.with_context(|| format!("removing stale socket {}", socket_path.display()));
}
}

Ok(socket_path)
}

pub fn run_varlink_service() -> anyhow::Result<()> {
smol::block_on(async {
let listener = if std::env::var_os("LISTEN_FDS").is_some() {
// Socket-activated
let fd = libsystemd::activation::receive_descriptors(false)
.context("receiving socket-activated fds")?
.into_iter()
.next()
.ok_or_else(|| anyhow!("no fds received"))?;
// SAFETY: `into_raw_fd` transfers ownership from `FileDescriptor`, ensuring the
// fd is valid and not closed elsewhere. `from_raw_fd` takes exclusive ownership.
let owned_fd = unsafe { OwnedFd::from_raw_fd(fd.into_raw_fd()) };
zlink::smol::unix::Listener::try_from(owned_fd)
.context("creating listener from socket-activated fd")?
} else {
// Bind our own socket
let socket_path = get_socket()?;
zlink::smol::unix::bind(socket_path)?
};

let server = zlink::Server::new(listener, BootupdVarlinkService);
server.run().await.context("running varlink service")
})
}
17 changes: 17 additions & 0 deletions systemd/bootupd-varlink.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[Unit]
Description=Bootupd Varlink Service
Documentation=https://github.com/coreos/bootupd
# Interface only implemented on EFI systems
ConditionPathExists=/sys/firmware/efi

[Service]
Type=simple
# It doesn't make sense to sync ESP updates in "Live" environments.
# https://github.com/coreos/fedora-coreos-tracker/issues/2136
ExecCondition=/bin/bash -c '[[ ! $(findmnt -n -o FSTYPE /sysroot) =~ ^(erofs|squashfs)$ ]]'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above, if we changed bootloader-update.service then we could avoid copy-pasta this

ExecStart=/usr/libexec/bootupd varlink
# Keep this stuff in sync with SYSTEMD_ARGS_BOOTUPD in general
PrivateNetwork=yes
ProtectHome=yes
KillMode=mixed
MountFlags=slave
12 changes: 12 additions & 0 deletions systemd/bootupd-varlink.socket
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[Unit]
Description=Bootupd Varlink Socket
Documentation=https://github.com/coreos/bootupd
# Interface only implemented on EFI systems
ConditionPathExists=/sys/firmware/efi

[Socket]
ListenStream=/run/bootupd/org.coreos.bootupd1
SocketMode=0600

[Install]
WantedBy=sockets.target
1 change: 1 addition & 0 deletions tests/kola/varlink/data/libtest.sh
Loading