Skip to content

Commit e0a14a6

Browse files
committed
ephemeral: Use SMBIOS credentials for systemd units
Fix ephemeral mode to use SMBIOS credentials (systemd.extra-unit.*) instead of writing systemd units directly to `/run/source-image/etc/systemd/system/`. This addresses ConditionFirstBoot issues on Fedora CoreOS where directly written units trigger systemd preset cleanup. The libvirt mode already uses SMBIOS credentials successfully. This change aligns ephemeral mode with that proven approach. Fixes: #106 Assisted-by: Claude Code (Sonnet 4.5) Signed-off-by: Colin Walters <[email protected]>
1 parent c954cb3 commit e0a14a6

File tree

2 files changed

+105
-92
lines changed

2 files changed

+105
-92
lines changed

crates/kit/src/run_ephemeral.rs

Lines changed: 97 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,8 @@ pub(crate) async fn run_impl(opts: RunEphemeralOpts) -> Result<()> {
791791

792792
// Process host mounts and prepare virtiofsd instances for each using async manager
793793
let mut additional_mounts = Vec::new();
794+
// Collect mount unit credentials to inject via SMBIOS instead of writing to filesystem
795+
let mut mount_unit_smbios_creds = Vec::new();
794796

795797
debug!(
796798
"Checking for host mounts directory: /run/host-mounts exists = {}",
@@ -801,8 +803,7 @@ pub(crate) async fn run_impl(opts: RunEphemeralOpts) -> Result<()> {
801803
Utf8Path::new("/run/systemd-units").exists()
802804
);
803805

804-
let target_unitdir = "/run/source-image/etc/systemd/system";
805-
806+
let mut mount_unit_names = Vec::new();
806807
if Utf8Path::new("/run/host-mounts").exists() {
807808
for entry in fs::read_dir("/run/host-mounts")? {
808809
let entry = entry?;
@@ -835,73 +836,48 @@ pub(crate) async fn run_impl(opts: RunEphemeralOpts) -> Result<()> {
835836
};
836837
additional_mounts.push((virtiofsd_config, tag.clone()));
837838

838-
// Create individual .mount unit for this virtiofs mount
839+
// Generate mount unit via SMBIOS credentials instead of writing to filesystem
839840
let mount_point = format!("/run/virtiofs-mnt-{}", mount_name_str);
840-
841-
// Use systemd-escape to properly escape the mount path
842-
let escaped_path = Command::new("systemd-escape")
843-
.args(["-p", &mount_point])
844-
.output()
845-
.map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string())
846-
.unwrap_or_else(|_| {
847-
// Fallback if systemd-escape is not available
848-
mount_point
849-
.replace("/", "-")
850-
.trim_start_matches('-')
851-
.to_string()
852-
});
853-
854-
let mount_unit_name = format!("{}.mount", escaped_path);
855-
let mount_options = if is_readonly { "ro" } else { "defaults" };
856-
857-
let mount_unit_content = format!(
858-
r#"[Unit]
859-
Description=Mount virtiofs {}
860-
DefaultDependencies=no
861-
After=systemd-remount-fs.service
862-
Before=local-fs.target shutdown.target
863-
864-
[Mount]
865-
What={}
866-
Where={}
867-
Type=virtiofs
868-
Options={}
869-
870-
[Install]
871-
WantedBy=local-fs.target
872-
"#,
873-
mount_name_str, tag, mount_point, mount_options
841+
let unit_name = crate::credentials::guest_path_to_unit_name(&mount_point);
842+
let mount_unit_content =
843+
crate::credentials::generate_mount_unit(&tag, &mount_point, is_readonly);
844+
let encoded_mount = data_encoding::BASE64.encode(mount_unit_content.as_bytes());
845+
846+
// Create SMBIOS credential for the mount unit
847+
let mount_cred = format!(
848+
"io.systemd.credential.binary:systemd.extra-unit.{unit_name}={encoded_mount}"
874849
);
850+
mount_unit_smbios_creds.push(mount_cred);
875851

876-
let mount_unit_path = format!("{target_unitdir}/{mount_unit_name}");
877-
fs::write(&mount_unit_path, mount_unit_content)
878-
.with_context(|| format!("Failed to write mount unit to {}", mount_unit_path))?;
879-
880-
// Enable the mount unit by creating symlink in local-fs.target.wants/
881-
let wants_dir = format!("{target_unitdir}/local-fs.target.wants");
882-
fs::create_dir_all(&wants_dir)?;
883-
let wants_link = format!("{}/{}", wants_dir, mount_unit_name);
884-
let relative_target = format!("../{}", mount_unit_name);
885-
std::os::unix::fs::symlink(&relative_target, &wants_link)?;
886-
887-
// Create mount point directory in the image
888-
let image_mount_point = format!("/run/source-image{}", mount_point);
889-
fs::create_dir_all(&image_mount_point).ok();
852+
// Collect unit name for the local-fs.target dropin
853+
mount_unit_names.push(unit_name.clone());
890854

891855
debug!(
892-
"Generated mount unit: {} (enabled in local-fs.target)",
893-
mount_unit_name
856+
"Generated SMBIOS credential for mount unit: {} ({})",
857+
unit_name, mode
894858
);
895859
}
896860
}
897861

862+
// If we have mount units, create a single dropin for local-fs.target
863+
if !mount_unit_names.is_empty() {
864+
let wants_list = mount_unit_names.join(" ");
865+
let dropin_content = format!("[Unit]\nWants={}\n", wants_list);
866+
let encoded_dropin = data_encoding::BASE64.encode(dropin_content.as_bytes());
867+
let dropin_cred = format!(
868+
"io.systemd.credential.binary:systemd.unit-dropin.local-fs.target~bcvk-mounts={encoded_dropin}"
869+
);
870+
mount_unit_smbios_creds.push(dropin_cred);
871+
debug!(
872+
"Created local-fs.target dropin for {} mount units",
873+
mount_unit_names.len()
874+
);
875+
}
876+
898877
// Handle --execute: pipes will be created when adding to qemu_config later
899878
// No need to create files anymore as we're using pipes
900879

901-
let default_wantsdir = format!("{target_unitdir}/default.target.wants");
902-
fs::create_dir_all(&default_wantsdir)?;
903-
904-
// Create systemd unit to stream journal to virtio-serial device
880+
// Create systemd unit to stream journal to virtio-serial device via SMBIOS credential
905881
let journal_stream_unit = r#"[Unit]
906882
Description=Stream systemd journal to host via virtio-serial
907883
DefaultDependencies=no
@@ -919,17 +895,23 @@ RestartSec=1s
919895
[Install]
920896
WantedBy=sysinit.target
921897
"#;
922-
let journal_unit_path = format!("{target_unitdir}/bcvk-journal-stream.service");
923-
tokio::fs::write(&journal_unit_path, journal_stream_unit).await?;
924-
debug!("Created journal streaming unit at {journal_unit_path}");
925-
926-
// Enable the journal streaming unit
927-
let sysinit_wantsdir = format!("{target_unitdir}/sysinit.target.wants");
928-
tokio::fs::create_dir_all(&sysinit_wantsdir).await?;
929-
let journal_wants_link = format!("{sysinit_wantsdir}/bcvk-journal-stream.service");
930-
tokio::fs::symlink("../bcvk-journal-stream.service", &journal_wants_link).await?;
931-
debug!("Enabled journal streaming unit in sysinit.target.wants");
898+
let encoded_journal = data_encoding::BASE64.encode(journal_stream_unit.as_bytes());
899+
let journal_cred = format!(
900+
"io.systemd.credential.binary:systemd.extra-unit.bcvk-journal-stream.service={encoded_journal}"
901+
);
902+
mount_unit_smbios_creds.push(journal_cred);
903+
debug!("Generated SMBIOS credential for journal streaming unit");
904+
905+
// Create dropin for sysinit.target to enable journal streaming
906+
let journal_dropin = "[Unit]\nWants=bcvk-journal-stream.service\n";
907+
let encoded_dropin = data_encoding::BASE64.encode(journal_dropin.as_bytes());
908+
let dropin_cred = format!(
909+
"io.systemd.credential.binary:systemd.unit-dropin.sysinit.target~bcvk-journal={encoded_dropin}"
910+
);
911+
mount_unit_smbios_creds.push(dropin_cred);
912+
debug!("Created sysinit.target dropin to enable journal streaming unit");
932913

914+
// Create execute units via SMBIOS credentials if needed
933915
match opts.common.execute.as_slice() {
934916
[] => {}
935917
elts => {
@@ -949,8 +931,7 @@ StandardError=inherit
949931
service_content.push_str(&format!("ExecStart={elt}\n"));
950932
}
951933

952-
let service_finish = format!(
953-
r#"[Unit]
934+
let service_finish = r#"[Unit]
954935
Description=Execute Script Service Completion
955936
After=bootc-execute.service
956937
Requires=dev-virtio\\x2dports-executestatus.device
@@ -960,24 +941,34 @@ Type=oneshot
960941
ExecStart=systemctl show bootc-execute
961942
ExecStart=systemctl poweroff
962943
StandardOutput=file:/dev/virtio-ports/executestatus
963-
"#
964-
);
944+
"#;
965945

966-
let service_path = format!("{target_unitdir}/bootc-execute.service");
967-
fs::write(service_path, service_content)?;
968-
let service_path = format!("{target_unitdir}/bootc-execute-finish.service");
969-
fs::write(service_path, service_finish)?;
946+
// Inject execute units via SMBIOS credentials
947+
let encoded_execute = data_encoding::BASE64.encode(service_content.as_bytes());
948+
let execute_cred = format!(
949+
"io.systemd.credential.binary:systemd.extra-unit.bootc-execute.service={encoded_execute}"
950+
);
951+
mount_unit_smbios_creds.push(execute_cred);
970952

971-
for svc in ["bootc-execute.service", "bootc-execute-finish.service"] {
972-
let wants_link = format!("{default_wantsdir}/{svc}");
973-
debug!("Creating execute service symlink: {}", &wants_link);
974-
std::os::unix::fs::symlink(format!("../{svc}"), wants_link)?;
975-
}
953+
let encoded_finish = data_encoding::BASE64.encode(service_finish.as_bytes());
954+
let finish_cred = format!(
955+
"io.systemd.credential.binary:systemd.extra-unit.bootc-execute-finish.service={encoded_finish}"
956+
);
957+
mount_unit_smbios_creds.push(finish_cred);
958+
959+
// Create dropin for default.target to enable execute services
960+
let execute_dropin =
961+
"[Unit]\nWants=bootc-execute.service bootc-execute-finish.service\n";
962+
let encoded_dropin = data_encoding::BASE64.encode(execute_dropin.as_bytes());
963+
let dropin_cred = format!(
964+
"io.systemd.credential.binary:systemd.unit-dropin.default.target~bcvk-execute={encoded_dropin}"
965+
);
966+
mount_unit_smbios_creds.push(dropin_cred);
967+
debug!("Generated SMBIOS credentials for execute units");
976968
}
977969
}
978970

979-
// Copy systemd units if provided (after mount units have been generated)
980-
// Also inject if we created mount units that need to be copied
971+
// Copy systemd units if provided (for --systemd-units-dir option)
981972
inject_systemd_units()?;
982973

983974
// Prepare main virtiofsd config for the source image (will be spawned by QEMU)
@@ -1062,22 +1053,30 @@ StandardOutput=file:/dev/virtio-ports/executestatus
10621053
"swap".into(),
10631054
crate::to_disk::Format::Raw,
10641055
);
1065-
let svc = format!(
1066-
r#"[Unit]
1056+
1057+
// Create swap unit via SMBIOS credential
1058+
let svc = r#"[Unit]
10671059
Description=bcvk ephemeral swap
10681060
10691061
[Swap]
10701062
What=/dev/disk/by-id/virtio-swap
10711063
Options=
1072-
"#
1073-
);
1074-
1064+
"#;
10751065
let service_name = r#"dev-disk-by\x2did-virtio\x2dswap.swap"#;
1076-
let service_path = format!("{target_unitdir}/{service_name}");
1077-
fs::write(&service_path, svc)?;
1066+
let encoded_swap = data_encoding::BASE64.encode(svc.as_bytes());
1067+
let swap_cred = format!(
1068+
"io.systemd.credential.binary:systemd.extra-unit.{service_name}={encoded_swap}"
1069+
);
1070+
mount_unit_smbios_creds.push(swap_cred);
10781071

1079-
let wants_link = format!("{default_wantsdir}/{service_name}");
1080-
std::os::unix::fs::symlink(format!("../{service_name}"), wants_link)?;
1072+
// Create dropin for default.target to enable swap
1073+
let swap_dropin = format!("[Unit]\nWants={service_name}\n");
1074+
let encoded_dropin = data_encoding::BASE64.encode(swap_dropin.as_bytes());
1075+
let dropin_cred = format!(
1076+
"io.systemd.credential.binary:systemd.unit-dropin.default.target~bcvk-swap={encoded_dropin}"
1077+
);
1078+
mount_unit_smbios_creds.push(dropin_cred);
1079+
debug!("Generated SMBIOS credential for swap unit");
10811080

10821081
tmp_swapfile = Some(tmpf);
10831082
}
@@ -1207,6 +1206,13 @@ Options=
12071206
})?;
12081207
};
12091208

1209+
// Add all SMBIOS credentials for mount units, journal, and execute services
1210+
let cred_count = mount_unit_smbios_creds.len();
1211+
for cred in mount_unit_smbios_creds {
1212+
qemu_config.add_smbios_credential(cred);
1213+
}
1214+
debug!("Added {} SMBIOS credentials to QEMU config", cred_count);
1215+
12101216
debug!("Starting QEMU with systemd debugging enabled");
12111217

