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
|
|
@ -66,6 +66,9 @@ int getdents(struct inode*, char *out, int cap, uint64 *poff);
|
||||||
// Mount table: a substitution applied during path resolution. Nothing on disk
|
// Mount table: a substitution applied during path resolution. Nothing on disk
|
||||||
// records a mount, so unmounting is forgetting.
|
// records a mount, so unmounting is forgetting.
|
||||||
void mountinit(void);
|
void mountinit(void);
|
||||||
|
// 1 if the filesystem on this device refuses writes -- a mounted snapshot.
|
||||||
|
// Ask before mutating, not after: the create path panics on a failed dirlink.
|
||||||
|
int fs_readonly(uint dev);
|
||||||
int mount_add(uint dev, uint64 inum, uint fsdev, uint64 root);
|
int mount_add(uint dev, uint64 inum, uint fsdev, uint64 root);
|
||||||
int mount_del_by_root(uint fsdev, uint64 root);
|
int mount_del_by_root(uint fsdev, uint64 root);
|
||||||
struct inode* ialloc(uint, short);
|
struct inode* ialloc(uint, short);
|
||||||
|
|
@ -173,6 +176,23 @@ int schfs_kdirdel(uint64 dir, const char *name, unsigned len);
|
||||||
int schfs_kfree(uint64 ino);
|
int schfs_kfree(uint64 ino);
|
||||||
uint64 schfs_kroot(void);
|
uint64 schfs_kroot(void);
|
||||||
|
|
||||||
|
// A snapshot mounted read-only: the same block device with an older tree root.
|
||||||
|
// The read path takes a device number so the VFS can address either view; there
|
||||||
|
// is no write path, because a snapshot that could be written to would not be
|
||||||
|
// one. See kernel/fs/schfs/kmount.c.
|
||||||
|
int schfs_ksnap_mount(const char *name, unsigned namelen);
|
||||||
|
void schfs_ksnap_unmount(void);
|
||||||
|
uint64 schfs_ksnap_root(void);
|
||||||
|
long schfs_kread_dev(uint dev, uint64 ino, uint64 off, void *buf,
|
||||||
|
unsigned n);
|
||||||
|
int schfs_kstat_dev(uint dev, uint64 ino, struct schfs_inode *out);
|
||||||
|
uint64 schfs_klookup_at_dev(uint dev, uint64 dir, const char *name,
|
||||||
|
unsigned len);
|
||||||
|
int schfs_kreaddir_dev(uint dev, uint64 dir,
|
||||||
|
int (*fn)(uint64, unsigned short, const char *,
|
||||||
|
unsigned, void *),
|
||||||
|
void *arg);
|
||||||
|
|
||||||
// ramdisk.c
|
// ramdisk.c
|
||||||
uint uptime(void);
|
uint uptime(void);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,13 @@ struct inode {
|
||||||
|
|
||||||
// Device number that means "the synthetic /proc". Like SCHFS_DEV, it is
|
// Device number that means "the synthetic /proc". Like SCHFS_DEV, it is
|
||||||
// deliberately nowhere near a real disk's 0 or 1.
|
// deliberately nowhere near a real disk's 0 or 1.
|
||||||
|
// A mounted snapshot. Same block device, a different tree root, read-only.
|
||||||
|
// It needs its own device number because an inode is identified by (dev, inum)
|
||||||
|
// and a snapshot's object ids are the *same numbers* as the live tree's -- one
|
||||||
|
// number would make the two views alias in the inode cache and hand a reader
|
||||||
|
// whichever was cached first.
|
||||||
|
#define SCHFS_SNAP_DEV 66
|
||||||
|
|
||||||
#define PROCFS_DEV 65
|
#define PROCFS_DEV 65
|
||||||
|
|
||||||
// table mapping major device number to
|
// table mapping major device number to
|
||||||
|
|
|
||||||
|
|
@ -275,10 +275,25 @@ extern const struct fs_ops schfs_ops;
|
||||||
// Defined in fs/procfs.c -- the synthetic /proc.
|
// Defined in fs/procfs.c -- the synthetic /proc.
|
||||||
extern const struct fs_ops procfs_ops;
|
extern const struct fs_ops procfs_ops;
|
||||||
|
|
||||||
|
// Is this filesystem read-only?
|
||||||
|
//
|
||||||
|
// A mounted snapshot is. Callers must ask *before* they start mutating: the
|
||||||
|
// create path allocates an inode and then panics if the dirlink fails, which is
|
||||||
|
// a reasonable assumption about a writable filesystem and a crash on a
|
||||||
|
// read-only one.
|
||||||
|
int
|
||||||
|
fs_readonly(uint dev)
|
||||||
|
{
|
||||||
|
return dev == SCHFS_SNAP_DEV;
|
||||||
|
}
|
||||||
|
|
||||||
static const struct fs_ops *
|
static const struct fs_ops *
|
||||||
ops_for(uint dev)
|
ops_for(uint dev)
|
||||||
{
|
{
|
||||||
if (dev == SCHFS_DEV) return &schfs_ops;
|
if (dev == SCHFS_DEV) return &schfs_ops;
|
||||||
|
// A mounted snapshot is the same filesystem seen through an older root, so it
|
||||||
|
// is the same operations; they dispatch on the device themselves.
|
||||||
|
if (dev == SCHFS_SNAP_DEV) return &schfs_ops;
|
||||||
if (dev == PROCFS_DEV) return &procfs_ops;
|
if (dev == PROCFS_DEV) return &procfs_ops;
|
||||||
return &v1_ops;
|
return &v1_ops;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
#include "fs.h"
|
#include "fs.h"
|
||||||
#include "buf.h"
|
#include "buf.h"
|
||||||
|
|
||||||
|
#include "file.h" /* SCHFS_DEV, SCHFS_SNAP_DEV */
|
||||||
#include "schfs_int.h"
|
#include "schfs_int.h"
|
||||||
|
|
||||||
// A SchFS block is 4096 bytes and a buffer-cache block is BSIZE (1024), so each
|
// A SchFS block is 4096 bytes and a buffer-cache block is BSIZE (1024), so each
|
||||||
|
|
@ -52,6 +53,16 @@ static struct schfs_kdev kdev;
|
||||||
static struct schfs_vol kvol;
|
static struct schfs_vol kvol;
|
||||||
static int kmounted;
|
static int kmounted;
|
||||||
|
|
||||||
|
// A mounted snapshot: the same block device with an older tree root, read-only.
|
||||||
|
//
|
||||||
|
// A second volume rather than a parameter threaded through all 23 entry points,
|
||||||
|
// because only the read side can ever address it -- a snapshot cannot be
|
||||||
|
// written to, so every mutation keeps talking to kvol and needs no change.
|
||||||
|
static struct schfs_vol ksnapvol;
|
||||||
|
static int ksnap_mounted;
|
||||||
|
static char *ksnapcurblk;
|
||||||
|
static char *ksnapdatablk;
|
||||||
|
|
||||||
// Block buffers, allocated once at mount.
|
// Block buffers, allocated once at mount.
|
||||||
//
|
//
|
||||||
// Nothing in SchFS may put a block on the stack: a cursor needs a whole
|
// Nothing in SchFS may put a block on the stack: a cursor needs a whole
|
||||||
|
|
@ -250,6 +261,181 @@ schfs_kmount(uint dev)
|
||||||
(int)kvol.generation);
|
(int)kvol.generation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Which volume a device number names. The VFS hands an inode to every fs_ops
|
||||||
|
// call and an inode carries its device, so this is all the dispatch a
|
||||||
|
// read-only second view needs.
|
||||||
|
static struct schfs_vol *
|
||||||
|
vol_for(uint dev)
|
||||||
|
{
|
||||||
|
return dev == SCHFS_SNAP_DEV ? &ksnapvol : &kvol;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *
|
||||||
|
curblk_for(uint dev)
|
||||||
|
{
|
||||||
|
return dev == SCHFS_SNAP_DEV ? ksnapcurblk : kcurblk;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *
|
||||||
|
datablk_for(uint dev)
|
||||||
|
{
|
||||||
|
return dev == SCHFS_SNAP_DEV ? ksnapdatablk : kdatablk;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
mounted_for(uint dev)
|
||||||
|
{
|
||||||
|
return dev == SCHFS_SNAP_DEV ? ksnap_mounted : kmounted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mount a snapshot by name, read-only.
|
||||||
|
//
|
||||||
|
// The snapshot's records live in the *live* tree, so the name is resolved
|
||||||
|
// there; what comes back is a root, and the second volume is a copy of the
|
||||||
|
// first with its tree pointed at it. Nothing else differs -- same device, same
|
||||||
|
// node format, same checksums.
|
||||||
|
int
|
||||||
|
schfs_ksnap_mount(const char *name, unsigned namelen)
|
||||||
|
{
|
||||||
|
if (!kmounted)
|
||||||
|
return -1;
|
||||||
|
if (ksnap_mounted)
|
||||||
|
return -2; /* one at a time, for now */
|
||||||
|
|
||||||
|
if (ksnapcurblk == 0) ksnapcurblk = kalloc();
|
||||||
|
if (ksnapdatablk == 0) ksnapdatablk = kalloc();
|
||||||
|
if (ksnapcurblk == 0 || ksnapdatablk == 0)
|
||||||
|
return -3;
|
||||||
|
|
||||||
|
acquiresleep(&schfs_lock);
|
||||||
|
|
||||||
|
struct schfs_cursor c;
|
||||||
|
schfs_cursor_init(&c, &kvol.tree, kcurblk);
|
||||||
|
|
||||||
|
struct schfs_key k;
|
||||||
|
schfs_mkkey(&k, SCHFS_SNAP_OBJECTID, SCHFS_KEY_SNAP, 0);
|
||||||
|
|
||||||
|
int found = 0;
|
||||||
|
struct schfs_snap sn;
|
||||||
|
int r = schfs_cursor_seek(&c, &k);
|
||||||
|
while (r == 0) {
|
||||||
|
const struct schfs_key *got = schfs_cursor_key(&c);
|
||||||
|
if (!got || got->objectid != SCHFS_SNAP_OBJECTID ||
|
||||||
|
got->type != SCHFS_KEY_SNAP)
|
||||||
|
break;
|
||||||
|
|
||||||
|
unsigned len;
|
||||||
|
const struct schfs_snap *p = (const struct schfs_snap *)
|
||||||
|
schfs_cursor_val(&c, &len);
|
||||||
|
if (p && len == sizeof(*p)) {
|
||||||
|
unsigned i = 0;
|
||||||
|
while (i < namelen && i < SCHFS_SNAP_NAME_MAX && p->name[i] == name[i])
|
||||||
|
i++;
|
||||||
|
if (i == namelen &&
|
||||||
|
(namelen == SCHFS_SNAP_NAME_MAX || p->name[namelen] == 0)) {
|
||||||
|
sn = *p;
|
||||||
|
found = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r = schfs_cursor_next(&c);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
releasesleep(&schfs_lock);
|
||||||
|
return -4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same volume, older root. The device, the space map and the block callbacks
|
||||||
|
// are all the live volume's -- a snapshot is not a separate filesystem, it is
|
||||||
|
// a second way into this one.
|
||||||
|
ksnapvol = kvol;
|
||||||
|
ksnapvol.tree.root = sn.root;
|
||||||
|
ksnapvol.tree.root_crc = sn.root_crc;
|
||||||
|
ksnapvol.tree.root_gen = sn.root_generation;
|
||||||
|
// Its own scratch, or a read through the snapshot would tread on a cursor
|
||||||
|
// the live volume still has positioned.
|
||||||
|
ksnapvol.ctx.a = ksnapcurblk;
|
||||||
|
|
||||||
|
ksnap_mounted = 1;
|
||||||
|
releasesleep(&schfs_lock);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
schfs_ksnap_unmount(void)
|
||||||
|
{
|
||||||
|
acquiresleep(&schfs_lock);
|
||||||
|
ksnap_mounted = 0;
|
||||||
|
releasesleep(&schfs_lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64
|
||||||
|
schfs_ksnap_root(void)
|
||||||
|
{
|
||||||
|
return ksnap_mounted ? ksnapvol.root_objectid : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The read path, addressable per device so a snapshot can be read through the
|
||||||
|
// same VFS. Each is the existing call with the volume chosen rather than
|
||||||
|
// assumed; the originals below stay as the live-volume spelling, because most
|
||||||
|
// callers have no business knowing there is a choice.
|
||||||
|
long
|
||||||
|
schfs_kread_dev(uint dev, uint64_t ino, uint64_t off, void *buf, unsigned n)
|
||||||
|
{
|
||||||
|
if (!mounted_for(dev))
|
||||||
|
return -1;
|
||||||
|
acquiresleep(&schfs_lock);
|
||||||
|
struct schfs_cursor c;
|
||||||
|
schfs_cursor_init(&c, &vol_for(dev)->tree, curblk_for(dev));
|
||||||
|
long r = schfs_file_read(vol_for(dev), &c, ino, off, buf, n, datablk_for(dev));
|
||||||
|
releasesleep(&schfs_lock);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
schfs_kstat_dev(uint dev, uint64_t ino, struct schfs_inode *out)
|
||||||
|
{
|
||||||
|
if (!mounted_for(dev))
|
||||||
|
return -1;
|
||||||
|
acquiresleep(&schfs_lock);
|
||||||
|
struct schfs_cursor c;
|
||||||
|
schfs_cursor_init(&c, &vol_for(dev)->tree, curblk_for(dev));
|
||||||
|
int r = schfs_inode_get(vol_for(dev), &c, ino, out);
|
||||||
|
releasesleep(&schfs_lock);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64
|
||||||
|
schfs_klookup_at_dev(uint dev, uint64_t dir, const char *name, unsigned len)
|
||||||
|
{
|
||||||
|
if (!mounted_for(dev))
|
||||||
|
return 0;
|
||||||
|
acquiresleep(&schfs_lock);
|
||||||
|
struct schfs_cursor c;
|
||||||
|
schfs_cursor_init(&c, &vol_for(dev)->tree, curblk_for(dev));
|
||||||
|
uint64_t ino = 0;
|
||||||
|
uint16_t type = 0;
|
||||||
|
int r = schfs_dir_lookup(vol_for(dev), &c, dir, name, len, &ino, &type);
|
||||||
|
releasesleep(&schfs_lock);
|
||||||
|
return r == 0 ? ino : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
schfs_kreaddir_dev(uint dev, uint64_t dir,
|
||||||
|
int (*fn)(uint64_t, uint16_t, const char *, unsigned, void *),
|
||||||
|
void *arg)
|
||||||
|
{
|
||||||
|
if (!mounted_for(dev))
|
||||||
|
return -1;
|
||||||
|
acquiresleep(&schfs_lock);
|
||||||
|
struct schfs_cursor c;
|
||||||
|
schfs_cursor_init(&c, &vol_for(dev)->tree, curblk_for(dev));
|
||||||
|
int r = schfs_dir_iterate(vol_for(dev), &c, dir, fn, arg);
|
||||||
|
releasesleep(&schfs_lock);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
schfs_kmounted(void)
|
schfs_kmounted(void)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
#include "fs.h"
|
#include "fs.h"
|
||||||
#include "buf.h"
|
#include "buf.h"
|
||||||
#include "file.h"
|
#include "file.h"
|
||||||
|
#include "errno.h"
|
||||||
|
|
||||||
#include "schfs_int.h"
|
#include "schfs_int.h"
|
||||||
|
|
||||||
|
|
@ -75,7 +76,7 @@ schfs_vfs_iload(struct inode *ip)
|
||||||
{
|
{
|
||||||
struct schfs_inode in;
|
struct schfs_inode in;
|
||||||
|
|
||||||
if (schfs_kstat(ip->inum, &in) != 0) {
|
if (schfs_kstat_dev(ip->dev, ip->inum, &in) != 0) {
|
||||||
// An unreadable inode on the root filesystem is a panic in v1 because the
|
// An unreadable inode on the root filesystem is a panic in v1 because the
|
||||||
// system cannot continue without it. Here it is a mounted volume that may
|
// system cannot continue without it. Here it is a mounted volume that may
|
||||||
// simply be damaged, so report an empty file and let the caller fail
|
// simply be damaged, so report an empty file and let the caller fail
|
||||||
|
|
@ -102,11 +103,23 @@ schfs_vfs_iload(struct inode *ip)
|
||||||
ip->valid = 1;
|
ip->valid = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Everything below this point mutates, and a snapshot is a view of a tree that
|
||||||
|
// has already been committed -- there is nowhere for a change to go. Refusing
|
||||||
|
// here rather than in each schfs_k* call keeps the check next to the reason.
|
||||||
|
static int
|
||||||
|
readonly(const struct inode *ip)
|
||||||
|
{
|
||||||
|
return ip->dev == SCHFS_SNAP_DEV;
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
schfs_vfs_istore(struct inode *ip)
|
schfs_vfs_istore(struct inode *ip)
|
||||||
{
|
{
|
||||||
struct schfs_inode in;
|
struct schfs_inode in;
|
||||||
|
|
||||||
|
if (readonly(ip))
|
||||||
|
return;
|
||||||
|
|
||||||
// Read-modify-write rather than build-from-scratch: the on-disk record holds
|
// Read-modify-write rather than build-from-scratch: the on-disk record holds
|
||||||
// fields the VFS has no slot for (uid, gid, the real mode bits, the
|
// fields the VFS has no slot for (uid, gid, the real mode bits, the
|
||||||
// generation), and rebuilding it here would quietly zero every one of them.
|
// generation), and rebuilding it here would quietly zero every one of them.
|
||||||
|
|
@ -130,6 +143,8 @@ schfs_vfs_istore(struct inode *ip)
|
||||||
static void
|
static void
|
||||||
schfs_vfs_ifree(struct inode *ip)
|
schfs_vfs_ifree(struct inode *ip)
|
||||||
{
|
{
|
||||||
|
if (readonly(ip))
|
||||||
|
return;
|
||||||
schfs_kfree(ip->inum);
|
schfs_kfree(ip->inum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,7 +183,7 @@ schfs_vfs_read(struct inode *ip, int user_dst, uint64 dst, uint64 off, uint n)
|
||||||
if (want > BOUNCE)
|
if (want > BOUNCE)
|
||||||
want = BOUNCE;
|
want = BOUNCE;
|
||||||
|
|
||||||
long got = schfs_kread(ip->inum, off + done, tmp, want);
|
long got = schfs_kread_dev(ip->dev, ip->inum, off + done, tmp, want);
|
||||||
if (got < 0) { kfree(tmp); return done ? (int)done : -1; }
|
if (got < 0) { kfree(tmp); return done ? (int)done : -1; }
|
||||||
if (got == 0) break; /* end of file */
|
if (got == 0) break; /* end of file */
|
||||||
|
|
||||||
|
|
@ -190,6 +205,9 @@ schfs_vfs_read(struct inode *ip, int user_dst, uint64 dst, uint64 off, uint n)
|
||||||
static int
|
static int
|
||||||
schfs_vfs_write(struct inode *ip, int user_src, uint64 src, uint64 off, uint n)
|
schfs_vfs_write(struct inode *ip, int user_src, uint64 src, uint64 off, uint n)
|
||||||
{
|
{
|
||||||
|
if (readonly(ip))
|
||||||
|
return -EROFS;
|
||||||
|
|
||||||
char *tmp = kalloc();
|
char *tmp = kalloc();
|
||||||
if (tmp == 0)
|
if (tmp == 0)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
@ -222,6 +240,8 @@ schfs_vfs_write(struct inode *ip, int user_src, uint64 src, uint64 off, uint n)
|
||||||
static void
|
static void
|
||||||
schfs_vfs_truncate(struct inode *ip, uint64 newsize)
|
schfs_vfs_truncate(struct inode *ip, uint64 newsize)
|
||||||
{
|
{
|
||||||
|
if (readonly(ip))
|
||||||
|
return;
|
||||||
if (schfs_ktruncate(ip->inum, newsize) == 0)
|
if (schfs_ktruncate(ip->inum, newsize) == 0)
|
||||||
ip->size = newsize;
|
ip->size = newsize;
|
||||||
}
|
}
|
||||||
|
|
@ -246,13 +266,13 @@ schfs_vfs_dirlookup(struct inode *dp, char *name, uint *poff)
|
||||||
|
|
||||||
if (namelen == 2 && name[0] == '.' && name[1] == '.') {
|
if (namelen == 2 && name[0] == '.' && name[1] == '.') {
|
||||||
struct schfs_inode in;
|
struct schfs_inode in;
|
||||||
if (schfs_kstat(dp->inum, &in) != 0)
|
if (schfs_kstat_dev(dp->dev, dp->inum, &in) != 0)
|
||||||
return 0;
|
return 0;
|
||||||
if (poff) *poff = 0;
|
if (poff) *poff = 0;
|
||||||
return iget(dp->dev, in.parent ? in.parent : dp->inum);
|
return iget(dp->dev, in.parent ? in.parent : dp->inum);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64 ino = schfs_klookup_at(dp->inum, name, namelen);
|
uint64 ino = schfs_klookup_at_dev(dp->dev, dp->inum, name, namelen);
|
||||||
if (ino == 0)
|
if (ino == 0)
|
||||||
return 0;
|
return 0;
|
||||||
// poff is v1's byte offset into the directory file. SchFS finds an entry by
|
// poff is v1's byte offset into the directory file. SchFS finds an entry by
|
||||||
|
|
@ -265,13 +285,16 @@ schfs_vfs_dirlookup(struct inode *dp, char *name, uint *poff)
|
||||||
static int
|
static int
|
||||||
schfs_vfs_dirlink(struct inode *dp, char *name, uint64 inum)
|
schfs_vfs_dirlink(struct inode *dp, char *name, uint64 inum)
|
||||||
{
|
{
|
||||||
|
if (readonly(dp))
|
||||||
|
return -EROFS;
|
||||||
|
|
||||||
unsigned namelen = 0;
|
unsigned namelen = 0;
|
||||||
while (name[namelen]) namelen++;
|
while (name[namelen]) namelen++;
|
||||||
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
|
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
struct schfs_inode in;
|
struct schfs_inode in;
|
||||||
if (schfs_kstat(inum, &in) != 0)
|
if (schfs_kstat_dev(dp->dev, inum, &in) != 0)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
int r = schfs_kdiradd(dp->inum, name, namelen, inum, in.type);
|
int r = schfs_kdiradd(dp->inum, name, namelen, inum, in.type);
|
||||||
|
|
@ -291,6 +314,9 @@ schfs_vfs_dirlink(struct inode *dp, char *name, uint64 inum)
|
||||||
static int
|
static int
|
||||||
schfs_vfs_dirunlink(struct inode *dp, char *name, uint off)
|
schfs_vfs_dirunlink(struct inode *dp, char *name, uint off)
|
||||||
{
|
{
|
||||||
|
if (readonly(dp))
|
||||||
|
return -EROFS;
|
||||||
|
|
||||||
(void)off;
|
(void)off;
|
||||||
unsigned namelen = 0;
|
unsigned namelen = 0;
|
||||||
while (name[namelen]) namelen++;
|
while (name[namelen]) namelen++;
|
||||||
|
|
@ -399,7 +425,7 @@ schfs_vfs_getdents(struct inode *dp, char *out, int cap, uint64 *poff)
|
||||||
g.written += reclen;
|
g.written += reclen;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schfs_kreaddir(dp->inum, gd_entry, &g) != 0 && g.written == 0)
|
if (schfs_kreaddir_dev(dp->dev, dp->inum, gd_entry, &g) != 0 && g.written == 0)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
*poff = g.index;
|
*poff = g.index;
|
||||||
|
|
@ -408,6 +434,10 @@ schfs_vfs_getdents(struct inode *dp, char *out, int cap, uint64 *poff)
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
// One table, not two. Every entry already takes an inode and every inode
|
||||||
|
// carries its device, so the same functions serve the live volume and a mounted
|
||||||
|
// snapshot -- the read path picks the volume, the write path refuses. A second
|
||||||
|
// table would be the same pointers with a different name on it.
|
||||||
const struct fs_ops schfs_ops = {
|
const struct fs_ops schfs_ops = {
|
||||||
.name = "schfs",
|
.name = "schfs",
|
||||||
.iload = schfs_vfs_iload,
|
.iload = schfs_vfs_iload,
|
||||||
|
|
|
||||||
|
|
@ -164,13 +164,26 @@ at_nameiparent(int dirfd, char *path, char *name)
|
||||||
}
|
}
|
||||||
|
|
||||||
static struct inode*
|
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;
|
struct inode *ip, *dp;
|
||||||
char name[DIRSIZ];
|
char name[DIRSIZ];
|
||||||
uint off;
|
uint off;
|
||||||
if (!(dp = at_nameiparent(dirfd, path, name))) return 0;
|
if (!(dp = at_nameiparent(dirfd, path, name))) return 0;
|
||||||
ilock(dp);
|
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) {
|
if ((ip = dirlookup(dp, name, &off)) != 0) {
|
||||||
iunlockput(dp);
|
iunlockput(dp);
|
||||||
ilock(ip);
|
ilock(ip);
|
||||||
|
|
@ -202,10 +215,16 @@ createat(int dirfd, char *path, short type, short major, short minor)
|
||||||
return ip;
|
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*
|
static struct inode*
|
||||||
create(char *path, short type, short major, short minor)
|
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);
|
struct inode *ex = namei(path);
|
||||||
if (ex) { iput(ex); end_op(); return -EEXIST; }
|
if (ex) { iput(ex); end_op(); return -EEXIST; }
|
||||||
}
|
}
|
||||||
ip = (omode & O_EXTENT) ? create(path, T_EXTENT, 0, 0) : create(path, T_FILE, 0, 0);
|
int rofs = 0;
|
||||||
if (!ip) { end_op(); return -ENOENT; }
|
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))
|
if ((omode & O_TRUNC) && (ip->type == T_FILE || ip->type == T_EXTENT))
|
||||||
itrunc(ip);
|
itrunc(ip);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1371,7 +1396,6 @@ addr_t sys_mount(void)
|
||||||
if (argstr(0, &source) < 0 || argstr(1, &target) < 0 ||
|
if (argstr(0, &source) < 0 || argstr(1, &target) < 0 ||
|
||||||
argstr(2, &fstype) < 0)
|
argstr(2, &fstype) < 0)
|
||||||
return -EFAULT;
|
return -EFAULT;
|
||||||
(void)source;
|
|
||||||
|
|
||||||
// proc: a synthetic filesystem, so there is no source device to check and
|
// 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`.
|
// 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.
|
// told no rather than quietly handed this.
|
||||||
if (strncmp(fstype, "schfs", 6) != 0)
|
if (strncmp(fstype, "schfs", 6) != 0)
|
||||||
return -ENODEV;
|
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())
|
if (!schfs_kmounted())
|
||||||
return -ENODEV;
|
return -ENODEV;
|
||||||
|
|
||||||
uint64 root = schfs_kroot();
|
uint64 root = snapmount ? schfs_ksnap_root() : schfs_kroot();
|
||||||
if (root == 0)
|
if (root == 0) {
|
||||||
|
if (snapmount)
|
||||||
|
schfs_ksnap_unmount();
|
||||||
return -ENODEV;
|
return -ENODEV;
|
||||||
|
}
|
||||||
|
|
||||||
begin_op();
|
begin_op();
|
||||||
struct inode *dp = namei(target);
|
struct inode *dp = namei(target);
|
||||||
|
|
@ -1402,12 +1460,23 @@ addr_t sys_mount(void)
|
||||||
iunlockput(dp);
|
iunlockput(dp);
|
||||||
end_op();
|
end_op();
|
||||||
|
|
||||||
// Mounting SchFS onto a directory that is already on SchFS would let an
|
// Mounting a filesystem onto a directory that is already on *it* would let
|
||||||
// unmount strand the covered directory behind its own mount.
|
// an unmount strand the covered directory behind its own mount: umount
|
||||||
if (dev == SCHFS_DEV)
|
// 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 -EBUSY;
|
||||||
|
if (mount_add(dev, inum, fsdev, root) != 0) {
|
||||||
return mount_add(dev, inum, SCHFS_DEV, root) == 0 ? 0 : -EBUSY;
|
if (snapmount)
|
||||||
|
schfs_ksnap_unmount();
|
||||||
|
return -EBUSY;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
addr_t sys_umount2(void)
|
addr_t sys_umount2(void)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue