schfs: a format version policy, and refusing is not falling back
uber_valid() answered one question -- valid or not -- and a version mismatch returned the same as a failed checksum. Those two states need opposite handling: A damaged uberblock means STEP OVER IT. That is what the 128-slot ring is for, and the older root is a consistent filesystem. This is crash recovery. A sound uberblock from a newer format means STOP. Falling back mounts a STALE filesystem: every commit made since the volume was upgraded is discarded, the mount reports success, and nobody finds out. Demonstrated rather than asserted. A forged image with two uberblocks -- (version 1, generation 1, root 129) and (version 2, generation 2, root 130) -- made the old code mount root 129 and exit 0, silently rolling the filesystem back to before the upgrade. uber_check() returns BAD / OK / RO / TOO_NEW, and asks its questions in order: is it ours (magic), is it intact (crc), can we read it (version, features), does it make sense (root in range). Compatibility is asked before sanity on purpose -- the fields a sanity check reads are only meaningfully placed if the format is one we know. Plus feature flags in three classes, which is what stops a version number being a blunt instrument: compat (ignore unknown bits), ro_compat (mount read-only), incompat (refuse). The test for which class is one question -- what happens if a driver that does not know this feature writes to the volume? Nothing -> compat. Something goes stale -> ro_compat. Something is unreadable -> incompat. They occupy what was reserved0, so every later field keeps its offset and existing images -- which have zeros there -- read unchanged. schfs_vol_commit memsets the uberblock before filling it in, which is why v1 volumes already say "no features" without having been written by code that knew the field existed. That trick is available exactly once. Four _Static_asserts now pin the layout. Verified on forged images: version bump refuses, incompat refuses, blocksize mismatch refuses, ro_compat mounts the NEWEST root read-only rather than falling back. Also here, because they are the same file: SchFS's transaction owner is a pid alongside the pointer, since a bare struct proc * does not identify a process -- ptable slots are recycled, and an exited owner's pointer compares equal to whichever unrelated process lands in that slot next, at which point klock() skips the lock entirely and two callers share one volume's scratch pages. And schfs_lockinfo(), so /proc/fsstat can report a wedged volume instead of leaving every path asleep in acquiresleep with no way to ask why. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
parent
3eed58fe9b
commit
73acc40dbb
6 changed files with 250 additions and 31 deletions
|
|
@ -106,7 +106,16 @@ struct schfs_mount {
|
|||
struct sleeplock lock;
|
||||
|
||||
// The explicit transaction bracket, per volume for the same reason.
|
||||
//
|
||||
// The pid is carried alongside the pointer, and both must match, because a
|
||||
// `struct proc *` alone does not identify a process: ptable slots are
|
||||
// recycled, so an exited owner's pointer compares equal to whichever
|
||||
// unrelated process next lands in that slot. tx_mine() would then be true for
|
||||
// a process that holds nothing, klock() would skip the lock entirely, and two
|
||||
// callers would share one volume's scratch pages. A recycled slot always
|
||||
// carries a new pid, so the pair is unambiguous.
|
||||
struct proc *tx_owner;
|
||||
int tx_owner_pid;
|
||||
int tx_depth;
|
||||
int tx_dirty;
|
||||
};
|
||||
|
|
@ -124,13 +133,27 @@ static int ksnap_home[SCHFS_SNAP_SLOTS]; // which volume slot
|
|||
static char *ksnapcurblk[SCHFS_SNAP_SLOTS];
|
||||
static char *ksnapdatablk[SCHFS_SNAP_SLOTS];
|
||||
|
||||
// Seconds, for inode timestamps. The kernel has no wall clock wired up, so this
|
||||
// matches what fs.c does for the v1 filesystem rather than inventing a second,
|
||||
// differently-wrong answer.
|
||||
// Seconds since the epoch, for inode timestamps.
|
||||
//
|
||||
// This used to return uptime(), with a comment saying the kernel had no wall
|
||||
// clock. It has had one since time_init() started reading the RTC at boot, and
|
||||
// the stale comment outlived the condition it described -- which is how every
|
||||
// file created after boot ended up stamped 1970.
|
||||
//
|
||||
// Two errors, not one, and the second is the less obvious: uptime() counts
|
||||
// 10 ms *ticks*, and they were being used as *seconds*. A node created 60 ticks
|
||||
// (0.6 s) into boot therefore read back as 00:01:00, so the timestamps were not
|
||||
// merely offset, they ran a hundred times too fast.
|
||||
//
|
||||
// wallclock() is the one source (kernel/core/time.c), so this cannot disagree
|
||||
// with gettimeofday about what time it is -- which was the whole reason that
|
||||
// function exists.
|
||||
static uint64_t
|
||||
know(void)
|
||||
{
|
||||
return (uint64_t)uptime();
|
||||
uint64 sec;
|
||||
wallclock(&sec, 0);
|
||||
return (uint64_t)sec;
|
||||
}
|
||||
|
||||
// Explicit transaction brackets.
|
||||
|
|
@ -220,7 +243,9 @@ writable_for(uint dev)
|
|||
static int
|
||||
tx_mine(struct schfs_mount *m)
|
||||
{
|
||||
return m->tx_owner != 0 && m->tx_owner == myproc();
|
||||
struct proc *p = myproc();
|
||||
return m->tx_owner != 0 && p != 0 &&
|
||||
m->tx_owner == p && m->tx_owner_pid == p->pid;
|
||||
}
|
||||
|
||||
// Take a volume's lock unless this process already holds it through a bracket.
|
||||
|
|
@ -243,6 +268,26 @@ kunlock(uint dev)
|
|||
releasesleep(&m->lock);
|
||||
}
|
||||
|
||||
// The bracket's state, for /proc/fsstat. A wedged filesystem looks exactly like
|
||||
// a slow one from userland -- every path hangs in acquiresleep with no way to
|
||||
// tell whether anything owns the lock. This says so.
|
||||
//
|
||||
// Read without the lock on purpose: the one time it matters is when the lock
|
||||
// cannot be taken, and a reporting path that blocks on the thing it is
|
||||
// reporting on is useless.
|
||||
void
|
||||
schfs_lockinfo(int slot, int *locked, int *owner_pid, int *depth)
|
||||
{
|
||||
if (slot < 0 || slot >= SCHFS_VOL_SLOTS) {
|
||||
*locked = *owner_pid = *depth = -1;
|
||||
return;
|
||||
}
|
||||
struct schfs_mount *m = &kmnt[slot];
|
||||
*locked = m->mounted ? m->lock.locked : -1;
|
||||
*owner_pid = m->tx_owner_pid;
|
||||
*depth = m->tx_depth;
|
||||
}
|
||||
|
||||
void
|
||||
schfs_ktx_open(uint dev)
|
||||
{
|
||||
|
|
@ -253,9 +298,10 @@ schfs_ktx_open(uint dev)
|
|||
return;
|
||||
}
|
||||
acquiresleep(&m->lock);
|
||||
m->tx_owner = myproc();
|
||||
m->tx_depth = 1;
|
||||
m->tx_dirty = 0;
|
||||
m->tx_owner = myproc();
|
||||
m->tx_owner_pid = myproc() ? myproc()->pid : 0;
|
||||
m->tx_depth = 1;
|
||||
m->tx_dirty = 0;
|
||||
}
|
||||
|
||||
int
|
||||
|
|
@ -271,8 +317,9 @@ schfs_ktx_close(uint dev)
|
|||
|
||||
if (m->tx_dirty)
|
||||
r = schfs_vol_commit(&m->vol);
|
||||
m->tx_dirty = 0;
|
||||
m->tx_owner = 0;
|
||||
m->tx_dirty = 0;
|
||||
m->tx_owner = 0;
|
||||
m->tx_owner_pid = 0;
|
||||
releasesleep(&m->lock);
|
||||
return r;
|
||||
}
|
||||
|
|
@ -461,6 +508,15 @@ kmount_slot(int slot, uint bdev)
|
|||
cprintf("schfs: no memory for the allocation map of device %d\n", bdev);
|
||||
return -ENOMEM;
|
||||
}
|
||||
if (r == -3) {
|
||||
// Refused on purpose, and the wording matters: the volume is fine, this
|
||||
// driver is the thing that is out of date. Older uberblocks may well be
|
||||
// readable, and mounting one would quietly roll the filesystem back to
|
||||
// before the upgrade -- so it is not offered.
|
||||
cprintf("schfs: device %d was written by a newer SchFS than this one "
|
||||
"understands; refusing rather than mounting a stale root\n", bdev);
|
||||
return -ENOTSUP;
|
||||
}
|
||||
if (r != 0) {
|
||||
cprintf("schfs: device %d has a SchFS uberblock but an unreadable tree\n",
|
||||
bdev);
|
||||
|
|
|
|||
Loading…
Reference in a new issue