12121218
// Spawn QEMU with all virtiofsd processes handled internally

crates/kit/src/to_disk.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,14 @@ impl ToDiskOpts {
226226
rm /var/lib/containers -rf
227227
ln -sr /var/tmp/containers /var/lib/containers
228228
229+
# Ensure virtiofs mount is available (fallback for older systemd without SMBIOS support)
230+
AIS=/run/virtiofs-mnt-hoststorage/
231+
if ! mountpoint -q ${AIS} &>/dev/null; then
232+
echo "virtiofs mount not found at ${AIS}, mounting manually..."
233+
mkdir -p ${AIS}
234+
mount -t virtiofs mount_hoststorage ${AIS} -o ro
235+
fi
236+
229237
echo "Starting bootc installation..."
230238
echo "Source image: {SOURCE_IMGREF}"
231239
echo "Additional args: {BOOTC_ARGS}"
@@ -237,7 +245,6 @@ impl ToDiskOpts {
237245
238246
# Execute bootc installation, having the outer podman pull from
239247
# the virtiofs store on the host, as well as the inner bootc.
240-
AIS=/run/virtiofs-mnt-hoststorage/
241248
export STORAGE_OPTS=additionalimagestore=${AIS}
242249
podman run --rm -i ${tty} --privileged --pid=host --net=none -v /sys:/sys:ro \
243250
-v /var/lib/containers:/var/lib/containers -v /dev:/dev -v ${AIS}:${AIS} --security-opt label=type:unconfined_t \

0 commit comments

Comments
 (0)