Make a new volume writable by every user in the capsule

A volume was only writable by root. Formatting a fresh volume leaves its
top-level directory owned by root, so anything running as the capsule's
normal user — including file uploads and downloads — was refused, and the
only way to write to your own volume was to reach for sudo.

A volume is now opened up to everyone in the capsule the moment it is first
formatted, the same way /tmp works: anyone can create files in it, and no
one can delete someone else's.

Only ever applied to a volume being formatted for the first time. Re-attaching
a volume you already have leaves its permissions exactly as you set them, so
this never overrides a choice you made. Volumes formatted before this change
stay as they are.
This commit is contained in:
2026-07-25 23:02:29 +06:00
parent 63a9d4a92c
commit 79a70ba9ed
2 changed files with 40 additions and 3 deletions

View File

@ -65,19 +65,23 @@ pub async fn post_mount(
// blkid exit status: 0 = a filesystem/signature is present (leave it alone),
// 2 = no recognized signature (format it), anything else = probe error.
match has_filesystem(&device).await {
// Tracked so the permissions of an existing volume are never touched — only
// a filesystem this call created gets opened up below.
let formatted = match has_filesystem(&device).await {
Ok(true) => {
tracing::info!(device, "volume already has a filesystem; skipping mkfs");
false
}
Ok(false) => {
if let Err(e) = mkfs_ext4(&device).await {
return json_error(StatusCode::INTERNAL_SERVER_ERROR, &e);
}
true
}
Err(e) => {
return json_error(StatusCode::INTERNAL_SERVER_ERROR, &e);
}
}
};
if let Err(e) = tokio::fs::create_dir_all(&req.mount_path).await {
return json_error(
@ -95,6 +99,9 @@ pub async fn post_mount(
.await
{
Ok(out) if out.status.success() => {
if formatted {
open_mount_root(&req.mount_path);
}
tracing::info!(device, mount_path = %req.mount_path, "volume mounted");
no_content()
}
@ -116,6 +123,35 @@ pub async fn post_mount(
}
}
/// open_mount_root relaxes the permissions of a just-formatted volume so every
/// user in the capsule can use it.
///
/// mkfs.ext4 leaves the filesystem root owned by root with mode 0755, which
/// locks out every non-root user — including the template's default user, which
/// is who the file API and exec run as. Without this a caller has to sudo to
/// write to their own volume. 1777 is /tmp's mode: world-writable, with the
/// sticky bit so one user cannot remove another's files.
///
/// Only ever called straight after a first-time format. Re-mounting an existing
/// volume leaves its permissions exactly as its owner last set them.
///
/// Applied to the mounted root rather than the directory created before the
/// mount — that one is hidden underneath and changing it would have no effect.
///
/// Best-effort: a failure here leaves a root-only volume that is still mounted
/// and holds its data, which is not worth failing the whole capsule create over.
fn open_mount_root(mount_path: &str) {
use std::os::unix::fs::PermissionsExt;
if let Err(e) = std::fs::set_permissions(mount_path, std::fs::Permissions::from_mode(0o1777)) {
tracing::warn!(
mount_path,
error = %e,
"failed to relax volume permissions; only root will be able to write to it"
);
}
}
/// POST /volumes/unmount — called best-effort before a graceful capsule destroy.
/// Flushes buffered writes (sync) and unmounts so the backing file is
/// consistent. Idempotent: unmounting an already-unmounted path is fine.

View File

@ -4263,7 +4263,8 @@ components:
description: >
External storage volumes to attach at boot, each given as a volume
ID ("vol-...") or a name ("vl-cache"). Each is mounted at
/mnt/<volume-id> inside the guest. At most 4 per capsule. Volumes
/mnt/<volume-id> inside the guest, formatted on first use and
writable by any user in the capsule. At most 4 per capsule. Volumes
can only be attached at create time and must be detached and owned
by your team. If a volume is already pinned to a host, the capsule is
scheduled onto that host; attaching volumes pinned to different hosts