schfs: mount a snapshot by name, read-only
`mount -t schfs @golden /mnt` mounts a snapshot. The source argument
finally means something: sys_mount ignored it outright, because there was
one volume and nothing to choose between. `@name` rather than a path
because a snapshot is not a device -- there is no node in /dev for it and
there should not be, since it lives inside the volume already mounted.
A second volume rather than a parameter threaded through all 23 schfs_k*
entry points. Only the read side can ever address it, so the write path
keeps talking to the live volume and needed no change; the four read
entry points grew _dev variants and pick the volume from the device the
inode already carries. One fs_ops table serves both, because every entry
takes an inode and dispatches on it -- a second table would be the same
pointers under a different name.
It needs its own device number: a snapshot's object ids are the *same
numbers* as the live tree's, so one device would make the two views alias
in the inode cache and hand a reader whichever was cached first.
Three things this turned up, none of them new but all of them reachable
for the first time:
- sys_mount refused with `dev == SCHFS_DEV`, meaning "do not mount SchFS
onto SchFS". A snapshot's mount point is necessarily on the live root,
so that refused the entire feature. The real condition is mounting a
device onto *itself*, which is `dev == fsdev`.
- createat allocates an inode and then panics if dirlink fails --
reasonable about a writable filesystem, a crash on a read-only one.
The refusal now happens before anything is allocated. Unlink and
rename already handled a refusal properly.
- a refused create reported ENOENT, so writing to a snapshot said "No
such file or directory" -- the sentence a shell prints for every
create failure. It reports EROFS now.
Verified in the running system against an image built with
--snapshot golden: the mount succeeds, ls lists the snapshot's tree,
/mnt2/etc/motd reads back through it, and a write is refused with
"Read-only file system".
One at a time, and no umount path for it yet: mount_del_by_root removes
the mount table entry, but ksnap_unmount is only called on the failure
paths. schfs-check passes; bootcheck 108 [ok].
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
36614b69c5
commit
3919f1446e
6 changed files with 345 additions and 18 deletions
|
|
@ -164,13 +164,26 @@ at_nameiparent(int dirfd, char *path, char *name)
|
|||
}
|
||||
|
||||
static struct inode*
|
||||
createat(int dirfd, char *path, short type, short major, short minor)
|
||||
createat_x(int dirfd, char *path, short type, short major, short minor,
|
||||
int *rofs)
|
||||
{
|
||||
struct inode *ip, *dp;
|
||||
char name[DIRSIZ];
|
||||
uint off;
|
||||
if (!(dp = at_nameiparent(dirfd, path, name))) return 0;
|
||||
ilock(dp);
|
||||
// Nothing below this point tolerates a refusal: ialloc and dirlink both
|
||||
// panic on failure, because on a writable filesystem they cannot fail for
|
||||
// any reason the caller could have avoided.
|
||||
//
|
||||
// The flag lets the caller report EROFS. Returning 0 alone made a refused
|
||||
// write say "No such file or directory", which is what a shell prints for
|
||||
// every create failure and tells the user nothing about why.
|
||||
if (fs_readonly(dp->dev)) {
|
||||
iunlockput(dp);
|
||||
if (rofs) *rofs = 1;
|
||||
return 0;
|
||||
}
|
||||
if ((ip = dirlookup(dp, name, &off)) != 0) {
|
||||
iunlockput(dp);
|
||||
ilock(ip);
|
||||
|
|
@ -202,10 +215,16 @@ createat(int dirfd, char *path, short type, short major, short minor)
|
|||
return ip;
|
||||
}
|
||||
|
||||
static struct inode*
|
||||
createat(int dirfd, char *path, short type, short major, short minor)
|
||||
{
|
||||
return createat_x(dirfd, path, type, major, minor, 0);
|
||||
}
|
||||
|
||||
static struct inode*
|
||||
create(char *path, short type, short major, short minor)
|
||||
{
|
||||
return createat(AT_FDCWD, path, type, major, minor);
|
||||
return createat_x(AT_FDCWD, path, type, major, minor, 0);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
|
|
@ -449,8 +468,14 @@ addr_t sys_open(void)
|
|||
struct inode *ex = namei(path);
|
||||
if (ex) { iput(ex); end_op(); return -EEXIST; }
|
||||
}
|
||||
ip = (omode & O_EXTENT) ? create(path, T_EXTENT, 0, 0) : create(path, T_FILE, 0, 0);
|
||||
if (!ip) { end_op(); return -ENOENT; }
|
||||
int rofs = 0;
|
||||
ip = createat_x(AT_FDCWD, path,
|
||||
(omode & O_EXTENT) ? T_EXTENT : T_FILE, 0, 0, &rofs);
|
||||
// A refusal because the filesystem is read-only is not "no such file".
|
||||
// A shell prints the same sentence for every create failure, so
|
||||
// returning ENOENT here made writing to a snapshot look like a missing
|
||||
// directory.
|
||||
if (!ip) { end_op(); return rofs ? -EROFS : -ENOENT; }
|
||||
if ((omode & O_TRUNC) && (ip->type == T_FILE || ip->type == T_EXTENT))
|
||||
itrunc(ip);
|
||||
} else {
|
||||
|
|
@ -1371,7 +1396,6 @@ addr_t sys_mount(void)
|
|||
if (argstr(0, &source) < 0 || argstr(1, &target) < 0 ||
|
||||
argstr(2, &fstype) < 0)
|
||||
return -EFAULT;
|
||||
(void)source;
|
||||
|
||||
// proc: a synthetic filesystem, so there is no source device to check and
|
||||
// no volume to have found at boot. `mount -t proc proc /proc`.
|
||||
|
|
@ -1385,12 +1409,46 @@ addr_t sys_mount(void)
|
|||
// told no rather than quietly handed this.
|
||||
if (strncmp(fstype, "schfs", 6) != 0)
|
||||
return -ENODEV;
|
||||
|
||||
// `mount -t schfs @name /mnt` mounts a snapshot, read-only.
|
||||
//
|
||||
// The source finally means something. It was ignored outright before --
|
||||
// there was one volume and nothing to choose between -- and a snapshot is
|
||||
// the first second thing to mount. `@` rather than a path because a
|
||||
// snapshot is not a device: there is no node in /dev for it and there
|
||||
// should not be, since it lives inside the volume that is already mounted.
|
||||
int snapmount = 0;
|
||||
if (source && source[0] == '@') {
|
||||
const char *nm = source + 1;
|
||||
unsigned nlen = 0;
|
||||
while (nm[nlen] && nlen < 64)
|
||||
nlen++;
|
||||
if (nlen == 0)
|
||||
return -EINVAL;
|
||||
|
||||
// Distinct codes, because "resource busy" for every possible failure
|
||||
// tells the caller nothing and cost an hour of guessing once already.
|
||||
int r = schfs_ksnap_mount(nm, nlen);
|
||||
if (r != 0) {
|
||||
cprintf("schfs: snapshot mount '%s' failed (%d)\n", nm, r);
|
||||
switch (r) {
|
||||
case -1: return -ENODEV; /* no volume mounted to look in */
|
||||
case -2: return -EBUSY; /* a snapshot is already mounted */
|
||||
case -3: return -ENOMEM; /* no scratch blocks */
|
||||
default: return -ENOENT; /* no snapshot by that name */
|
||||
}
|
||||
}
|
||||
snapmount = 1;
|
||||
}
|
||||
if (!schfs_kmounted())
|
||||
return -ENODEV;
|
||||
|
||||
uint64 root = schfs_kroot();
|
||||
if (root == 0)
|
||||
uint64 root = snapmount ? schfs_ksnap_root() : schfs_kroot();
|
||||
if (root == 0) {
|
||||
if (snapmount)
|
||||
schfs_ksnap_unmount();
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
begin_op();
|
||||
struct inode *dp = namei(target);
|
||||
|
|
@ -1402,12 +1460,23 @@ addr_t sys_mount(void)
|
|||
iunlockput(dp);
|
||||
end_op();
|
||||
|
||||
// Mounting SchFS onto a directory that is already on SchFS would let an
|
||||
// unmount strand the covered directory behind its own mount.
|
||||
if (dev == SCHFS_DEV)
|
||||
// Mounting a filesystem onto a directory that is already on *it* would let
|
||||
// an unmount strand the covered directory behind its own mount: umount
|
||||
// names the mounted root, and a self-mount makes that ambiguous.
|
||||
//
|
||||
// This read `dev == SCHFS_DEV`, which was the same thing while SchFS was
|
||||
// the only volume there could be. A snapshot is a second device, and its
|
||||
// mount point is necessarily on the live root -- there is nowhere else to
|
||||
// put it -- so the old spelling refused every snapshot mount.
|
||||
uint fsdev = snapmount ? SCHFS_SNAP_DEV : SCHFS_DEV;
|
||||
if (dev == fsdev)
|
||||
return -EBUSY;
|
||||
|
||||
return mount_add(dev, inum, SCHFS_DEV, root) == 0 ? 0 : -EBUSY;
|
||||
if (mount_add(dev, inum, fsdev, root) != 0) {
|
||||
if (snapmount)
|
||||
schfs_ksnap_unmount();
|
||||
return -EBUSY;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
addr_t sys_umount2(void)
|
||||
|
|
|
|||
Loading…
Reference in a new issue