Skip to content
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

efi: update the ESP by creating a tmpdir and RENAME_EXCHANGE #669

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

HuijingHei
Copy link
Member

@HuijingHei HuijingHei commented Jun 19, 2024

See Timothée's comment #454 (comment)
logic is like this:

  • cp -a fedora fedora.tmp
    • We start with a copy to make sure to keep all other files
      that we do not explicitly track in bootupd
  • Update the content of fedora.tmp with the new binaries
  • Exchange fedora.tmp -> fedora
  • Remove now "old" fedora.tmp

If we have a file not in a directory in EFI, then we can copy
it to foo.tmp and then act on it and finally rename it. No
need to copy the entire EFI.
Fixes #454

Additional info for the logic:

  • for removals, copy to temp dir and remember, remove file
    in temp dir; if have a file not in a directory, remove it directly
  • for additionals, write new files to temp dir if exists, else write
    directly in dest; if have a file not in a directory, add it directly
  • for changes, copy to temp dir and remember, write changed
    files to temp dir; if have a file not in a directory, copy to foo.tmp
  • Do local exchange and remove temp dir/file

@HuijingHei HuijingHei changed the title efi: update the ESP by creating a tmpdir and RENAME_EXCHANGE WIP: efi: update the ESP by creating a tmpdir and RENAME_EXCHANGE Jun 19, 2024
@HuijingHei HuijingHei force-pushed the esp_temp branch 2 times, most recently from 3a7cd83 to f3bb105 Compare June 19, 2024 14:25
Copy link
Member

@cgwalters cgwalters left a comment

Choose a reason for hiding this comment

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

Thanks for working on this!

src/efi.rs Outdated Show resolved Hide resolved
src/efi.rs Outdated
Comment on lines 75 to 78
fn esp_path_tmp(&self) -> Result<PathBuf> {
self.ensure_mounted_esp(Path::new("/"))
.map(|v| v.join(".EFI.tmp"))
}
Copy link
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 clearer to have just const TEMP_EFI_DIR: &str = ".EFI.tmp"; or so and use it where needed?

Copy link
Member

Choose a reason for hiding this comment

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

But...related to @travier's comment...it'd be good to try to scope this to the "lowest" directory we need to make the change.

For us that means e.g. we only affect EFI/fedora for example and not all of EFI.

Copy link
Member Author

@HuijingHei HuijingHei Jun 20, 2024

Choose a reason for hiding this comment

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

The bootupd-state.json also includes BOOT/BOOTX64.EFI and BOOT/fbx64.efi under EFI/, both are from shim-x64, we might never do the update? Is it necessary to check or just do not care?

Copy link
Member

Choose a reason for hiding this comment

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

I think it should be reasonable to prepare both the new BOOT and fedora dirs and then do fedora first, BOOT second.

I don't think there is a dependency "across" those folders for the bootloader files, i.e. grub.efi may depend on an update shim.efi from the same folder but not on an update fbx.efy from the BOOT folder.

I'm not sure however.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

See #669 (comment), will prepare both the new BOOT and fedora dirs and then do fedora first, BOOT second.

Copy link
Member

Choose a reason for hiding this comment

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

I don't think there is a dependency "across" those folders for the bootloader files, i.e. grub.efi may depend on an update shim.efi from the same folder but not on an update fbx.efy from the BOOT folder.

Hmmmm...this kind of rains on the parade here though given that shim includes files in these two directories, and we can't update it atomically without updating everything in EFI, which also includes potentially other files we don't manage.

Copy link
Member Author

@HuijingHei HuijingHei Jul 1, 2024

Choose a reason for hiding this comment

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

According to @travier 's suggestion, copy first sub dir as temp (like BOOT/foo -> BOOT.tmp/foo) and remember, then do remove/addition/change files in temp dir, finally scan temp and do exchange. I think it might be helpful when we do A/B updates, instead of current updates based on loop updates dirs.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, this just means that shim updates won't be transactional, which is unfortunate as shim is the main one we need to update.

It should be an improvement...but...I'm a bit uncertain about saying it's enough that we unconditionally turn on auto-updates by default?

It feels like shim/grub update infrequently enough that in practice we could just accept the hit today of the non-transactionality and recommend turning on auto-updates in various distros/OSes?

I'd be roughly in favor of doing that in FCOS and fedora bootc today just to start.

