diff --git a/modules/libext/ext_vnops.cc b/modules/libext/ext_vnops.cc index 063fe1e46..f91f687e2 100644 --- a/modules/libext/ext_vnops.cc +++ b/modules/libext/ext_vnops.cc @@ -635,6 +635,13 @@ ext_readdir(struct vnode *dvp, struct file *fp, struct dirent *dir) if (ext4_dir_en_get_inode(it.curr) != 0) { memset(dir->d_name, 0, sizeof(dir->d_name)); uint16_t name_length = ext4_dir_en_get_name_len(&fs->sb, it.curr); + // On an old-rev (rev0, minor<5) image ext4_dir_en_get_name_len can + // fold in name_length_high and return up to block_size-8 (~4 KiB), + // while d_name is only sizeof(dir->d_name) (256). Clamp so a + // crafted directory entry cannot overflow the fixed d_name buffer. + if (name_length >= sizeof(dir->d_name)) { + name_length = sizeof(dir->d_name) - 1; + } memcpy(dir->d_name, it.curr->name, name_length); ext_debug("readdir directory with i-node=%ld at offset:%ld => entry name:%s\n", dvp->v_ino, file_offset(fp), dir->d_name); @@ -1341,7 +1348,24 @@ ext_readlink(vnode_t *vp, uio_t *uio) return uiomove(content, fsize, uio); } else { uint32_t block_size = ext4_sb_get_block_size(&fs->sb); + // A symlink target is stored in at most one block (and is bounded by + // PATH_MAX); a slow symlink never spans more. The inode size is read + // straight off disk and is fully attacker-controlled for a crafted + // image, so clamp it to block_size before reading into the block_size + // buffer -- otherwise ext_internal_read() would write @fsize bytes + // (up to 2^64-1) into a block_size heap allocation (heap overflow). + if (fsize > block_size) { + return EINVAL; // malformed symlink inode + } + // Also guard the read start: ext_internal_read subtracts offset from + // fsize, so an offset past EOF would underflow the read length. + if ((uint64_t)uio->uio_offset >= fsize) { + return 0; + } void *buf = malloc(block_size); + if (!buf) { + return ENOMEM; + } size_t read_count = 0; int ret = ext_internal_read(fs, &inode_ref._ref, uio->uio_offset, buf, fsize, &read_count); if (ret) { diff --git a/modules/tests/Makefile b/modules/tests/Makefile index 1eb3df46e..3bce52b40 100644 --- a/modules/tests/Makefile +++ b/modules/tests/Makefile @@ -115,7 +115,7 @@ ext-only-tests := tst-readdir.so tst-concurrent-read.so tst-fs-link.so specific-fs-tests := $($(fs_type)-only-tests) -tests := tst-pthread.so misc-ramdisk.so tst-vblk.so tst-mq-smoke.so tst-bsd-evh.so \ +tests := tst-ext-readlink.so tst-pthread.so misc-ramdisk.so tst-vblk.so tst-mq-smoke.so tst-bsd-evh.so \ misc-bsd-callout.so tst-bsd-kthread.so tst-bsd-taskqueue.so \ tst-fpu.so tst-preempt.so tst-tracepoint.so tst-hub.so \ misc-console.so misc-leak.so misc-readbench.so misc-mmap-anon-perf.so \ diff --git a/tests/tst-ext-readlink.cc b/tests/tst-ext-readlink.cc new file mode 100644 index 000000000..8167f5d66 --- /dev/null +++ b/tests/tst-ext-readlink.cc @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2026 Greg Burd + * + * This work is open source software, licensed under the terms of the + * BSD license as described in the LICENSE file in the top-level directory. + */ + +// Regression test for the ext_readlink hardening: a normal symlink must still +// read back correctly (fast + slow), and the bounds guards must not break it. +#include +#include +#include +#include + +int main(int argc, char** argv) +{ + const char* link = (argc > 1) ? argv[1] : "/data/mylink"; + char buf[256]; + memset(buf, 0, sizeof(buf)); + ssize_t n = readlink(link, buf, sizeof(buf) - 1); + if (n < 0) { + fprintf(stderr, "readlink(%s) FAILED: %s\n", link, strerror(errno)); + return 1; + } + buf[n] = 0; + fprintf(stderr, "readlink(%s) = '%s' (%zd bytes)\n", link, buf, n); + if (strcmp(buf, "realfile") != 0) { + fprintf(stderr, "READLINK MISMATCH: expected 'realfile'\n"); + return 1; + } + fprintf(stderr, "ext-readlink regression PASSED\n"); + return 0; +}