Skip to content

Commit 932b4bb

Browse files
committed
implement flock
1 parent 7df8476 commit 932b4bb

5 files changed

Lines changed: 182 additions & 13 deletions

File tree

Cargo.lock

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

TODO.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,19 @@ reference workload that fails.
4040
RMW in `write_impl`, `do_truncate`, `truncate_expand`, `truncate_shrink`, and
4141
`copy_file_with_header_rewrite` (read lock for `read_impl`).
4242

43-
- [ ] **2. POSIX/BSD locks don't actually lock.**
44-
`flock(2)` is never forwarded (typed-fuse leaves `ops.flock` unset), so `flock()`
45-
is a no-op success and two cargos can hold the registry cache lock
46-
"simultaneously". `fcntl` locks are forwarded (src/fs.rs:1670-1691) but each
47-
`open` creates a new backing fd. Fix: implement `flock` in the typed-fuse fork and
48-
pass through to the backing fd.
43+
- [x] **2. POSIX/BSD locks don't actually lock.**
44+
Fixed: the typed-fuse fork now has an opt-in `flock` callback, wired through
45+
its session, runtime, node trait, and path adapter. `EncFs` opts in and passes
46+
each operation through to `flock(2)` on that open handle's backing fd, so the
47+
backing filesystem preserves BSD open-file-description lock semantics.
48+
49+
POSIX record locks remain kernel-managed: EncFS deliberately does not enable
50+
`FUSE_POSIX_LOCKS`, because forwarding every client's `fcntl` locks from the
51+
daemon process would merge their process identities and produce incorrect
52+
close/unlock behavior.
53+
54+
Regression coverage includes a mountless two-handle test and a live,
55+
cross-process FUSE test in `tests/flock_test.rs` and `tests/live_mount.rs`.
4956

5057
- [ ] **3. `fsync`/`flush`/`fdatasync` are silent no-ops.**
5158
The `PathFilesystem` defaults return `Ok(())` and `EncFs` doesn't override

