diff --git a/include/defs.h b/include/defs.h index acb6406..b985e0c 100644 --- a/include/defs.h +++ b/include/defs.h @@ -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 diff --git a/include/errno.h b/include/errno.h index f29bd5c..a29d184 100644 --- a/include/errno.h +++ b/include/errno.h @@ -57,6 +57,9 @@ #define EISCONN 106 // Transport endpoint is already connected #define ENODATA 61 // No data available -- no such attribute #define ENOBUFS 105 // No buffer space available +#define ENOTSUP 95 // Operation not supported +#define ENETDOWN 100 // Network is down +#define ENETUNREACH 101 // Network is unreachable #define ENOTCONN 107 // Transport endpoint is not connected #define ECONNREFUSED 111 // Connection refused diff --git a/include/schfs.h b/include/schfs.h index 452bc20..25b5928 100644 --- a/include/schfs.h +++ b/include/schfs.h @@ -136,7 +136,52 @@ struct schfs_item { // the boot block and the uberblock ring. #define SCHFS_FIRST_BLOCK (SCHFS_UBER_START + SCHFS_UBER_COUNT) +// --------------------------------------------------------------------------- +// The on-disk format version, and what a bump means +// +// The rule, so that "can this driver read this volume" has an answer that does +// not depend on reading the release notes: +// +// * SCHFS_VERSION changes only when the *frame* changes -- the uberblock, the +// node header, the key, the things every reader must understand before it +// can parse anything at all. A driver refuses a version above its own, +// loudly, and says so; it does not guess and it does not fall back to an +// older uberblock, because an older uberblock is a stale filesystem. +// +// * Everything else is a FEATURE FLAG, and this is what keeps the version +// number from being a blunt instrument. Adding a record type, an index +// kind or an optional field does not break a reader that ignores it, so it +// must not cost every older reader the volume. +// +// Three classes, the arrangement ext4 and btrfs both arrived at because it is +// the one that works: +// +// compat Unknown bits may be ignored. Mount read-write regardless. +// ro_compat Unknown bits mean the volume may be MOUNTED READ-ONLY. The +// writer would maintain a structure this driver does not know +// about, so writing would silently invalidate it. +// incompat Unknown bits mean REFUSE. The volume cannot be parsed correctly +// without understanding them. +// +// Choosing the class is the whole design decision, and the test is simple: +// what happens if a driver that does not know this feature writes to the +// volume? Nothing -> compat. Something on disk goes stale -> ro_compat. +// Something is unreadable -> incompat. +// +// All three words are zero on every volume written so far: schfs_vol_commit +// memsets the uberblock before filling it in, so v1 images already say "no +// features" without having been written by code that knew the field existed. +// --------------------------------------------------------------------------- + #define SCHFS_VERSION 1 + +// No features are defined yet. The masks exist so that the *checks* exist: +// adding a flag later is one bit here, and every driver already refuses +// correctly for the bits it does not have. +#define SCHFS_FEAT_COMPAT_SUPP 0u +#define SCHFS_FEAT_RO_COMPAT_SUPP 0u +#define SCHFS_FEAT_INCOMPAT_SUPP 0u + #define SCHFS_LABEL_MAX 32 // A retained tree root. @@ -180,14 +225,36 @@ struct schfs_uberblock { // tree at mount rather than stored, so this is what the volume believed at // commit time — useful for df before anything is mounted, never trusted. uint64_t free_blocks; - uint64_t reserved0; + // Feature flags. See the block comment above SCHFS_VERSION for what the + // three classes mean and how to choose between them. + // + // These occupy what was one `uint64_t reserved0`, so the offset of every + // field after them is unchanged and a v1 image -- which has zeros here -- + // reads identically. That is the only reason this could be added without a + // version bump, and it is the last time that trick is available. + uint32_t feature_incompat; + uint32_t feature_ro_compat; uint64_t next_objectid; uint64_t root_objectid; // normally SCHFS_ROOT_OBJECTID uint8_t uuid[16]; char label[SCHFS_LABEL_MAX]; - uint8_t pad[SCHFS_BLKSIZE - 136]; + uint32_t feature_compat; + uint8_t pad[SCHFS_BLKSIZE - 140]; }; +// The uberblock is one block, and every field above is at a fixed offset in it. +// Both are on-disk facts rather than C conveniences, so the compiler checks +// them: a padding change or a reordered field is a format change that would +// otherwise be found by a volume that no longer mounts. +_Static_assert(sizeof(struct schfs_uberblock) == SCHFS_BLKSIZE, + "uberblock must be exactly one block"); +_Static_assert(__builtin_offsetof(struct schfs_uberblock, version) == 8, + "uberblock version moved -- older drivers read this offset"); +_Static_assert(__builtin_offsetof(struct schfs_uberblock, feature_incompat) == 64, + "feature flags moved -- they must stay where reserved0 was"); +_Static_assert(__builtin_offsetof(struct schfs_uberblock, next_objectid) == 72, + "fields after the feature flags must not shift"); + // --------------------------------------------------------------------------- // Records // --------------------------------------------------------------------------- diff --git a/kernel/fs/schfs/kmount.c b/kernel/fs/schfs/kmount.c index 81c683b..fd83518 100644 --- a/kernel/fs/schfs/kmount.c +++ b/kernel/fs/schfs/kmount.c @@ -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); diff --git a/kernel/fs/schfs/vol.c b/kernel/fs/schfs/vol.c index e119727..cba4c9f 100644 --- a/kernel/fs/schfs/vol.c +++ b/kernel/fs/schfs/vol.c @@ -34,24 +34,66 @@ uber_crc(struct schfs_uberblock *u) return c; } -static int -uber_valid(struct schfs_uberblock *u) +// What this uberblock is, which is not the same question as whether it is good. +// +// This used to be one predicate returning "valid" or not, and a version +// mismatch answered the same as a failed checksum. That conflates two states +// the caller has to treat in opposite ways: +// +// A damaged uberblock means TRY THE NEXT ONE. That is what the ring is for, +// and falling back to an older generation is exactly right -- it is crash +// recovery, and the older root is a consistent filesystem. +// +// An uberblock this driver is too old to understand means STOP. Falling back +// mounts a *stale* filesystem: every commit made since the volume was +// upgraded is silently discarded, and the mount succeeds, so nobody finds +// out. That is data loss reported as success, which is the worst outcome +// available here. +// +// The order of the checks is the order of the questions. 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. +enum uber_state { + UBER_BAD = 0, // not SchFS, or damaged -- keep looking + UBER_OK, // ours, and fully usable + UBER_RO, // ours, sound, but carries ro_compat bits we lack + UBER_TOO_NEW, // ours, sound, and beyond this driver -- stop the scan +}; + +static enum uber_state +uber_check(struct schfs_uberblock *u) { if (u->magic != SCHFS_MAGIC) - return 0; - if (u->version != SCHFS_VERSION) - return 0; - // A volume made with a different block size would otherwise be read with - // every offset wrong, which looks exactly like corruption. - if (u->blocksize != SCHFS_BLKSIZE) - return 0; + return UBER_BAD; if (uber_crc(u) != u->crc) - return 0; + return UBER_BAD; + + // From here the block is certainly ours and certainly landed intact, so + // anything wrong with it is a statement about the *format*, not the media. + if (u->version > SCHFS_VERSION) + return UBER_TOO_NEW; + // A different block size is not damage either: every offset would be read + // wrong, which is precisely what an incompatible format means. + if (u->blocksize != SCHFS_BLKSIZE) + return UBER_TOO_NEW; + if (u->feature_incompat & ~SCHFS_FEAT_INCOMPAT_SUPP) + return UBER_TOO_NEW; + // Self-consistency: a root inside the ring or past the end of the device is // a corrupt uberblock that happens to checksum, not a usable one. if (u->root < SCHFS_FIRST_BLOCK || u->root >= u->total_blocks) - return 0; - return 1; + return UBER_BAD; + + // Read-only is a real mount, so it is decided last -- after everything that + // would have refused outright. + if (u->feature_ro_compat & ~SCHFS_FEAT_RO_COMPAT_SUPP) + return UBER_RO; + + // feature_compat is deliberately not consulted. Unknown bits there are + // unknown on purpose; that is what the class means. + return UBER_OK; } static void @@ -89,6 +131,8 @@ schfs_vol_open(struct schfs_vol *v, struct schfs_bdev *dev, int seen = 0; // valid uberblocks found, at any generation uint64_t ceiling = ~0ULL; // consider only generations below this int spaced = 0; // the allocation map is built and owned + uint64_t too_new = 0; // newest generation this driver cannot read + int ro_forced = 0; // the adopted root carries ro_compat bits v->dev = *dev; v->ctx = *x; @@ -116,8 +160,19 @@ schfs_vol_open(struct schfs_vol *v, struct schfs_bdev *dev, io_errors++; // count each slot once, on the first sweep continue; // an unreadable slot is not fatal: try the rest } - if (!uber_valid(u)) - continue; + { + enum uber_state st = uber_check(u); + if (st == UBER_TOO_NEW) { + // Recorded, not mounted, and not skipped past. Counted on + // the first sweep only, like io_errors: later sweeps walk + // the same slots with a lower ceiling. + if (ceiling == ~0ULL && u->generation > too_new) + too_new = u->generation; + continue; + } + if (st == UBER_BAD) + continue; + } if (u->generation >= ceiling) continue; // already tried, and rejected if (best_slot < 0 || u->generation > best_gen) { @@ -130,6 +185,15 @@ schfs_vol_open(struct schfs_vol *v, struct schfs_bdev *dev, if (best_slot < 0) break; // nothing older left to try + + // Refuse rather than mount something older than a root we could not + // read. The candidate below is a perfectly good filesystem; it is just + // not the *current* one, and mounting it would discard every commit + // made since the volume was written by a newer driver -- silently, and + // with the mount reporting success. + if (too_new > best_gen) + return -3; + ceiling = best_gen; // next round considers strictly older ones // Re-read the winner rather than keeping a copy of it. @@ -140,8 +204,14 @@ schfs_vol_open(struct schfs_vol *v, struct schfs_bdev *dev, // very first boot. Re-reading costs one block read per candidate. if (v->dev.bread(v->dev.ctx, SCHFS_UBER_START + best_slot, u) != 0) goto next; - if (!uber_valid(u)) - goto next; /* changed under us, or a flaky device */ + { + enum uber_state st = uber_check(u); + if (st == UBER_TOO_NEW) + return -3; /* rewritten under us by a newer driver */ + if (st == UBER_BAD) + goto next; /* changed under us, or a flaky device */ + ro_forced = (st == UBER_RO); + } vol_from_uber(v, u, best_slot); v->readonly = 1; // until an allocation map exists, below @@ -178,7 +248,11 @@ schfs_vol_open(struct schfs_vol *v, struct schfs_bdev *dev, if (schfs_space_scan(v) != 0) goto next; v->free_blocks = v->space.free_blocks; - v->readonly = 0; + // A volume carrying ro_compat bits we do not implement stays + // read-only even though the map is now good: writing would leave + // whatever structure those bits describe stale, and stale is worse + // than absent because nothing would notice. + v->readonly = ro_forced; } // Which attributes are indexed, so attr.c can answer without a lookup // on every write. A volume with no indexes -- every volume until @@ -194,6 +268,8 @@ schfs_vol_open(struct schfs_vol *v, struct schfs_bdev *dev, // Nothing usable. Distinguish "not a SchFS volume" from "a SchFS volume // that cannot be read", because the first is an ordinary answer to a probe // and the second is a damaged disk somebody needs to hear about. + if (too_new) + return -3; if (io_errors == SCHFS_UBER_COUNT) return -2; return seen ? -2 : -1; diff --git a/tools/schfs_fsck.c b/tools/schfs_fsck.c index b617279..2f1a671 100644 --- a/tools/schfs_fsck.c +++ b/tools/schfs_fsck.c @@ -791,6 +791,14 @@ main(int argc, char **argv) "volume, or never successfully written\n", imgpath); return 1; } + if (r == -3) { + fprintf(stderr, "schfs_fsck: %s: written by a newer SchFS than this " + "tool understands (this tool: version %d)\n", + imgpath, SCHFS_VERSION); + fprintf(stderr, "schfs_fsck: refusing -- older uberblocks may be " + "readable, but they are a stale filesystem\n"); + return 1; + } if (r != 0) { fprintf(stderr, "schfs_fsck: %s: newest uberblock names an unreadable " "tree root\n", imgpath);