exfat: adversarial-review fixes — overflow safety, sparse gaps, dir size, big-image bitmap (S4 step 9)
A 7-dimension adversarial review of the engine, tool, and routing found nine real defects (host tests + the in-VM drill missed them). Fixed: - geometryOf now rejects a crafted VBR whose cluster shift exceeds the exFAT ceiling (bytes+sectors shift > 25) or whose cluster_count exceeds the spec max (0xFFFFFFF5) — either would overflow the engine's u32 cluster-byte / cluster-bounds arithmetic and panic under ReleaseSafe on untrusted removable media. validCluster/allocateCluster widened to u64, and writeFile's clusters_needed widened, for a >4 GiB file near the u32 offset boundary. - writeFile no longer claims valid_data_length = size unconditionally: a sparse write past a foreign file's old valid boundary now zero-fills the skipped gap on disk, so a read there returns zero, not stale bytes. - ensureDirCapacity rewrites a grown subdirectory's own DataLength, so a spec-compliant reader that bounds a directory by DataLength sees the new entries (danos itself bounds by the end marker, but chkdsk / other OSes do not). - make-exfat-image lays the allocation bitmap across as many clusters as it needs; a >128 MiB image (whose bitmap exceeds one cluster) was self-inconsistent. Verified: the engine mounts+reads both the 48 MiB fixture and a 256 MiB image. Documented (not fixed here — a shared vfs-layer limit, like the u32 offset cap): non-ASCII names fold to '?', the same as the FAT engine. New host tests pin each fix (crafted-VBR rejection, sparse-gap zero, subdir-grows-and-records-size). Full suite 131/131, bounds green.
This commit is contained in:
+33
-15
@@ -97,7 +97,16 @@ class ExfatImage:
|
||||
if self.cluster_count < 16:
|
||||
sys.exit(f"error: image too small for exFAT ({self.cluster_count} clusters)")
|
||||
self.cluster_bytes = self.spc * SECTOR
|
||||
self.root_cluster = 4 # 2=bitmap, 3=up-case, 4=root
|
||||
# Layout: the allocation bitmap (as many clusters as it needs — one per
|
||||
# 8*cluster_bytes clusters of the volume), then the up-case table, the root
|
||||
# directory, and the seeded file. A single-cluster bitmap (small volumes,
|
||||
# e.g. the 48 MiB fixture) puts root at cluster 4, as before.
|
||||
self.bitmap_bytes = (self.cluster_count + 7) // 8
|
||||
self.bitmap_clusters = (self.bitmap_bytes + self.cluster_bytes - 1) // self.cluster_bytes
|
||||
self.bitmap_cluster = 2
|
||||
self.upcase_cluster = self.bitmap_cluster + self.bitmap_clusters
|
||||
self.root_cluster = self.upcase_cluster + 1
|
||||
self.hello_cluster = self.root_cluster + 1
|
||||
self.image = bytearray(self.total_sectors * SECTOR)
|
||||
|
||||
def cluster_offset(self, cluster):
|
||||
@@ -148,37 +157,46 @@ class ExfatImage:
|
||||
# Backup boot region (sectors 12..23) is a copy of 0..11.
|
||||
self.image[12 * SECTOR : 24 * SECTOR] = self.image[0 : 12 * SECTOR]
|
||||
|
||||
# FAT: reserved entries + the metadata/file chains (each a single cluster).
|
||||
# FAT: reserved entries, then a single-cluster chain per metadata object,
|
||||
# except the bitmap which spans self.bitmap_clusters (a real FAT chain).
|
||||
self.set_fat(0, 0xFFFFFFF8)
|
||||
self.set_fat(1, 0xFFFFFFFF)
|
||||
for cluster in (2, 3, 4, 5):
|
||||
used = []
|
||||
for i in range(self.bitmap_clusters):
|
||||
cluster = self.bitmap_cluster + i
|
||||
self.set_fat(cluster, 0xFFFFFFFF if i == self.bitmap_clusters - 1 else cluster + 1)
|
||||
used.append(cluster)
|
||||
for cluster in (self.upcase_cluster, self.root_cluster, self.hello_cluster):
|
||||
self.set_fat(cluster, 0xFFFFFFFF)
|
||||
used.append(cluster)
|
||||
|
||||
# Allocation bitmap (cluster 2): clusters 2,3,4,5 in use.
|
||||
for cluster in (2, 3, 4, 5):
|
||||
# Allocation bitmap: every metadata/file cluster in use.
|
||||
for cluster in used:
|
||||
self.mark_allocated(cluster)
|
||||
|
||||
# Up-case table (cluster 3): 256 explicit units, a-z -> A-Z.
|
||||
# Up-case table: 256 explicit units, a-z -> A-Z.
|
||||
upcase = bytearray(UPCASE_UNITS * 2)
|
||||
for i in range(UPCASE_UNITS):
|
||||
struct.pack_into("<H", upcase, i * 2, ascii_upper(i))
|
||||
self.image[self.cluster_offset(3) : self.cluster_offset(3) + len(upcase)] = upcase
|
||||
off = self.cluster_offset(self.upcase_cluster)
|
||||
self.image[off : off + len(upcase)] = upcase
|
||||
table_checksum = upcase_checksum(upcase)
|
||||
|
||||
# Seed file HELLO.TXT (cluster 5, contiguous).
|
||||
# Seed file HELLO.TXT (contiguous, one cluster).
|
||||
content = b"exfat hello danos\n"
|
||||
self.image[self.cluster_offset(5) : self.cluster_offset(5) + len(content)] = content
|
||||
off = self.cluster_offset(self.hello_cluster)
|
||||
self.image[off : off + len(content)] = content
|
||||
|
||||
# Root directory (cluster 4): bitmap, up-case, volume label, HELLO set.
|
||||
root = self.cluster_offset(4)
|
||||
# Root directory: bitmap, up-case, volume label, HELLO set.
|
||||
root = self.cluster_offset(self.root_cluster)
|
||||
# 0x81 Allocation Bitmap
|
||||
struct.pack_into("<BBB", self.image, root, 0x81, 0, 0)
|
||||
struct.pack_into("<I", self.image, root + 20, 2)
|
||||
struct.pack_into("<Q", self.image, root + 24, (self.cluster_count + 7) // 8)
|
||||
struct.pack_into("<I", self.image, root + 20, self.bitmap_cluster)
|
||||
struct.pack_into("<Q", self.image, root + 24, self.bitmap_bytes)
|
||||
# 0x82 Up-case Table
|
||||
struct.pack_into("<B", self.image, root + 32, 0x82)
|
||||
struct.pack_into("<I", self.image, root + 32 + 4, table_checksum)
|
||||
struct.pack_into("<I", self.image, root + 32 + 20, 3)
|
||||
struct.pack_into("<I", self.image, root + 32 + 20, self.upcase_cluster)
|
||||
struct.pack_into("<Q", self.image, root + 32 + 24, UPCASE_UNITS * 2)
|
||||
# 0x83 Volume Label
|
||||
label_units = [ord(c) for c in self.label[:11]]
|
||||
@@ -187,7 +205,7 @@ class ExfatImage:
|
||||
struct.pack_into("<H", self.image, root + 64 + 2 + i * 2, u)
|
||||
# HELLO.TXT set: File (0x85) + Stream (0xC0) + Name (0xC1)
|
||||
name = "HELLO.TXT"
|
||||
self.write_file_set(root + 96, name, first_cluster=5, length=len(content))
|
||||
self.write_file_set(root + 96, name, first_cluster=self.hello_cluster, length=len(content))
|
||||
|
||||
def write_file_set(self, offset, name, first_cluster, length):
|
||||
entries = bytearray(32 * 3)
|
||||
|
||||
Reference in New Issue
Block a user