The last three phases of the filesystem, which were written together and
touch the same files throughout -- kmount.c, objw.c and vfs.c are each
rewritten by all three -- so they land as one commit rather than three
that could not be built separately.
Durability. A transaction that reaches the platter out of order is worse
than one that never reached it: the uberblock can name a tree whose
blocks are still in the drive's write cache. bdirty/bflush give the
buffer layer an explicit barrier, ideflush issues ATA FLUSH CACHE and
usb_storage_flush the SCSI SYNCHRONIZE CACHE that means the same thing,
and a commit now waits for the data before it writes the root that
points at it. The uberblock groups are read newest-first with fallback,
so a volume whose newest roots are torn mounts at an older one and says
which generations were lost.
Reflink. Extents carry a refcount, so two objects can name the same
blocks and neither frees them while the other holds them. Exposed as
FICLONE -- Linux's ioctl value, so `cp --reflink` works unmodified --
and as cmd/reflink for the case where there is no cp to hand.
Attributes, and the one genuinely new idea here. attr.c stores named
values on an object, reachable through setxattr/getxattr/removexattr at
Linux's syscall numbers. On top of that, schfs_index declares an index
over an attribute name and schfs_query answers "which objects carry this
value" from the B-tree rather than by walking. A query against an
unindexed attribute is refused with an error, never silently answered by
a full scan: a query that quietly becomes a scan is worse than one that
admits it cannot help. cmd/schattr drives both.
Three bugs fixed on the way, each found by the numbers being wrong
rather than by inspection:
- statfs's f_fsid was declared int64[2]. Linux's fsid_t is two *ints*,
so every field after it landed one slot late: userland read f_frsize
out of f_namelen, got 255, and df reported a 32 MB root filesystem
as 1992 KB. It hid for as long as it did because the fields used to
be invented constants, so wrong numbers looked like the usual ones.
- struct inode's nlink was a short against SchFS's uint32, so a file
past 32767 names came back negative -- and iput frees an object
whose count reaches zero. It is an int now, signed on purpose, so an
underflow lands somewhere a check can see rather than wrapping.
- filewrite split every write to fit v1's log. That limit belongs to
the journal, not to writing, and SchFS makes one transaction of
whatever it is handed: a 1 MB write was becoming 170 commits and
170 flushes for nothing. fs_ops carries max_write now, and SchFS
sets no limit.
Also here: a real mount table, so `mount -t schfs /dev/hdb /mnt` shows
up in /proc/mounts -- df looks a path up there before it calls statfs at
all, so a mount missing from that list was invisible to it -- several
volumes mounted at once, snapshots mounted by name, and /proc/fsstat
with an rdtsc-based counter, which is the only instrument fine enough to
see inside a single syscall.
Verified: `make schfs-check` passes in full, including the sweep that
crashes the volume at each of 119 points mid-transaction and requires
every one to mount with exactly its committed data, and the case where
both newest uberblocks are rubbish and the volume still comes back. The
headless boot check passes with 27 [ok] and no failures, on a SchFS root.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
432 lines
14 KiB
C
432 lines
14 KiB
C
// USB mass storage: bulk-only transport, SCSI transparent command set.
|
|
//
|
|
// Almost every USB stick, external drive and card reader is this, and the
|
|
// name is a fair description of the whole protocol: there are exactly two
|
|
// bulk endpoints and everything happens by sending three things over them.
|
|
//
|
|
// command a 31-byte wrapper (CBW) carrying a SCSI command block, OUT
|
|
// data the payload, IN or OUT, if the command has one
|
|
// status a 13-byte wrapper (CSW) saying whether it worked, IN
|
|
//
|
|
// That is the entire transport. The interesting content is SCSI, which is
|
|
// what the wrapper carries and what the device actually speaks -- so a USB
|
|
// stick is a SCSI disk that happens to be reached over USB, and the commands
|
|
// below (INQUIRY, READ CAPACITY, READ(10), WRITE(10)) are the same ones a
|
|
// SCSI drive answered in 1990.
|
|
|
|
#include "types.h"
|
|
#include "defs.h"
|
|
#include "param.h"
|
|
#include "memlayout.h"
|
|
#include "mmu.h"
|
|
#include "spinlock.h"
|
|
#include "sleeplock.h"
|
|
#include "fs.h"
|
|
#include "buf.h"
|
|
#include "dma.h"
|
|
#include "usb.h"
|
|
|
|
#define CBW_SIGNATURE 0x43425355 // "USBC"
|
|
#define CSW_SIGNATURE 0x53425355 // "USBS"
|
|
|
|
struct cbw {
|
|
uint32 signature;
|
|
uint32 tag;
|
|
uint32 data_len;
|
|
uint8 flags; // 0x80 = device-to-host
|
|
uint8 lun;
|
|
uint8 cb_len; // 1..16
|
|
uint8 cb[16];
|
|
} __attribute__((packed));
|
|
|
|
struct csw {
|
|
uint32 signature;
|
|
uint32 tag;
|
|
uint32 residue;
|
|
uint8 status; // 0 = passed, 1 = failed, 2 = phase error
|
|
} __attribute__((packed));
|
|
|
|
// SCSI opcodes
|
|
#define SCSI_TEST_UNIT_READY 0x00
|
|
#define SCSI_REQUEST_SENSE 0x03
|
|
#define SCSI_INQUIRY 0x12
|
|
#define SCSI_READ_CAPACITY10 0x25
|
|
#define SCSI_READ10 0x28
|
|
#define SCSI_WRITE10 0x2A
|
|
// SYNCHRONIZE CACHE(10): commit the device's write cache to the medium. The
|
|
// SCSI spelling of ATA's FLUSH CACHE, and needed for the same reason -- see
|
|
// ideflush() and the barrier in schfs_vol_commit.
|
|
#define SCSI_SYNC_CACHE10 0x35
|
|
|
|
#define SECTOR_SIZE 512
|
|
|
|
static struct {
|
|
struct usb_dev *dev;
|
|
int present;
|
|
uint32 nsectors;
|
|
uint32 sector_size;
|
|
uint32 tag;
|
|
void *xfer; // DMA bounce buffer, one block
|
|
struct cbw *cbw; // allocated once, not per command
|
|
struct csw *csw;
|
|
struct sleeplock lock; // one command at a time
|
|
} ums;
|
|
|
|
// SCSI is big-endian, everywhere, for everything. x86 is not.
|
|
static void
|
|
be32(uint8 *p, uint32 v)
|
|
{
|
|
p[0] = v >> 24; p[1] = v >> 16; p[2] = v >> 8; p[3] = v;
|
|
}
|
|
|
|
static uint32
|
|
rd_be32(const uint8 *p)
|
|
{
|
|
return ((uint32)p[0] << 24) | ((uint32)p[1] << 16) |
|
|
((uint32)p[2] << 8) | p[3];
|
|
}
|
|
|
|
// One complete command-data-status exchange.
|
|
//
|
|
// `buf`, when there is data, must be DMA memory -- it is handed to the
|
|
// controller as a physical address.
|
|
static int
|
|
bot_command(const uint8 *cdb, int cdb_len, void *buf, int len, int in)
|
|
{
|
|
struct usb_dev *dev = ums.dev;
|
|
struct cbw *cbw = ums.cbw;
|
|
struct csw *csw = ums.csw;
|
|
uint32 tag = ++ums.tag;
|
|
int i, data_ok = 1;
|
|
|
|
memset(cbw, 0, sizeof(*cbw));
|
|
cbw->signature = CBW_SIGNATURE;
|
|
cbw->tag = tag;
|
|
cbw->data_len = len;
|
|
cbw->flags = in ? 0x80 : 0x00;
|
|
cbw->lun = 0;
|
|
cbw->cb_len = cdb_len;
|
|
for(i = 0; i < cdb_len && i < 16; i++)
|
|
cbw->cb[i] = cdb[i];
|
|
|
|
if(dev->ops->bulk(dev, dev->bulk_out.bEndpointAddress, cbw,
|
|
sizeof(*cbw)) != (int)sizeof(*cbw)){
|
|
cprintf("usb-storage: the device would not take the command\n");
|
|
return -1;
|
|
}
|
|
|
|
if(len > 0){
|
|
int addr = in ? dev->bulk_in.bEndpointAddress
|
|
: dev->bulk_out.bEndpointAddress;
|
|
if(dev->ops->bulk(dev, addr, buf, len) < 0)
|
|
data_ok = 0;
|
|
}
|
|
|
|
// The status wrapper is not optional and not advisory: a device that has
|
|
// sent its data is still waiting to be asked how it went.
|
|
//
|
|
// It is read even when the data phase failed, which is the whole point --
|
|
// returning early there would leave the device still holding a status it
|
|
// has not been asked for, so the *next* command's data phase would receive
|
|
// this command's status instead. One failed read would turn into every
|
|
// subsequent read failing, which is a far worse fault than the one that
|
|
// started it, and one that looks nothing like its cause.
|
|
memset(csw, 0, sizeof(*csw));
|
|
if(dev->ops->bulk(dev, dev->bulk_in.bEndpointAddress, csw,
|
|
sizeof(*csw)) != (int)sizeof(*csw)){
|
|
cprintf("usb-storage: no status returned for command 0x%x\n", cdb[0]);
|
|
return -1;
|
|
}
|
|
|
|
if(csw->signature != CSW_SIGNATURE || csw->tag != tag){
|
|
cprintf("usb-storage: status wrapper does not match the command\n");
|
|
return -1;
|
|
}
|
|
if(csw->status != 0){
|
|
cprintf("usb-storage: command 0x%x failed (status %d)\n",
|
|
cdb[0], csw->status);
|
|
return -1;
|
|
}
|
|
if(!data_ok)
|
|
return -1;
|
|
|
|
// Residue is what was *not* transferred, and a device is allowed to report
|
|
// more than was asked for only if it is broken -- clamp rather than return
|
|
// a negative length that a caller would read as an error code.
|
|
if(csw->residue > (uint32)len)
|
|
return -1;
|
|
return len - (int)csw->residue;
|
|
}
|
|
|
|
static int
|
|
scsi_inquiry(char *vendor, char *product)
|
|
{
|
|
uint8 cdb[6] = { SCSI_INQUIRY, 0, 0, 0, 36, 0 };
|
|
uint8 *r = ums.xfer;
|
|
int i;
|
|
|
|
if(bot_command(cdb, sizeof(cdb), r, 36, 1) < 0)
|
|
return -1;
|
|
|
|
// Bytes 8-15 are the vendor, 16-31 the product, space padded and not
|
|
// NUL-terminated -- the only fixed-width strings left in computing.
|
|
for(i = 0; i < 8; i++)
|
|
vendor[i] = r[8 + i];
|
|
vendor[8] = 0;
|
|
for(i = 0; i < 16; i++)
|
|
product[i] = r[16 + i];
|
|
product[16] = 0;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
scsi_read_capacity(void)
|
|
{
|
|
uint8 cdb[10] = { SCSI_READ_CAPACITY10, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
|
|
uint8 *r = ums.xfer;
|
|
|
|
if(bot_command(cdb, sizeof(cdb), r, 8, 1) < 0)
|
|
return -1;
|
|
|
|
// The first word is the address of the *last* sector, not the count.
|
|
ums.nsectors = rd_be32(r) + 1;
|
|
ums.sector_size = rd_be32(r + 4);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
scsi_rw(uint32 lba, int count, void *buf, int write)
|
|
{
|
|
uint8 cdb[10];
|
|
|
|
memset(cdb, 0, sizeof(cdb));
|
|
cdb[0] = write ? SCSI_WRITE10 : SCSI_READ10;
|
|
be32(&cdb[2], lba);
|
|
cdb[7] = (count >> 8) & 0xFF;
|
|
cdb[8] = count & 0xFF;
|
|
|
|
return bot_command(cdb, sizeof(cdb), buf, count * SECTOR_SIZE, !write);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Block device
|
|
// ---------------------------------------------------------------------------
|
|
|
|
int
|
|
usb_storage_present(void)
|
|
{
|
|
return ums.present;
|
|
}
|
|
|
|
// Sync one buffer-cache block with the stick. Same contract as iderw() and
|
|
// ramdiskrw(): on a read fill b->data and set B_VALID, on a write push it out
|
|
// and clear B_DIRTY.
|
|
void
|
|
usb_storage_rw(struct buf *b)
|
|
{
|
|
int per_block = BSIZE / SECTOR_SIZE;
|
|
uint32 lba = b->blockno * per_block;
|
|
int write = (b->flags & B_DIRTY) != 0;
|
|
int i, n = -1;
|
|
|
|
if(!holdingsleep(&b->lock))
|
|
panic("usb_storage_rw: buf not locked");
|
|
if(!ums.present)
|
|
panic("usb_storage_rw: no USB disk");
|
|
if(lba + per_block > ums.nsectors)
|
|
panic("usb_storage_rw: block out of range");
|
|
|
|
// One command at a time. Bulk-only transport has no tagging beyond the CBW
|
|
// tag and no queueing: a second command sent while the first is mid-phase
|
|
// desynchronises the endpoint and every command after it fails.
|
|
acquiresleep(&ums.lock);
|
|
|
|
// Removable media fails transiently in ways a fixed disk does not, so a
|
|
// couple of retries are worth having before giving up.
|
|
for(i = 0; i < 3; i++){
|
|
if(write)
|
|
memmove(ums.xfer, b->data, BSIZE);
|
|
n = scsi_rw(lba, per_block, ums.xfer, write);
|
|
if(n == BSIZE)
|
|
break;
|
|
cprintf("usb-storage: %s of block %d failed, retrying\n",
|
|
write ? "write" : "read", b->blockno);
|
|
}
|
|
|
|
releasesleep(&ums.lock);
|
|
|
|
// A read that failed must not return quietly. bread() hands the buffer
|
|
// straight back to its caller, which has no way to ask whether it worked --
|
|
// so leaving B_VALID clear would deliver whatever the buffer happened to
|
|
// contain as though it were the block on disk. Corrupting a filesystem
|
|
// silently is worse than stopping.
|
|
if(n != BSIZE)
|
|
panic("usb_storage_rw: transfer failed");
|
|
|
|
if(!write)
|
|
memmove(b->data, ums.xfer, BSIZE);
|
|
b->flags |= B_VALID;
|
|
b->flags &= ~B_DIRTY;
|
|
}
|
|
|
|
// Make everything written so far durable. Returns 0, or -1 if the stick would
|
|
// not confirm it.
|
|
//
|
|
// A whole-device sync with no LBA range, which is what the callers want: the
|
|
// filesystem's barrier is "everything before this point", not "these blocks".
|
|
//
|
|
// Cheap sticks routinely reject this command outright. That is reported rather
|
|
// than ignored, because the caller is about to write an uberblock on the
|
|
// strength of it -- and a barrier that silently does nothing is worse than no
|
|
// barrier at all, since it makes an unsafe volume look safe.
|
|
int
|
|
usb_storage_flush(void)
|
|
{
|
|
uint8 cdb[10];
|
|
int r;
|
|
|
|
if(!ums.present)
|
|
return 0;
|
|
|
|
memset(cdb, 0, sizeof(cdb));
|
|
cdb[0] = SCSI_SYNC_CACHE10; // LBA 0, count 0: the entire medium
|
|
|
|
acquiresleep(&ums.lock);
|
|
r = bot_command(cdb, sizeof(cdb), 0, 0, 0);
|
|
releasesleep(&ums.lock);
|
|
|
|
if(r < 0){
|
|
cprintf("usb-storage: SYNCHRONIZE CACHE failed; writes are not durable\n");
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Read block 1 through the ordinary buffer cache and report what is there.
|
|
//
|
|
// This is the only test that proves the whole path rather than a piece of it:
|
|
// bread() -> bdevrw() -> usb_storage_rw() -> bulk-only transport -> SCSI
|
|
// READ(10) -> an interrupt -> a sleeping process woken with data. Everything
|
|
// before it -- INQUIRY answering, a plausible capacity -- can be true of a
|
|
// device that cannot actually read a sector.
|
|
//
|
|
// Reached with `echo s > /proc/sysrq`.
|
|
void
|
|
usb_storage_dump(void)
|
|
{
|
|
struct buf *b;
|
|
struct superblock *sb;
|
|
|
|
if(!ums.present){
|
|
cprintf("usb-storage: no disk\n");
|
|
return;
|
|
}
|
|
|
|
cprintf("usb-storage: %d sectors of %d bytes\n", ums.nsectors,
|
|
ums.sector_size);
|
|
|
|
b = bread(USBDISK_DEV, 1);
|
|
sb = (struct superblock*)b->data;
|
|
cprintf("usb-storage: block 1 magic %x size %d nblocks %d ninodes %d\n",
|
|
sb->magic, sb->size, sb->nblocks, sb->ninodes);
|
|
brelse(b);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static int
|
|
storage_probe(struct usb_dev *dev)
|
|
{
|
|
char vendor[9], product[17];
|
|
int i;
|
|
|
|
if(ums.present)
|
|
return -1; // one disk is enough for now
|
|
if(!dev->has_bulk_in || !dev->has_bulk_out){
|
|
cprintf("usb-storage: needs a bulk endpoint in each direction\n");
|
|
return -1;
|
|
}
|
|
|
|
initsleeplock(&ums.lock, "usbms");
|
|
ums.dev = dev;
|
|
|
|
// One block, used for every transfer. bread() hands us b->data, which lives
|
|
// in the buffer cache rather than in DMA memory, so everything bounces
|
|
// through here. The wrappers are allocated once too: they are used on every
|
|
// single I/O, and churning the DMA extent list twice per block read would
|
|
// fragment an arena that has no business fragmenting.
|
|
ums.xfer = dma_alloc(BSIZE, 4096);
|
|
ums.cbw = dma_alloc(sizeof(struct cbw), 64);
|
|
ums.csw = dma_alloc(sizeof(struct csw), 64);
|
|
if(ums.xfer == 0 || ums.cbw == 0 || ums.csw == 0)
|
|
goto fail;
|
|
|
|
// Deliberately OUT first, which is the order that used to break.
|
|
//
|
|
// The slot context's Context Entries field must cover the highest DCI in
|
|
// use, and xhci_open_ep originally wrote whichever endpoint it was adding.
|
|
// Bulk OUT here has the higher DCI, so opening it first and then IN would
|
|
// *lower* the field and silently invalidate the endpoint that was already
|
|
// configured. Opening IN first hid that. Now that xhci_open_ep tracks the
|
|
// maximum itself the order genuinely does not matter -- and doing it in the
|
|
// formerly-broken order is what keeps that true.
|
|
if(dev->ops->open_ep(dev, &dev->bulk_out) < 0 ||
|
|
dev->ops->open_ep(dev, &dev->bulk_in) < 0)
|
|
goto fail;
|
|
|
|
// Deliberately no GET MAX LUN. It is answered with a STALL by
|
|
// single-LUN devices, and recovering from a STALL on endpoint 0 needs a
|
|
// Reset Endpoint command this driver does not implement yet. LUN 0 is the
|
|
// only one used below, so asking would gain nothing and could wedge us.
|
|
|
|
// A device that has just been configured is usually "not ready" for the
|
|
// first command or two while it spins up or reads its own media.
|
|
for(i = 0; i < 5; i++){
|
|
uint8 cdb[6] = { SCSI_TEST_UNIT_READY, 0, 0, 0, 0, 0 };
|
|
if(bot_command(cdb, sizeof(cdb), 0, 0, 0) >= 0)
|
|
break;
|
|
microdelay(100000);
|
|
}
|
|
|
|
if(scsi_inquiry(vendor, product) < 0)
|
|
goto fail;
|
|
if(scsi_read_capacity() < 0)
|
|
goto fail;
|
|
|
|
if(ums.sector_size != SECTOR_SIZE){
|
|
cprintf("usb-storage: %d-byte sectors are not supported\n",
|
|
ums.sector_size);
|
|
goto fail;
|
|
}
|
|
if(ums.nsectors == 0){
|
|
cprintf("usb-storage: device reports no capacity\n");
|
|
goto fail;
|
|
}
|
|
|
|
ums.present = 1;
|
|
cprintf("usb-storage: %s %s, %d sectors (%d MB)\n",
|
|
vendor, product, ums.nsectors,
|
|
(int)((uint64)ums.nsectors * SECTOR_SIZE / (1024 * 1024)));
|
|
return 0;
|
|
|
|
fail:
|
|
// Give the memory back and forget the device. Leaving ums.dev set would let
|
|
// a later block read reach a device we decided we could not drive.
|
|
if(ums.xfer) dma_free(ums.xfer, BSIZE);
|
|
if(ums.cbw) dma_free(ums.cbw, sizeof(struct cbw));
|
|
if(ums.csw) dma_free(ums.csw, sizeof(struct csw));
|
|
ums.xfer = 0;
|
|
ums.cbw = 0;
|
|
ums.csw = 0;
|
|
ums.dev = 0;
|
|
return -1;
|
|
}
|
|
|
|
const struct usb_driver usb_storage_driver = {
|
|
.name = "usb-storage",
|
|
.class = USB_CLASS_MASS_STORAGE,
|
|
.subclass = USB_MSC_SUBCLASS_SCSI,
|
|
.protocol = USB_MSC_PROTO_BOT,
|
|
.devclass = DEVCLASS_STORAGE,
|
|
.probe = storage_probe,
|
|
};
|