chown only a lock file swarmctl just created and refuse a hard-linked one
This commit is contained in:
parent
be744d45b9
commit
6b93839290
3 changed files with 149 additions and 34 deletions
|
|
@ -56,9 +56,12 @@ What still has to hold, since two processes share the file:
|
|||
hold it until the rename lands, waiting at most 10 seconds. Reading the
|
||||
file first is not enough: both writers spend about a second minting a
|
||||
password between load and write, and the second rename would drop the
|
||||
first writer's user. The lock file is opened `O_NOFOLLOW`, so neither side
|
||||
can be pointed at another file through it. `swarmctl` is root and hands
|
||||
the file it creates to the directory's owner, so the bridge can open it.
|
||||
first writer's user. The lock file is opened `O_NOFOLLOW`, so a symlink
|
||||
cannot point either side at another file. `swarmctl` is root: it refuses an
|
||||
existing lock file with more than one hard link, and it changes the owner
|
||||
of a lock file only when it created that file in the same call, handing it
|
||||
to the directory's owner so the bridge can open it. The bridge opens the
|
||||
lock read-only, which is all `flock` needs.
|
||||
- **Unknown keys survive.** Both carry unmodelled top-level and per-user
|
||||
fields through a round-trip, or whichever writes second would silently
|
||||
delete what the first added.
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::fs::{self, File, Permissions};
|
||||
use std::io::Write as _;
|
||||
use std::os::unix::fs::{OpenOptionsExt as _, PermissionsExt as _};
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
|
|
@ -305,15 +305,19 @@ pub async fn lock_users_file(users_file: &Path, wait: Duration) -> Result<File>
|
|||
lock_name.push(".lock");
|
||||
let lock_path = dir.join(lock_name);
|
||||
|
||||
let flags = rustix::fs::OFlags::NOFOLLOW | rustix::fs::OFlags::NONBLOCK;
|
||||
let file = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.mode(0o600)
|
||||
.custom_flags(flags.bits().cast_signed())
|
||||
.open(&lock_path)
|
||||
.with_context(|| format!("opening {}", lock_path.display()))?;
|
||||
// Read-only: `flock` needs no write access, and a lock file `swarmctl`
|
||||
// (root) left behind readable but not writable must still be lockable.
|
||||
let fd = rustix::fs::open(
|
||||
&lock_path,
|
||||
rustix::fs::OFlags::RDONLY
|
||||
| rustix::fs::OFlags::CREATE
|
||||
| rustix::fs::OFlags::NOFOLLOW
|
||||
| rustix::fs::OFlags::NONBLOCK
|
||||
| rustix::fs::OFlags::CLOEXEC,
|
||||
rustix::fs::Mode::RUSR | rustix::fs::Mode::WUSR,
|
||||
)
|
||||
.with_context(|| format!("opening {}", lock_path.display()))?;
|
||||
let file = File::from(fd);
|
||||
if !file
|
||||
.metadata()
|
||||
.with_context(|| format!("stat {}", lock_path.display()))?
|
||||
|
|
@ -741,6 +745,23 @@ some_future_top_level_key: 7
|
|||
fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
/// A lock file left behind without write access (root-owned after a crash
|
||||
/// in `swarmctl`) can still be locked: `flock` needs only a read fd.
|
||||
#[tokio::test]
|
||||
async fn a_read_only_lock_file_can_still_be_locked() {
|
||||
let dir = tempdir();
|
||||
let users_file = dir.join("users.yml");
|
||||
let lock_path = dir.join("users.yml.lock");
|
||||
fs::write(&lock_path, "").unwrap();
|
||||
fs::set_permissions(&lock_path, Permissions::from_mode(0o444)).unwrap();
|
||||
|
||||
lock_users_file(&users_file, Duration::from_secs(1))
|
||||
.await
|
||||
.expect("lock a read-only lock file");
|
||||
|
||||
fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
/// Two read-modify-writes that each sit between load and publish long
|
||||
/// enough for the other to load: without the lock the second `publish`
|
||||
/// drops the first user.
|
||||
|
|
|
|||
Loading…
Reference in a new issue