Skip to content

Commit 20987eb

Browse files
committed
efi: update the ESP by creating a tmpdir and RENAME_EXCHANGE
See Timothée's comment coreos#454 (comment) Reuse `TMP_PREFIX`, logic is like this: - `cp -a fedora .btmp.fedora` - 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 `.btmp.fedora` with the new binaries - Exchange `.btmp.fedora` -> `fedora` - Remove now "old" `.btmp.fedora` If we have a file not in a directory in `EFI`, then we can copy it to `.btmp.foo` and then act on it and finally rename it. No need to copy the entire `EFI`. And use `insert()` instead of `push()` to match `starts_with()` when scanning temp files & dirs. Fixes coreos#454
1 parent 8a472ea commit 20987eb

File tree

3 files changed

+242
-34
lines changed

3 files changed

+242
-34
lines changed

Cargo.lock

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ path = "src/main.rs"
2121
[dependencies]
2222
anyhow = "1.0"
2323
bincode = "1.3.2"
24+
camino = "1.1.7"
2425
chrono = { version = "0.4.38", features = ["serde"] }
2526
clap = { version = "3.2", default-features = false, features = ["cargo", "derive", "std", "suggestions"] }
2627
env_logger = "0.10"

src/filetree.rs

+234-34
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,20 @@
77
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
88
use anyhow::{bail, Context, Result};
99
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
10+
use camino::{Utf8Path, Utf8PathBuf};
11+
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
1012
use openat_ext::OpenatDirExt;
1113
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
1214
use openssl::hash::{Hasher, MessageDigest};
15+
use rustix::fd::BorrowedFd;
1316
use serde::{Deserialize, Serialize};
1417
#[allow(unused_imports)]
1518
use std::collections::{BTreeMap, HashMap, HashSet};
1619
use std::fmt::Display;
1720
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
1821
use std::os::unix::io::AsRawFd;
19-
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
20-
use std::path::Path;
22+
use std::os::unix::process::CommandExt;
23+
use std::process::Command;
2124

2225
/// The prefix we apply to our temporary files.
2326
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
@@ -231,7 +234,7 @@ impl FileTree {
231234
}
232235
}
233236

