Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions modules/libext/ext_vnops.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion modules/tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
33 changes: 33 additions & 0 deletions tests/tst-ext-readlink.cc
Original file line number Diff line number Diff line change
@@ -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 <unistd.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

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;
}