fs: validate ROFS on-disk metadata and fix symlink-path off-by-one - #1451
Conversation
There was a problem hiding this comment.
Pull request overview
This PR hardens ROFS mounting and runtime operations against crafted on-disk metadata to prevent kernel memory corruption, and fixes a 1-byte out-of-bounds write in symlink resolution during path lookup.
Changes:
- Limit
read_link()reads toPATH_MAX - 1to keep the explicit NUL terminator in-bounds. - Add ROFS superblock-derived size/count validation, overflow checks, allocation checks, and structure-buffer bounds guards during mount.
- Add runtime bounds checks for ROFS table indexing in
readlink,readdir, andlookup.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| fs/vfs/vfs_lookup.cc | Fixes namei_follow_link() symlink buffer off-by-one by reading at most PATH_MAX-1. |
| fs/rofs/rofs_vnops.cc | Adds bounds checks for on-disk-derived indices when accessing ROFS metadata tables at runtime. |
| fs/rofs/rofs_vfsops.cc | Adds mount-time validation/guardrails for ROFS on-disk metadata parsing and allocations. |
Comments suppressed due to low confidence (3)
fs/rofs/rofs_vnops.cc:249
- de_idx is computed as inode->data_offset + idx from on-disk values. This addition can overflow and wrap, which could bypass the de_idx >= directory_entries_count check and lead to an out-of-bounds access in rofs->dir_entries[de_idx].
for (unsigned int idx = 0; idx < inode->dir_children_count; idx++) {
uint64_t de_idx = (uint64_t)inode->data_offset + idx;
if (de_idx >= rofs->sb->directory_entries_count) {
break; // directory metadata is inconsistent; stop rather than OOB
}
fs/rofs/rofs_vfsops.cc:194
- This loop counter is unsigned int while symlinks_count is uint64_t from disk. If symlinks_count > UINT_MAX, idx wraps and the loop may not terminate.
for (unsigned int idx = 0; idx < sb->symlinks_count; idx++) {
if (!in_bounds(data_ptr, sizeof(unsigned short))) {
fs/rofs/rofs_vfsops.cc:229
- This loop counter is unsigned int while inodes_count is uint64_t from disk. If inodes_count > UINT_MAX, idx wraps and the loop may not terminate.
memcpy(rofs->inodes, data_ptr, sb->inodes_count * sizeof(struct rofs_inode));
for (unsigned int idx = 0; idx < sb->inodes_count; idx++) {
print("[rofs] inode: %d, size: %d\n", rofs->inodes[idx].inode_no, rofs->inodes[idx].file_size);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
ROFS trusted its on-disk superblock and structure blocks, so a crafted image could corrupt kernel memory during mount and path lookup. Harden the mount and runtime paths and fix a 1-byte out-of-bounds write in symlink resolution. Mount (rofs_vfsops.cc): - Reject a structure_info_blocks_count that is zero or overflows. - Allocate the directory-entry / symlink / inode arrays through a checked allocator that rejects count*elem overflow and zero-fills, and bound every walk of the structure buffer against the buffer end. - The in_bounds() helper now guards the upper bound explicitly, so a pointer already past the buffer end cannot pass via a negative difference wrapping to a huge size_t. - Iterate the tables with uint64_t counters that match the on-disk uint64_t counts, so a count > UINT_MAX cannot wrap the loop index. - On any post-superblock failure, free everything allocated so far (arrays plus inner filename/symlink strings and the superblock) so repeated mount failures cannot leak kernel memory. Runtime (rofs_vnops.cc): - readdir/lookup detect uint64_t wrap when computing data_offset + index before indexing dir_entries[], and validate the 1-based inode number before indexing inodes[]. - read_link limits the copy to PATH_MAX - 1 and NUL-terminates, fixing a 1-byte OOB write during path lookup. Signed-off-by: Greg Burd <greg@burd.me>
|
Thanks Copilot. All four addressed in the current tip (737322e, rebased onto master):
Needs a build to confirm it compiles and the crafted-image path behaves; queued. |
|
Build-validated on x86_64: |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
fs/rofs/rofs_vfsops.cc:136
- After copying the superblock into a freshly-allocated
sb(heap), the new early-return error paths (bad structure_info_blocks_count, allocation failure, androfs_read_blocksfailure) return without deletingsb. This leaks kernel memory on crafted images that trigger these failures repeatedly.
uint64_t struct_bytes = (uint64_t)BSIZE * sb->structure_info_blocks_count;
if (sb->structure_info_blocks_count == 0 ||
struct_bytes / BSIZE != sb->structure_info_blocks_count ||
struct_bytes > (SIZE_MAX)) {
kprintf("[rofs] bad structure_info_blocks_count\n");
device_close(device);
return EINVAL;
}
buf.reset(alloc_phys_contiguous_aligned(struct_bytes, PAGE_SIZE));
if (!buf.get()) {
device_close(device);
return ENOMEM;
}
error = rofs_read_blocks(device, sb->structure_info_first_block, sb->structure_info_blocks_count, buf.get());
if (error) {
kprintf("[rofs] Error reading rofs structure info blocks\n");
device_close(device);
return error;
}
| if (strcmp(name, rofs->dir_entries[de_idx].filename) == 0) { | ||
| int inode_no = rofs->dir_entries[de_idx].inode_no; | ||
|
|
||
| if (vget(vnode->v_mount, inode_no, &vp)) { //TODO: Will it ever work? Revisit | ||
| print("[rofs] found vp in cache!\n"); | ||
| *vpp = vp; | ||
| return 0; | ||
| } | ||
|
|
||
| struct rofs_inode *found_inode = rofs->inodes + (inode_no - 1); //Check if exists | ||
| // inode_no is 1-based into inodes[]; validate before indexing. | ||
| if (inode_no <= 0 || (unsigned)inode_no > rofs->sb->inodes_count) { | ||
| return EIO; | ||
| } | ||
| struct rofs_inode *found_inode = rofs->inodes + (inode_no - 1); | ||
| rofs_set_vnode(vp, found_inode); |
nyh
left a comment
There was a problem hiding this comment.
I tentatively approve, but I don't understand the security threat here: You can crash the kernel by supplying a crafted filesystem? But if you can change the filesystem, you can also write any executable you want in it. If you can run code that mount()s some bad filesystem, you can also run any code that does anything... What am I missing>
Security fix — kernel heap corruption from a crafted ROFS image + a 1-byte OOB write in symlink resolution. Relevant for any attacker-controlled/attached volume; in a unikernel these are kernel memory corruption.
1. ROFS mount trusts all superblock counts/sizes (
fs/rofs/rofs_vfsops.cc)alloc_phys_contiguous_aligned(BSIZE * structure_info_blocks_count)— integer overflow + no null check.malloc(sizeof(x) * {directory_entries,symlinks,inodes}_count)— overflow to a tiny allocation.data_ptrwith no bound against the buffer actually read. A crafted count or per-entryfilename_size/symlink_path_sizewalks far past the buffer → OOB read (heap contents copied into filenames) then a fault.Fix: checked multiplication, null checks, and an
in_bounds()guard on every read from the structure buffer;EINVALon any inconsistency.2. ROFS runtime index OOB (
fs/rofs/rofs_vnops.cc)rofs_readlink/readdir/lookupindexedsymlinks[],dir_entries[],inodes[]usingdata_offset/inode_no/d_inotaken straight from the image, unbounded. The readlink guard was anassert()— compiled out in release.inodes + (inode_no - 1)withinode_no==0underflows.Fix: validate every index against the parsed table sizes; reject
inode_no==0.3.
namei_follow_linkoff-by-one (fs/vfs/vfs_lookup.cc)read_link(dp, lp, PATH_MAX, &sz)can setsz==PATH_MAX, thenlp[sz]=0writes 1 byte past thePATH_MAXbuffer (symlink target of on-disk size >= PATH_MAX). Fix: read at mostPATH_MAX-1.Severity
High (crafted image → kernel heap corruption) + Low (namei off-by-one). All in shipped
master.Verified: ROFS still mounts and boots to userspace, file access (lookup/readdir/readlink) works normally.