234-
// Recursively remove all files in the directory that start with our TMP_PREFIX
237+
// Recursively remove all files/dirs in the directory that start with our TMP_PREFIX
235238
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
236239
fn cleanup_tmp(dir: &openat::Dir) -> Result<()> {
237240
for entry in dir.list_dir(".")? {
@@ -245,8 +248,13 @@ fn cleanup_tmp(dir: &openat::Dir) -> Result<()> {
245248

246249
match dir.get_file_type(&entry)? {
247250
openat::SimpleType::Dir => {
248-
let child = dir.sub_dir(name)?;
249-
cleanup_tmp(&child)?;
251+
if name.starts_with(TMP_PREFIX) {
252+
dir.remove_all(name)?;
253+
continue;
254+
} else {
255+
let child = dir.sub_dir(name)?;
256+
cleanup_tmp(&child)?;
257+
}
250258
}
251259
openat::SimpleType::File => {
252260
if name.starts_with(TMP_PREFIX) {
@@ -272,20 +280,44 @@ pub(crate) struct ApplyUpdateOptions {
272280
// Let's just fork off a helper process for now.
273281
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
274282
pub(crate) fn syncfs(d: &openat::Dir) -> Result<()> {
275-
use rustix::fd::BorrowedFd;
276283
use rustix::fs::{Mode, OFlags};
277284
let d = unsafe { BorrowedFd::borrow_raw(d.as_raw_fd()) };
278285
let oflags = OFlags::RDONLY | OFlags::CLOEXEC | OFlags::DIRECTORY;
279286
let d = rustix::fs::openat(d, ".", oflags, Mode::empty())?;
280287
rustix::fs::syncfs(d).map_err(Into::into)
281288
}
282289

290+
/// Copy from src to dst at root dir
291+
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
292+
fn copy_dir(root: &openat::Dir, src: &str, dst: &str) -> Result<()> {
293+
let rootfd = unsafe { BorrowedFd::borrow_raw(root.as_raw_fd()) };
294+
let r = unsafe {
295+
Command::new("cp")
296+
.args(["-a"])
297+
.arg(src)
298+
.arg(dst)
299+
.pre_exec(move || rustix::process::fchdir(rootfd).map_err(Into::into))
300+
.status()?
301+
};
302+
if !r.success() {
303+
anyhow::bail!("Failed to copy {src} to {dst}");
304+
}
305+
log::debug!("Copy {src} to {dst}");
306+
Ok(())
307+
}
308+
309+
/// Get first sub dir and tmp sub dir for the path
310+
/// "fedora/foo/bar" -> ("fedora", ".btmp.fedora")
311+
/// "foo" -> ("foo", ".btmp.foo")
283312
#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
284-
fn tmpname_for_path<P: AsRef<Path>>(path: P) -> std::path::PathBuf {
285-
let path = path.as_ref();
286-
let mut buf = path.file_name().expect("filename").to_os_string();
287-
buf.push(TMP_PREFIX);
288-
path.with_file_name(buf)
313+
fn get_first_dir(path: &Utf8Path) -> Result<(&Utf8Path, String)> {
314+
let first = path
315+
.iter()
316+
.next()
317+
.ok_or_else(|| anyhow::anyhow!("Invalid path: {path}"))?;
318+
let mut tmp = first.to_owned();
319+
tmp.insert_str(0, TMP_PREFIX);
320+
Ok((first.into(), tmp))
289321
}
290322

291323
/// Given two directories, apply a diff generated from srcdir to destdir
@@ -302,41 +334,83 @@ pub(crate) fn apply_diff(
302334
let opts = opts.unwrap_or(&default_opts);
303335
cleanup_tmp(destdir).context("cleaning up temporary files")?;
304336

305-
// Write new and changed files
306-
for pathstr in diff.additions.iter().chain(diff.changes.iter()) {
307-
let path = Path::new(pathstr);
308-
if let Some(parent) = path.parent() {
309-
destdir.ensure_dir_all(parent, DEFAULT_FILE_MODE)?;
337+
let mut updates = HashMap::new();
338+
// Handle removals in temp dir, or remove directly if file not in dir
339+
if !opts.skip_removals {
340+
for pathstr in diff.removals.iter() {
341+
let path = Utf8Path::new(pathstr);
342+
let (first_dir, first_dir_tmp) = get_first_dir(path)?;
343+
let path_tmp;
344+
if first_dir != path {
345+
path_tmp = Utf8Path::new(&first_dir_tmp).join(path.strip_prefix(&first_dir)?);
346+
// copy to temp dir and remember
347+
if !destdir.exists(&first_dir_tmp)? {
348+
copy_dir(destdir, first_dir.as_str(), &first_dir_tmp)?;
349+
updates.insert(first_dir, first_dir_tmp);
350+
}
351+
} else {
352+
path_tmp = path.to_path_buf();
353+
}
354+
destdir
355+
.remove_file(path_tmp.as_std_path())
356+
.with_context(|| format!("removing {:?}", path_tmp))?;
357+
}
358+
}
359+
// Write changed or new files to temp dir or temp file
360+
for pathstr in diff.changes.iter().chain(diff.additions.iter()) {
361+
let path = Utf8Path::new(pathstr);
362+
let (first_dir, first_dir_tmp) = get_first_dir(path)?;
363+
let mut path_tmp = Utf8PathBuf::from(&first_dir_tmp);
364+
if first_dir != path {
365+
if !destdir.exists(&first_dir_tmp)? && destdir.exists(first_dir.as_std_path())? {
366+
// copy to temp dir if not exists
367+
copy_dir(destdir, first_dir.as_str(), &first_dir_tmp)?;
368+
}
369+
path_tmp = path_tmp.join(path.strip_prefix(&first_dir)?);
370+
// ensure new additions dir exists
371+
if let Some(parent) = path_tmp.parent() {
372+
destdir.ensure_dir_all(parent.as_std_path(), DEFAULT_FILE_MODE)?;
373+
}
374+
// remove changed file before copying
375+
destdir
376+
.remove_file_optional(path_tmp.as_std_path())
377+
.with_context(|| format!("removing {path_tmp} before copying"))?;
310378
}
311-
let destp = tmpname_for_path(path);
379+
updates.insert(first_dir, first_dir_tmp);
312380
srcdir
313-
.copy_file_at(path, destdir, destp.as_path())
314-
.with_context(|| format!("writing {}", &pathstr))?;
381+
.copy_file_at(path.as_std_path(), destdir, path_tmp.as_std_path())
382+
.with_context(|| format!("copying {:?} to {:?}", path, path_tmp))?;
383+
}
384+
385+
// do local exchange or rename
386+
for (dst, tmp) in updates.iter() {
387+
let dst = dst.as_std_path();
388+
log::trace!("doing local exchange for {} and {:?}", tmp, dst);
389+
if destdir.exists(dst)? {
390+
destdir
391+
.local_exchange(tmp, dst)
392+
.with_context(|| format!("exchange for {} and {:?}", tmp, dst))?;
393+
} else {
394+
destdir
395+
.local_rename(tmp, dst)
396+
.with_context(|| format!("rename for {} and {:?}", tmp, dst))?;
397+
}
315398
}
316-
// Ensure all of the new files are written persistently to disk
399+
// Ensure all of the updates & changes are written persistently to disk
317400
if !opts.skip_sync {
318401
syncfs(destdir)?;
319402
}
320-
// Now move them all into place (TODO track interruption)
321-
for path in diff.additions.iter().chain(diff.changes.iter()) {
322-
let pathtmp = tmpname_for_path(path);
323-
destdir
324-
.local_rename(&pathtmp, path)
325-
.with_context(|| format!("renaming {path}"))?;
326-
}
327-
if !opts.skip_removals {
328-
for path in diff.removals.iter() {
329-
destdir
330-
.remove_file_optional(path)
331-
.with_context(|| format!("removing {path}"))?;
332-
}
403+
404+
// finally remove the temp dir
405+
for (_, tmp) in updates.iter() {
406+
log::trace!("cleanup: {}", tmp);
407+
destdir.remove_all(tmp).context("clean up temp")?;
333408
}
334409
// A second full filesystem sync to narrow any races rather than
335410
// waiting for writeback to kick in.
336411
if !opts.skip_sync {
337412
syncfs(destdir)?;
338413
}
339-
340414
Ok(())
341415
}
342416

@@ -345,6 +419,7 @@ mod tests {
345419
use super::*;
346420
use std::fs;
347421
use std::io::Write;
422+
use std::path::Path;
348423

349424
fn run_diff(a: &openat::Dir, b: &openat::Dir) -> Result<FileTreeDiff> {
350425
let ta = FileTree::new_from_dir(a)?;
@@ -508,4 +583,129 @@ mod tests {
508583
assert!(!a.join(relp).join("shim.x64").exists());
509584
Ok(())
510585
}
586+
#[test]
587+
fn test_get_first_dir() -> Result<()> {
588+
// test path
589+
let path = Utf8Path::new("foo/subdir/bar");
590+
let (tp, tp_tmp) = get_first_dir(path)?;
591+
assert_eq!(tp, Utf8Path::new("foo"));
592+
assert_eq!(tp_tmp, ".btmp.foo");
593+
// test file
594+
let path = Utf8Path::new("testfile");
595+
let (tp, tp_tmp) = get_first_dir(path)?;
596+
assert_eq!(tp, Utf8Path::new("testfile"));
597+
assert_eq!(tp_tmp, ".btmp.testfile");
598+
Ok(())
599+
}
600+
#[test]
601+
fn test_cleanup_tmp() -> Result<()> {
602+
let tmpd = tempfile::tempdir()?;
603+
let p = tmpd.path();
604+
let pa = p.join("a/.btmp.a");
605+
let pb = p.join(".btmp.b/b");
606+
std::fs::create_dir_all(&pa)?;
607+
std::fs::create_dir_all(&pb)?;
608+
let dp = openat::Dir::open(p)?;
609+
{
610+
let mut buf = dp.write_file("a/foo", 0o644)?;
611+
buf.write_all("foocontents".as_bytes())?;
612+
let mut buf = dp.write_file("a/.btmp.foo", 0o644)?;
613+
buf.write_all("foocontents".as_bytes())?;
614+
let mut buf = dp.write_file(".btmp.b/foo", 0o644)?;
615+
buf.write_all("foocontents".as_bytes())?;
616+
}
617+
assert!(dp.exists("a/.btmp.a")?);
618+
assert!(dp.exists("a/foo")?);
619+
assert!(dp.exists("a/.btmp.foo")?);
620+
assert!(dp.exists("a/.btmp.a")?);
621+
assert!(dp.exists(".btmp.b/b")?);
622+
assert!(dp.exists(".btmp.b/foo")?);
623+
cleanup_tmp(&dp)?;
624+
assert!(!dp.exists("a/.btmp.a")?);
625+
assert!(dp.exists("a/foo")?);
626+
assert!(!dp.exists("a/.btmp.foo")?);
627+
assert!(!dp.exists(".btmp.b")?);
628+
Ok(())
629+
}
630+
#[test]
631+
fn test_apply_with_file() -> Result<()> {
632+
let tmpd = tempfile::tempdir()?;
633+
let p = tmpd.path();
634+
let pa = p.join("a");
635+
let pb = p.join("b");
636+
std::fs::create_dir(&pa)?;
637+
std::fs::create_dir(&pb)?;
638+
let a = openat::Dir::open(&pa)?;
639+
let b = openat::Dir::open(&pb)?;
640+
a.create_dir("foo", 0o755)?;
641+
a.create_dir("bar", 0o755)?;
642+
let foo = Path::new("foo/bar");
643+
let bar = Path::new("bar/foo");
644+
let testfile = "testfile";
645+
{
646+
let mut buf = a.write_file(foo, 0o644)?;
647+
buf.write_all("foocontents".as_bytes())?;
648+
let mut buf = a.write_file(bar, 0o644)?;
649+
buf.write_all("barcontents".as_bytes())?;
650+
let mut buf = a.write_file(testfile, 0o644)?;
651+
buf.write_all("testfilecontents".as_bytes())?;
652+
}
653+
654+
let diff = run_diff(&a, &b)?;
655+
assert_eq!(diff.count(), 3);
656+
b.create_dir("foo", 0o755)?;
657+
{
658+
let mut buf = b.write_file(foo, 0o644)?;
659+
buf.write_all("foocontents".as_bytes())?;
660+
}
661+
let b_btime_foo = fs::metadata(pb.join(foo))?.created()?;
662+
663+
{
664+
let diff = run_diff(&b, &a)?;
665+
assert_eq!(diff.count(), 2);
666+
apply_diff(&a, &b, &diff, None).context("test additional files")?;
667+
assert_eq!(
668+
String::from_utf8(std::fs::read(pb.join(testfile))?)?,
669+
"testfilecontents"
670+
);
671+
assert_eq!(
672+
String::from_utf8(std::fs::read(pb.join(bar))?)?,
673+
"barcontents"
674+
);
675+
// creation time is not changed for unchanged file
676+
let b_btime_foo_new = fs::metadata(pb.join(foo))?.created()?;
677+
assert_eq!(b_btime_foo_new, b_btime_foo);
678+
}
679+
{
680+
fs::write(pa.join(testfile), "newtestfile")?;
681+
fs::write(pa.join(bar), "newbar")?;
682+
let diff = run_diff(&b, &a)?;
683+
assert_eq!(diff.count(), 2);
684+
apply_diff(&a, &b, &diff, None).context("test changed files")?;
685+
assert_eq!(
686+
String::from_utf8(std::fs::read(pb.join(testfile))?)?,
687+
"newtestfile"
688+
);
689+
assert_eq!(String::from_utf8(std::fs::read(pb.join(bar))?)?, "newbar");
690+
// creation time is not changed for unchanged file
691+
let b_btime_foo_new = fs::metadata(pb.join(foo))?.created()?;
692+
assert_eq!(b_btime_foo_new, b_btime_foo);
693+
}
694+
{
695+
a.remove_file(testfile)?;
696+
a.remove_file(bar)?;
697+
let diff = run_diff(&b, &a)?;
698+
assert_eq!(diff.count(), 2);
699+
apply_diff(&a, &b, &diff, None).context("test removed files")?;
700+
assert_eq!(b.exists(testfile)?, false);
701+
assert_eq!(b.exists(bar)?, false);
702+
let diff = run_diff(&b, &a)?;
703+
assert_eq!(diff.count(), 0);
704+
// creation time is not changed for unchanged file
705+
let b_btime_foo_new = fs::metadata(pb.join(foo))?.created()?;
706+
assert_eq!(b_btime_foo_new, b_btime_foo);
707+
}
708+
709+
Ok(())
710+
}
511711
}

0 commit comments

Comments
 (0)