Copy link
Member Author

Choose a reason for hiding this comment

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

It should be an improvement...but...I'm a bit uncertain about saying it's enough that we unconditionally turn on auto-updates by default?

Not sure, is it necessary to check the shim files integrity (like sha512) after sync to provide more confidence?

src/efi.rs Outdated Show resolved Hide resolved
src/efi.rs Outdated Show resolved Hide resolved
@HuijingHei HuijingHei force-pushed the esp_temp branch 2 times, most recently from b635467 to e91cab0 Compare June 20, 2024 07:37
src/efi.rs Outdated Show resolved Hide resolved
@travier
Copy link
Member

travier commented Jun 20, 2024

[2024-06-20T08:07:06.180Z] # Previous BIOS: grub2-tools-1:2.06-89.fc38.x86_64
[2024-06-20T08:07:06.180Z] # Updated BIOS: grub2-tools-1:2.06-102.fc38.x86_64
[2024-06-20T08:07:06.180Z] # error: Failed to update EFI: No such file or directory (os error 2)
[2024-06-20T08:07:06.180Z] + fatal 'test failed'
[2024-06-20T08:07:06.180Z] + echo error: test failed
[2024-06-20T08:07:06.180Z] error: test failed

🤔

src/efi.rs Outdated Show resolved Hide resolved
@HuijingHei HuijingHei force-pushed the esp_temp branch 2 times, most recently from 366b8eb to a6c8400 Compare June 20, 2024 10:43
src/efi.rs Outdated Show resolved Hide resolved
@HuijingHei HuijingHei force-pushed the esp_temp branch 2 times, most recently from d033a4e to ad56520 Compare June 20, 2024 12:10
@HuijingHei
Copy link
Member Author

[2024-06-20T12:39:28.464Z] # Previous BIOS: grub2-tools-1:2.06-89.fc38.x86_64

[2024-06-20T12:39:28.464Z] # Updated BIOS: grub2-tools-1:2.06-102.fc38.x86_64

[2024-06-20T12:39:28.464Z] # Previous EFI: grub2-efi-x64-1:2.06-89.fc38.x86_64,shim-x64-15.8-3.x86_64

[2024-06-20T12:39:28.464Z] # Updated EFI: grub2-efi-x64-1:2.06-102.fc38.x86_64,shim-x64-15.8-3.x86_64,test-bootupd-payload-1.0-1.x86_64

[2024-06-20T12:39:28.464Z] # grep: /tmp/tmp.hin3bC02Ci/EFI/fedora/test-bootupd.efi: No such file or directory

[2024-06-20T12:39:28.464Z] # ls: cannot access '/tmp/tmp.hin3bC02Ci/EFI/fedora/test-bootupd.efi': No such file or directory

@travier
Copy link
Member

travier commented Jun 21, 2024

We've merged the CI fixes. Can you rebase this one? Thanks

@HuijingHei
Copy link
Member Author

We've merged the CI fixes. Can you rebase this one? Thanks

Sure, updated, thanks!

@HuijingHei HuijingHei force-pushed the esp_temp branch 3 times, most recently from 79cb3ad to 3cf8636 Compare June 21, 2024 09:06
@HuijingHei
Copy link
Member Author

Updating shim-x64 15.8-2 -> 15.8-3, it will also update BOOT/fbx64.efi

[root@cosa-devsh ~]# bootupctl update -vvv
Previous EFI: grub2-efi-x64-1:2.06-103.fc40.x86_64,shim-x64-15.8-2.x86_64
Updated EFI: grub2-efi-x64-1:2.06-123.fc40.x86_64,shim-x64-15.8-3.x86_64

[root@cosa-devsh ~]# mount /dev/vda2 /boot/efi/
[root@cosa-devsh ~]# ls /boot/efi/EFI/BOOT/ -l
total 1014
-rwxr-xr-x. 1 root root 949424 Jan  1  1980 BOOTX64.EFI
-rwxr-xr-x. 1 root root  87816 Jun 21 10:01 fbx64.efi

@HuijingHei
Copy link
Member Author

If using .fedora.tmp will cause CI failed grep: /tmp/tmp.hin3bC02Ci/EFI/fedora/test-bootupd.efi: No such file or directory, guess it is because filetree is based on "BOOT/BOOTX64.EFI" or "fedora/BOOTX64.CSV", so the apply_diff will also create the dir like BOOT and fedora under temp dir, which makes it actually /tmp/tmp.hin3bC02Ci/EFI/fedora/fedora/test-bootupd.efi.

