-
Notifications
You must be signed in to change notification settings - Fork 166
install/bootupd: chroot to deployment #1816
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces a chroot into the deployment before calling bootupd when --src-imgref is used. This is a sensible approach to ensure that the binaries and configuration files from the target image are used for bootloader installation. The implementation correctly sets up bind mounts for necessary pseudo-filesystems. However, I've identified a potential resource leak where these bind mounts would not be cleaned up if the bootupd command fails. My review includes a suggestion to refactor the cleanup logic to ensure it runs regardless of the command's success or failure, preventing leaked mounts.
2e335d8 to
3b92a48
Compare
|
/gemini review |
0b51f0e to
7d79124
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces a chroot into the deployment before calling bootupd when a source image reference is provided. This is a good change to ensure the correct binaries and configuration from the target image are used.
My review has identified two issues with the resource management of the bind mounts:
- A high-severity issue where mounts can be leaked if an error occurs during the setup of the bind mounts.
- A medium-severity issue in the cleanup logic where only the last error from multiple unmount failures would be reported.
I've provided detailed comments and a suggestion for one of the issues. Addressing these will make the implementation more robust.
| for src in bind_mount_dirs { | ||
| let dest = target_root | ||
| // joining an absolute path | ||
| // makes it replace self, so we strip the prefix | ||
| .join_os(src.strip_prefix("/").unwrap()); | ||
| tracing::debug!("bind mounting {}", dest.display()); | ||
| rustix::mount::mount_bind_recursive(src, dest)?; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If mount_bind_recursive fails for any of the directories (e.g., the second one), the function will return early due to the ? operator. This will leak any mounts that were successfully created in previous iterations of the loop, as the cleanup code at the end of the function will not be executed.
To fix this, you should ensure that cleanup is always performed. A common pattern in Rust is to use a guard struct with a Drop implementation. However, since you want to propagate unmount errors, a Drop guard (which cannot return errors) might not be what you want.
An alternative is to restructure the code to ensure the cleanup block is always reached. For example, you could wrap the mounting and command execution in a closure that returns a Result, and then perform cleanup regardless of whether it succeeded or failed. This would involve tracking which directories were successfully mounted.
Example structure:
let mut mounted_dirs = Vec::new();
let result = (|| {
// Mount loop, add to mounted_dirs on success
for src in bind_mount_dirs {
// ... mount logic ...
mounted_dirs.push(src);
}
// Execute command
// ...
Ok(())
})();
// Cleanup loop over mounted_dirs
// ...
// Return combined resultThis is a significant refactoring, but it's crucial for resource safety.
| if let Err(e) = rustix::mount::unmount(&mount, UnmountFlags::DETACH) { | ||
| tracing::warn!("Error unmounting {}: {e}", mount.display()); | ||
| unmount_res = Err(e.into()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the case of multiple unmount failures, this logic will overwrite previous errors, and only the last error will be propagated. To ensure the first error is preserved and reported, you should only set unmount_res if it's currently Ok.
if let Err(e) = rustix::mount::unmount(&mount, UnmountFlags::DETACH) {
tracing::warn!("Error unmounting {}: {e}", mount.display());
if unmount_res.is_ok() {
unmount_res = Err(e.into());
}
}| let src_root_arg = if let Some(p) = abs_deployment_path.as_deref() { | ||
| vec!["--src-root", p.as_str()] | ||
| let abs_deployment_path = deployment_path.map(|deploy| rootfs.join(deploy)); | ||
| // When not running inside the target container (through `--src-imgref`) we chroot |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's other threads were we talked about offering a bootc install mount as a general ability to mount a deployment outside of booting it; were we to do that it would make a lot of sense for this code to use it.
In ostree we resisted doing that for a long time but eventually did just internally for selinux, see https://github.com/ostreedev/ostree/blob/c6f0b5b2bc26b22fbceee0dc28a0f31349c28d41/src/libostree/ostree-sysroot-deploy.c#L3308
On that topic, it'd be a lot cleaner even here to use a more proper containerization than just setting up the mounts. It's a bit tricky though because we actually do need to e.g. pass through all of /dev and /sys here (i.e. --privileged in docker/podman terms) in order to update the ESP if desired.
I haven't looked at which of bwrap/{runc,crun}/nspawn/podman would make the most sense for this use case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On that topic, it'd be a lot cleaner even here to use a more proper containerization than just setting up the mounts. It's a bit tricky though because we actually do need to e.g. pass through all of
/devand/syshere (i.e.--privilegedin docker/podman terms) in order to update the ESP if desired.I haven't looked at which of bwrap/{runc,crun}/nspawn/podman would make the most sense for this use case.
I am not sure of what you mean with this comment. Do you want to block this change until there are more proper containerization helpers in bootc, or are you just making a note that this should be revisited later on ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We had a live chat about this and agreed to merge as is and file a tracker followup issue for improving the mount setup.
| .run_inherited_with_cmd_context() | ||
| .run_inherited_with_cmd_context(); | ||
|
|
||
| // Clean up the mounts after ourselves |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could entirely avoid the need to clean up by using the new mount API to get file descriptors instead, and then use https://docs.rs/cap-std-ext/latest/cap_std_ext/cmdext/trait.CapStdExtCommandExt.html#tymethod.cwd_dir with chroot . or so
|
OK there's some legit failures here like |
cgwalters
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Marking as requested changes due to failing CI
023a4c6 to
d636cb1
Compare
cgwalters
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Marking as requested changes due to failing CI
| } | ||
| // Append the `bootupctl` command, it will be passed as | ||
| // an argument to chroot | ||
| vec![target_root.as_str(), "bootupctl"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we just pass / here instead of target_root?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we would then need to have another bind mount rootfs/boot into deployment_path/boot I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok so i ended up doing just that and now more the tests are passing.
| // let's bind mount it to a temp mountpoint under /run | ||
| // so it gets carried over in the chroot. | ||
|
|
||
| let rootfs_mount = if rootfs.starts_with("/run") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about this it may be a lot simpler to just setup the API filesystems by default in the install flow.
(and per discussion mount e.g. /boot in the target rootfs)
|
Note that it's only Fedora variants that are failing; should reproduce locally via e.g. |
Yes, i can reproduce that locally ! Ok so I dug deeper and I think I figured out something : I am not sure what happens because removing the Without the systemd wrapper : In the second case the chroot works. Another thing to note, and i cannot figure this out yet : the error finding the |
|
Thinking more about this : in both cases the mounting and formating of the block device ( |
|
Ok so after doing a bunch of testing around I will try to add the full path but that requires a rebuild of bootupd as well, so that's for tomorrow. edit: ok I did ask bootc to run |
e225788 to
7a14a4b
Compare
77cd963 to
421e427
Compare
When `--src-imgref` is passed, the deployed systemd does not match the running environnement. In this case, let's chroot into the deployment before calling bootupd. This makes sure we are using the binaries shipped in the image (and relevant config files such as grub fragements). We could do that in all cases but i kept it behind the `--src-imgref` option since when using the target container as the buildroot it will have no impact, and we expect this scenario to be the most common. In CoreOS we have a specific test that checks if the bootloader was installed with the `grub2-install` of the image. Fixes bootc-dev#1559 Also see bootc-dev#1455 Signed-off-by: jbtrystram <[email protected]>
Signed-off-by: jbtrystram <[email protected]>
Signed-off-by: jbtrystram <[email protected]>
e6bc7e1 to
f7891ca
Compare
f7891ca to
9e2fbc4
Compare
|
/gemini review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces a chroot mechanism when installing the bootloader with bootupd via --src-imgref. This ensures that the binaries and configuration from the target image are used. The changes look mostly correct, but I've found a few issues.
There is a critical bug in how the rootfs_mount path is determined, which will break the non-chroot installation path. I've also found a high-severity issue where an unmount operation is performed unconditionally while the corresponding mount is conditional, which could lead to errors.
Additionally, I've left some comments regarding leftover commented-out code, a WIP comment, and a suggestion to make the code more robust by avoiding .unwrap().
Once these issues are addressed, the changes should be good to go.
| let rootfs_mount = if rootfs.starts_with("/run") { | ||
| rootfs.as_str() | ||
| } else { | ||
| "/" | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for determining rootfs_mount is incorrect and will cause failures in some scenarios.
- Non-chroot case: If
abs_deployment_pathisNoneandrootfsdoes not start with/run(e.g.,/tmp/target),rootfs_mountincorrectly becomes/. Thebootupctlcommand will then fail because it's pointed at the wrong root filesystem. It should berootfs.as_str(). - Chroot case: If
abs_deployment_pathisSomeandrootfsstarts with/run,rootfs_mountbecomes the path torootfs. However, when chrooted,bootupctlshould operate on/as its root filesystem.
This logic should be dependent on whether a chroot is being performed. I recommend refactoring this to set rootfs_mount to / in the chroot case and rootfs.as_str() otherwise.
| if let Err(e) = rustix::mount::unmount( | ||
| target_root.join("boot").into_std_path_buf(), | ||
| UnmountFlags::DETACH, | ||
| ) { | ||
| tracing::warn!("Error unmounting target/boot: {e}"); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The unmount of target_root.join("boot") is performed unconditionally, but the corresponding mount only happens if !rootfs.starts_with("/run"). This can lead to attempts to unmount a path that was not mounted by this function, resulting in unnecessary warnings or errors. The unmount operation should be wrapped in the same condition as the mount operation.
if !rootfs.starts_with("/run") {
if let Err(e) = rustix::mount::unmount(
target_root.join("boot").into_std_path_buf(),
UnmountFlags::DETACH,
) {
tracing::warn!("Error unmounting target/boot: {e}");
}
}| let dest = target_root | ||
| // joining an absolute path | ||
| // makes it replace self, so we strip the prefix | ||
| .join_os(src.strip_prefix("/").unwrap()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using .unwrap() on the result of strip_prefix could cause a panic if a path without a leading / is ever added to bind_mount_dirs. While it's safe with the current hardcoded values, it would be more robust to handle the None case, for example by using expect() with a clear error message or by restructuring the code to avoid the possibility of a panic. This also applies to the similar usage on line 186.
| tracing::debug!("bind mounting {}", dest.display()); | ||
| rustix::mount::mount_bind_recursive(src, dest)?; | ||
| } | ||
| // WIP : let's try to bind-mount /target/boot into the deployment as well rather than bind-mounting the whole thing?? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
--src-imgrefis passed, the deployed systemd does not match the running environnement. In this case, let's chroot into the deployment before calling bootupd. This makes sure we are using the binaries shipped in the image (and relevant config files such as grub fragements).We could do that in all cases but i kept it behind the
--src-imgrefoption since when using the target container as the buildroot it will have no impact, and we expect this scenario to be the most common.In CoreOS we have a specific test that checks if the bootloader was installed with the
grub2-installof the image.Fixes #1559 Also see #1455