src/fs.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1710,6 +1710,7 @@ impl PathFilesystem for EncFs {
17101710
// of the INIT reply, so the kernel enforces locks locally with the right
17111711
// per-process semantics.
17121712
const SUPPORTS_POSIX_LOCKS: bool = false;
1713+
const SUPPORTS_FLOCK: bool = true;
17131714
const SUPPORTS_READDIRPLUS: bool = true;
17141715

17151716
fn init(&self, _conn: &mut typed_fuse::ConnInfo) {
@@ -1917,6 +1918,21 @@ impl PathFilesystem for EncFs {
19171918
Ok(self.write_impl(handle, offset, data)? as usize)
19181919
}
19191920

1921+
fn flock(
1922+
&self,
1923+
_path: Option<&Path>,
1924+
handle: &FileHandle,
1925+
operation: i32,
1926+
_caller: &Request,
1927+
) -> Result<(), Errno> {
1928+
let result = unsafe { libc::flock(handle.file.as_raw_fd(), operation) };
1929+
if result == 0 {
1930+
Ok(())
1931+
} else {
1932+
Err(std::io::Error::last_os_error().into())
1933+
}
1934+
}
1935+
19201936
fn create(
19211937
&self,
19221938
parent: &Path,

tests/flock_test.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use encfs::config::Interface;
2+
use encfs::crypto::ssl::SslCipher;
3+
use encfs::fs::EncFs;
4+
use std::ffi::OsStr;
5+
use std::path::Path;
6+
use tempfile::TempDir;
7+
use typed_fuse::{Caller, Errno, PathFilesystem};
8+
9+
fn caller() -> Caller {
10+
Caller {
11+
pid: 1,
12+
gid: 0,
13+
uid: 0,
14+
umask: 0,
15+
}
16+
}
17+
18+
fn make_fs(root: &Path) -> EncFs {
19+
let iface = Interface {
20+
name: "ssl/aes".to_string(),
21+
major: 3,
22+
minor: 0,
23+
age: 0,
24+
};
25+
let mut cipher = SslCipher::new(&iface, 192).unwrap();
26+
cipher.set_key(&[1u8; 24], &[2u8; 16]);
27+
EncFs::new(
28+
root.to_path_buf(),
29+
Box::new(cipher),
30+
encfs::config::EncfsConfig::test_default(),
31+
)
32+
}
33+
34+
#[test]
35+
fn flock_is_forwarded_to_each_backing_file_description() {
36+
let tmp = TempDir::new().unwrap();
37+
let fs = make_fs(tmp.path());
38+
let caller = caller();
39+
let parent = Path::new("");
40+
let name = OsStr::new("locked");
41+
let path = parent.join(name);
42+
43+
let (_, created) = fs
44+
.create(parent, name, 0o644, 0, libc::O_RDWR, &caller)
45+
.unwrap();
46+
fs.release(Some(&path), created.handle, &caller).unwrap();
47+
48+
let first = fs.open(&path, libc::O_RDWR, &caller).unwrap().handle;
49+
let second = fs.open(&path, libc::O_RDWR, &caller).unwrap().handle;
50+
51+
fs.flock(Some(&path), &first, libc::LOCK_EX | libc::LOCK_NB, &caller)
52+
.unwrap();
53+
let error = fs
54+
.flock(Some(&path), &second, libc::LOCK_EX | libc::LOCK_NB, &caller)
55+
.unwrap_err();
56+
assert!(
57+
error == Errno::from_raw(libc::EAGAIN) || error == Errno::from_raw(libc::EWOULDBLOCK),
58+
"second open unexpectedly acquired the lock: {error}"
59+
);
60+
61+
fs.flock(Some(&path), &first, libc::LOCK_UN, &caller)
62+
.unwrap();
63+
fs.flock(Some(&path), &second, libc::LOCK_EX | libc::LOCK_NB, &caller)
64+
.unwrap();
65+
fs.flock(Some(&path), &second, libc::LOCK_UN, &caller)
66+
.unwrap();
67+
68+
fs.release(Some(&path), first, &caller).unwrap();
69+
fs.release(Some(&path), second, &caller).unwrap();
70+
}

tests/live_mount.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,82 @@ fn live_cross_process_posix_locks() -> Result<()> {
835835
Ok(())
836836
}
837837

838+
#[test]
839+
#[ignore]
840+
fn live_flock_child() -> Result<()> {
841+
let Some(path) = std::env::var_os("ENCFS_FLOCK_CHILD_PATH") else {
842+
return Ok(());
843+
};
844+
let expect_blocked = std::env::var_os("ENCFS_FLOCK_EXPECT_BLOCKED").is_some();
845+
let file = OpenOptions::new().read(true).write(true).open(path)?;
846+
let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
847+
let result = if result == 0 {
848+
Ok(())
849+
} else {
850+
Err(std::io::Error::last_os_error())
851+
};
852+
match (result, expect_blocked) {
853+
(Err(error), true)
854+
if error.raw_os_error() == Some(libc::EAGAIN)
855+
|| error.raw_os_error() == Some(libc::EWOULDBLOCK) =>
856+
{
857+
Ok(())
858+
}
859+
(Ok(()), false) => Ok(()),
860+
(result, expected) => {
861+
anyhow::bail!("unexpected child flock result {result:?}; expected blocked={expected}")
862+
}
863+
}
864+
}
865+
866+
#[test]
867+
#[ignore]
868+
fn live_cross_process_flock() -> Result<()> {
869+
require_live();
870+
if !live_enabled() {
871+
return Ok(());
872+
}
873+
let cfg = load_live_config(live::LiveConfigKind::Standard)?;
874+
let mount = MountGuard::mount(cfg, false)?;
875+
let path = mount.mount_point.join("flocked.txt");
876+
fs::write(&path, b"locked")?;
877+
let file = OpenOptions::new().read(true).write(true).open(&path)?;
878+
let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
879+
if result == -1 {
880+
return Err(std::io::Error::last_os_error()).context("parent flock failed");
881+
}
882+
883+
let child = |expect_blocked: bool| -> Result<()> {
884+
let mut command = Command::new(std::env::current_exe()?);
885+
command
886+
.arg("--exact")
887+
.arg("live_flock_child")
888+
.arg("--ignored")
889+
.arg("--test-threads=1")
890+
.env("ENCFS_FLOCK_CHILD_PATH", &path);
891+
if expect_blocked {
892+
command.env("ENCFS_FLOCK_EXPECT_BLOCKED", "1");
893+
}
894+
let output = command.output()?;
895+
anyhow::ensure!(
896+
output.status.success(),
897+
"flock-check child failed (expect_blocked={expect_blocked}): {}\nstdout:\n{}\nstderr:\n{}",
898+
output.status,
899+
String::from_utf8_lossy(&output.stdout),
900+
String::from_utf8_lossy(&output.stderr)
901+
);
902+
Ok(())
903+
};
904+
905+
child(true)?;
906+
let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_UN) };
907+
if result == -1 {
908+
return Err(std::io::Error::last_os_error()).context("parent flock unlock failed");
909+
}
910+
child(false)?;
911+
Ok(())
912+
}
913+
838914
#[test]
839915
#[ignore]
840916
fn live_chmod_utimens_statfs() -> Result<()> {

0 commit comments

Comments
 (0)