@HuijingHei HuijingHei changed the title WIP: efi: update the ESP by creating a tmpdir and RENAME_EXCHANGE efi: update the ESP by creating a tmpdir and RENAME_EXCHANGE Jun 21, 2024
@travier
Copy link
Member

travier commented Jun 21, 2024

The problem with copying the entire EFI directory is that we don't know what's there, and there might a lot of other vendor directories (windows, ubuntu, etc.) that we don't really want to copy as that might take more space than what we have in the EFI partition.

src/filetree.rs Outdated
path.with_file_name(buf)
let mut parent = String::from("");
if let Some(p) = path.parent() {
parent = format!("{}.tmp", p.display());
Copy link
Member Author

Choose a reason for hiding this comment

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

Use fedora.tmp instead of .fedora.tmp, in case if the parent is like a/b, then it will be .a/b.tmp which is not expected.

@HuijingHei
Copy link
Member Author

Update the change in apply_diff(), not sure if it is the right place, steps are:

  • get unique updates dir in diff, save them in hashset
  • scan the hashset,
    • extract the diff based on the same parent
    • copy original dir to temp dir
    • remove original file in temp dir first, then write new and changed files to temp dir
    • do local exchange for fedora.tmp to fedora
    • remove fedora.tmp

let path = path.as_ref();
let mut buf = path.file_name().expect("filename").to_os_string();
buf.push(TMP_PREFIX);
Copy link
Member Author

Choose a reason for hiding this comment

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

Seems the file name is likea.btmp., so to check should not use start_with, but maybe I am wrong.

@travier
Copy link
Member

travier commented Jun 25, 2024

That looks ok but the logic feels a little bit difficult to follow. I might have missed something however so maybe my understanding is not correct.

I'm thinking about the following logic for apply_diff:

  • First look at removals (to minimize disk space usage)
    • Look at which "root/parent" folder the file to remove is
    • Does a temporary copy of this folder exists?
      • Yes, remove the file in the copy
      • No:
        • Copy the folder
        • Remember that we made a copy of this folder
        • Remove the file in the copy
  • Repeat the same logic for changes and additions
  • Sync changes to the disk once here
    • At this point, we haven't yet touched the content actually used to boot
    • If there is not enough space to write all the files, we should have failed before this point
  • Iterate over the folders we made a copy of and do RENAME_EXCHANGE
  • Sync changes between each rename (or once at the end?)
  • Remove all the temporary folders
  • Sync one last time (optional?)

@HuijingHei
Copy link
Member Author

HuijingHei commented Jun 26, 2024

I'm thinking about the following logic for apply_diff:

  • First look at removals (to minimize disk space usage)

    • Look at which "root/parent" folder the file to remove is

    • Does a temporary copy of this folder exists?

      • Yes, remove the file in the copy

      • No:

        • Copy the folder
        • Remember that we made a copy of this folder
        • Remove the file in the copy
  • Repeat the same logic for changes and additions

  • Sync changes to the disk once here

    • At this point, we haven't yet touched the content actually used to boot
    • If there is not enough space to write all the files, we should have failed before this point
  • Iterate over the folders we made a copy of and do RENAME_EXCHANGE

Copy the changed files to prepared temp dir, the final result will not be as expected if the change dir is included by another change. For example, if changes are EFI/fedora/subdir/newgrub.x64 and EFI/fedora/grub.x64, the temp dir will be EFI/fedora/subdir.tmp and EFI/fedora.tmp, then do exchange EFI/fedora/subdir.tmp -> EFI/fedora/subdir, the problem is EFI/fedora.tmp -> EFI/fedora will not include the new change in EFI/fedora/subdir, this is the unit test in test_filetree2() that I debug most of the time, so I do the exchange separately based on dir, any other workarounds for this?
It is not problem for the current bootupd(no this patch) as we only update the file, not related to the dir.

  • Sync changes between each rename (or once at the end?)
  • Remove all the temporary folders
  • Sync one last time (optional?)

@travier
Copy link
Member

travier commented Jun 26, 2024

if changes are EFI/fedora/subdir/newgrub.x64 and EFI/fedora/grub.x64, the temp dir will be EFI/fedora/subdir.tmp and EFI/fedora.tmp

With EFI/fedora/subdir/newgrub.x64 changed, we should copy EFI/fedora to EFI/fedora.tmp. That should cover all the subdirs and should be granular enough.

I.e. we need to find the first subdir in EFI and copy that.

@HuijingHei
Copy link
Member Author

HuijingHei commented Jun 27, 2024

if changes are EFI/fedora/subdir/newgrub.x64 and EFI/fedora/grub.x64, the temp dir will be EFI/fedora/subdir.tmp and EFI/fedora.tmp

With EFI/fedora/subdir/newgrub.x64 changed, we should copy EFI/fedora to EFI/fedora.tmp. That should cover all the subdirs and should be granular enough.

I.e. we need to find the first subdir in EFI and copy that.

This is complicated as we do not know which comes first, we just scan them in diff.additions, diff.changes, diff.removals.

If the parent already in temp dir, then no need to create sub temp dir; if no, will copy to temp dir;
if temp sub dir exists, then need to rename the changed temp sub dir first, to keep temp dir to updated.

This will make the update slow as we do a lot of checking, WDYT?

@travier
Copy link
Member

travier commented Jun 27, 2024

I'm not sure I understand what you mean by "which comes first". I think the idea is more to think of it as the first subdir in the EFI folder instead of thinking about it as "parent" directory.

If I have the following update/addition/removal for example:

  • EFI/fedora/subdir/foo
  • EFI/fedora/subdir/bar/toto
  • EFI/fedora/baz

Then the logic would be:

  • For the file EFI/fedora/subdir/foo as part of the diff:
    • Look if fedora.tmp exists (the first subdir of EFI)
    • Copy fedora to fedora.tmp and remember it
    • Update/remove/add EFI/fedora.tmp/subdir/foo
  • For the file EFI/fedora/subdir/bar/toto as part of the diff:
    • Look if fedora.tmp exists
    • I does, so nothing to copy
    • Update/remove/addEFI/fedora/subdir/bar/toto
  • Repeat for EFI/fedora/baz
  • Once done for all files in the diff, do the rename for the folders that we remembered we copied:
    • Rename exchange fedora.tmp -> fedora, etc.

@HuijingHei
Copy link
Member Author

HuijingHei commented Jun 27, 2024

I'm not sure I understand what you mean by "which comes first"

Sorry for the confusion,
if the changes are EFI/fedora/subdir/newgrub.x64, EFI/fedora/grub.x64,
copy to EFI/fedora/subdir.tmp first, then comes EFI/fedora.tmp, need to remove EFI/fedora/subdir.tmp first, just copy to EFI/fedora.tmp.

if the changes are EFI/fedora/grub.x64, EFI/fedora/subdir/newgrub.x64,
copy to EFI/fedora.tmp first, then comes EFI/fedora/subdir.tmp, check parent EFI/fedora.tmp exists, just using EFI/fedora.tmp.

For the file EFI/fedora/subdir/foo as part of the diff:
Look if fedora.tmp exists (the first subdir of EFI)

If the change is like EFI/foo, the temp dir should be EFI.tmp.

@HuijingHei
Copy link
Member Author

HuijingHei commented Jun 27, 2024

If I have the following update/addition/removal for example:

EFI/fedora/subdir/foo
EFI/fedora/subdir/bar/toto
EFI/fedora/baz

Maybe should scan update/addition/removal in diff to get parent dir, like {EFI/fedora/subdir, EFI/fedora/subdir/bar, EFI/fedora}, then check them, and get only one EFI/fedora, then do copy to temp EFI/fedora.tmp?

@cgwalters
Copy link
Member

Thanks for working on this! It's a really important feature. I hope to get some time by the end of this week or early next week to look at this.

src/filetree.rs Outdated Show resolved Hide resolved
src/filetree.rs Outdated Show resolved Hide resolved
src/filetree.rs Outdated
// waiting for writeback to kick in.
if !opts.skip_sync {
syncfs(destdir)?;
}

Ok(())
}
Copy link
Member

Choose a reason for hiding this comment

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

I think what would really help build confidence here is a few new unit tests. A good way to implement this would be by e.g. checking the ctime (creation time) on files. If we did any extra copying we shouldn't have, then the ctime on the files will have changed when it shouldn't.

Copy link
Member Author

@HuijingHei HuijingHei Jul 1, 2024

Choose a reason for hiding this comment

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

Sorry that I changed the logic, use the first sub dir, if change like fedora/subdir/foo, will copy to feodra.tmp/, this might also copy other files that should not update. Not sure if the current change is OK.

src/filetree.rs Outdated Show resolved Hide resolved
@HuijingHei HuijingHei force-pushed the esp_temp branch 4 times, most recently from d73ed88 to 39cd6aa Compare July 1, 2024 07:42
@travier
Copy link
Member

travier commented Jul 1, 2024

Sorry for the confusion, if the changes are EFI/fedora/subdir/newgrub.x64, EFI/fedora/grub.x64, copy to EFI/fedora/subdir.tmp first, then comes EFI/fedora.tmp, need to remove EFI/fedora/subdir.tmp first, just copy to EFI/fedora.tmp.

If we have EFI/fedora/subdir/newgrub.x64, EFI/fedora/grub.x64, we only need to copy at the fedora level, not at any other sub-levels.

