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:
Nexoniarz 2026-09-12 17:13:59 +02:00
commit 73acc40dbb
6 changed files with 250 additions and 31 deletions

View file

@ -181,6 +181,7 @@ int schfs_kreaddir_from_dev(uint dev, uint64 dir, uint64 start_hash,
unsigned, uint64, void *),
void *arg);
uint64 schfs_kroot(uint dev);
void schfs_lockinfo(int slot, int *locked, int *owner_pid, int *depth);
int schfs_kinfo(char *buf, int cap);
// Write side. Each is one transaction and is durable by the time it returns,
@ -366,7 +367,7 @@ int pipewrite(struct pipe*, char*, int);
// net/unix.c
struct socket;
void sockinit(void);
struct socket* socknew(int domain, int type);
struct socket* socknew(int domain, int type, int proto);
void sockclose(struct socket*);
void sockdup(struct socket*);
int sockread(struct socket*, char*, int);
@ -389,13 +390,19 @@ int socksendto(struct socket*, char *buf, int n,
int sockbind_in(struct socket*, uint addr, ushort port);
int sockconnect_in(struct socket*, uint addr, ushort port);
int sock_domain(struct socket*);
// Socket options. The syscall layer does the user-memory marshalling and these
// do the work, because struct socket's fields live in kernel/net/unix.c and the
// syscall layer deliberately cannot see them.
int sockopt_set(struct socket*, int level, int name, int val);
int sockopt_get(struct socket*, int level, int name, int *out);
// net/inet.c -- AF_INET on lwIP's raw API. Called only from net/unix.c, which
// is where the per-domain dispatch lives.
void inet_init(void);
void* inet_new(int type);
void* inet_new(int type, int proto);
int inet_bind(void *h, uint addr, ushort port);
int inet_listen(void *h, int backlog);
int inet_setopt(void *h, int level, int name, int val);
void* inet_accept(void *h);
int inet_connect(void *h, uint addr, ushort port);
int inet_read(void *h, char *buf, int n);
@ -464,6 +471,7 @@ void* memset(void*, int, uint64);
char* safestrcpy(char*, const char*, int);
int strlen(const char*);
int strnlen(const char*, int);
int strcmp(const char*, const char*);
int strncmp(const char*, const char*, uint64);
char* strncpy(char*, const char*, int);
@ -517,6 +525,7 @@ int diskwrite(struct inode*, uint64, char*, int);
// net_is_up reports whether an address has been obtained.
void netinit(void);
int net_is_up(void);
int net_is_ready(void);
int net_info(char *buf, int cap);
// net/lwip/ -- the port layer and the Ethernet interfaces, called only from