Skip to content

Commit be778a2

Browse files
repository: Atomically replace refs/ symlinks
Naming a new version of an image the same as the old version is pretty standard, and this is currently giving EEXIST errors. We fix this by doing an atomic symlink + rename operation to replace the symlink if it doesn't already have the expected value. Drop our existing Repository::ensure_symlink() function and use the new function for that as well. Co-authored-by: Alexander Larsson <[email protected]> Signed-off-by: Alexander Larsson <[email protected]> Signed-off-by: Allison Karlitskaya <[email protected]>
1 parent 7696b0a commit be778a2

File tree

3 files changed

+68
-16
lines changed

3 files changed

+68
-16
lines changed

crates/composefs/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ tempfile = { version = "3.8.0", optional = true, default-features = false }
2929
xxhash-rust = { version = "0.8.2", default-features = false, features = ["xxh32"] }
3030
zerocopy = { version = "0.8.0", default-features = false, features = ["derive", "std"] }
3131
zstd = { version = "0.13.0", default-features = false }
32+
rand = { version = "0.9.1", default-features = true }
3233

3334
[dev-dependencies]
3435
insta = "1.42.2"

crates/composefs/src/repository.rs

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ use anyhow::{bail, ensure, Context, Result};
1212
use once_cell::sync::OnceCell;
1313
use rustix::{
1414
fs::{
15-
fdatasync, flock, linkat, mkdirat, open, openat, readlinkat, symlinkat, AtFlags, Dir,
16-
FileType, FlockOperation, Mode, OFlags, CWD,
15+
fdatasync, flock, linkat, mkdirat, open, openat, readlinkat, AtFlags, Dir, FileType,
16+
FlockOperation, Mode, OFlags, CWD,
1717
},
1818
io::{Errno, Result as ErrnoResult},
1919
};
@@ -26,7 +26,7 @@ use crate::{
2626
},
2727
mount::{composefs_fsmount, mount_at},
2828
splitstream::{DigestMap, SplitStreamReader, SplitStreamWriter},
29-
util::{filter_errno, proc_self_fd, Sha256Digest},
29+
util::{filter_errno, proc_self_fd, replace_symlinkat, Sha256Digest},
3030
};
3131

3232
/// Call openat() on the named subdirectory of "dirfd", possibly creating it first.
@@ -309,7 +309,7 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
309309
let stream_path = format!("streams/{}", hex::encode(sha256));
310310
let object_id = writer.done()?;
311311
let object_path = Self::format_object_path(&object_id);
312-
self.ensure_symlink(&stream_path, &object_path)?;
312+
self.symlink(&stream_path, &object_path)?;
313313

314314
if let Some(name) = reference {
315315
let reference_path = format!("streams/refs/{name}");
@@ -358,7 +358,7 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
358358
let object_id = writer.done()?;
359359

360360
let object_path = Self::format_object_path(&object_id);
361-
self.ensure_symlink(&stream_path, &object_path)?;
361+
self.symlink(&stream_path, &object_path)?;
362362
object_id
363363
}
364364
};
@@ -414,7 +414,7 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
414414
let object_path = Self::format_object_path(&object_id);
415415
let image_path = format!("images/{}", object_id.to_hex());
416416

417-
self.ensure_symlink(&image_path, &object_path)?;
417+
self.symlink(&image_path, &object_path)?;
418418

419419
if let Some(reference) = name {
420420
let ref_path = format!("images/refs/{reference}");
@@ -499,14 +499,8 @@ impl<ObjectID: FsVerityHashValue> Repository<ObjectID> {
499499
relative.push(target_component);
500500
}
501501

502-
symlinkat(relative, &self.repository, name)
503-
}
504-
505-
pub fn ensure_symlink<P: AsRef<Path>>(&self, name: P, target: &str) -> ErrnoResult<()> {
506-
self.symlink(name, target).or_else(|e| match e {
507-
Errno::EXIST => Ok(()),
508-
_ => Err(e),
509-
})
502+
// Atomically replace existing symlink
503+
replace_symlinkat(&relative, &self.repository, name)
510504
}
511505

512506
fn read_symlink_hashvalue(dirfd: &OwnedFd, name: &CStr) -> Result<ObjectID> {

crates/composefs/src/util.rs

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
1+
use rand::{distr::Alphanumeric, Rng};
12
use std::{
23
io::{Error, ErrorKind, Read, Result},
3-
os::fd::{AsFd, AsRawFd},
4+
os::{
5+
fd::{AsFd, AsRawFd, OwnedFd},
6+
unix::ffi::OsStrExt,
7+
},
8+
path::Path,
49
};
510

6-
use rustix::io::{Errno, Result as ErrnoResult};
11+
use rustix::{
12+
fs::{readlinkat, renameat, symlinkat, unlinkat, AtFlags},
13+
io::{Errno, Result as ErrnoResult},
14+
};
715
use tokio::io::{AsyncRead, AsyncReadExt};
816

917
/// Formats a string like "/proc/self/fd/3" for the given fd. This can be used to work with kernel
@@ -109,6 +117,55 @@ pub(crate) fn filter_errno<T>(
109117
}
110118
}
111119

120+
fn generate_tmpname(prefix: &str) -> String {
121+
let rand_string: String = rand::rng()
122+
.sample_iter(&Alphanumeric)
123+
.take(12)
124+
.map(char::from)
125+
.collect();
126+
format!("{}{}", prefix, rand_string)
127+
}
128+
129+
pub(crate) fn replace_symlinkat(
130+
target: impl AsRef<Path>,
131+
dirfd: &OwnedFd,
132+
name: impl AsRef<Path>,
133+
) -> ErrnoResult<()> {
134+
let name = name.as_ref();
135+
let target = target.as_ref();
136+
137+
// Step 1: try to create the symlink
138+
if filter_errno(symlinkat(target, dirfd, name), Errno::EXIST)?.is_some() {
139+
return Ok(());
140+
};
141+
142+
// Step 2: the symlink already exists. Maybe it already has the correct target?
143+
if let Some(current_target) = filter_errno(readlinkat(dirfd, name, []), Errno::NOENT)? {
144+
if current_target.into_bytes() == target.as_os_str().as_bytes() {
145+
return Ok(());
146+
}
147+
}
148+
149+
// Step 3: full atomic replace path
150+
for _ in 0..16 {
151+
let tmp_name = generate_tmpname(".symlink-");
152+
if filter_errno(symlinkat(target, dirfd, &tmp_name), Errno::EXIST)?.is_none() {
153+
// This temporary filename already exists, try another
154+
continue;
155+
}
156+
157+
match renameat(dirfd, &tmp_name, dirfd, name) {
158+
Ok(_) => return Ok(()),
159+
Err(e) => {
160+
let _ = unlinkat(dirfd, tmp_name, AtFlags::empty());
161+
return Err(e);
162+
}
163+
}
164+
}
165+
166+
Err(Errno::EXIST)
167+
}
168+
112169
#[cfg(test)]
113170
mod test {
114171
use similar_asserts::assert_eq;

0 commit comments

Comments
 (0)