For the file EFI/fedora/subdir/foo as part of the diff:
Look if fedora.tmp exists (the first subdir of EFI)

If the change is like EFI/foo, the temp dir should be EFI.tmp.

If we have a file not in a directory in EFI, then we can copy it to foo.tmp and then act on it and finally rename it. No need to copy the entire EFI.

@travier
Copy link
Member

travier commented Jul 1, 2024

The goal is to make the operation atomic at the vendor (i.e. fedora, BOOT, etc.) directory level, not at the EFI or any other sub-directory level.

@travier
Copy link
Member

travier commented Jul 1, 2024

(I've not looked at the code again yet)

.args(["-a"])
.arg(src)
.arg(dst)
.pre_exec(move || rustix::process::fchdir(rootfd).map_err(Into::into))
Copy link
Member Author

Choose a reason for hiding this comment

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

This can only copy from src to dst at same rootfd, it will not work if src or dst is not under the same rootfd, maybe better to use cap-std in #674

@HuijingHei HuijingHei force-pushed the esp_temp branch 3 times, most recently from e663d4d to 7413a99 Compare July 2, 2024 13:43
src/filetree.rs Outdated Show resolved Hide resolved
src/filetree.rs Outdated
/// "foo" -> ("foo", "foo.tmp")
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
fn get_subdir(path: &Utf8Path) -> Result<(String, String)> {
let buf = if let Some(p) = path.parent().filter(|&v| !v.as_os_str().is_empty()) {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe https://docs.rs/camino/latest/camino/struct.Utf8Path.html#method.components would make it easier than iterating over the parents.

Copy link
Member Author

@HuijingHei HuijingHei Jul 4, 2024

Choose a reason for hiding this comment

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

Agree with you, no need to iter parent, just iter path and get iter.next() is enough.
If using components, need to import module camino::Utf8Component, but I think iter the path is enough. WDYT?

src/filetree.rs Outdated
.local_rename(&pathtmp, path)
.with_context(|| format!("renaming {path}"))?;

// Write new files to temp dir if exists, else write directly in dest
Copy link
Member

Choose a reason for hiding this comment

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

Theoretically, a binary could change behavior if another file is there or not.

So maybe let's move that after the changed files loop to make it safer. Or we add only to a temp dir copy in all cases.

Copy link
Member Author

@HuijingHei HuijingHei Jul 4, 2024

Choose a reason for hiding this comment

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

So maybe let's move that after the changed files loop to make it safer. Or we add only to a temp dir copy in all cases.

Move additions after changed seems easier.
If move to temp dir, it might be different as there are no original copy and finally will use rename() instead of exchange(). But we can change to temp if it is necessary. WDYT?

See Timothée's comment coreos#454 (comment)
logic is like this:
- `cp -a fedora fedora.tmp`
  - We start with a copy to make sure to keep all other files
that we do not explicitly track in bootupd
- Update the content of `fedora.tmp` with the new binaries
- Exchange `fedora.tmp` -> `fedora`
- Remove now "old" `fedora.tmp`

If we have a file not in a directory in `EFI`, then we can copy
it to `foo.tmp` and then act on it and finally rename it. No
need to copy the entire `EFI`.
Fixes coreos#454
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Update the ESP by creating a tmpdir and RENAME_EXCHANGE
3 participants