ntfs3: two out-of-bounds reads in $LogFile replay (restart page resize, restart table offset)

From: Guikun Yang

Date: Fri Nov 06 2026 - 11:27:11 EST


Hi,

Two related out-of-bounds reads are reachable through $LogFile replay
during mount. Both are in fs/ntfs3/fslog.c, both need only a crafted
volume whose restart area is internally consistent (valid USA, client
list, seq_num_bits), and both are unfixed in current mainline.

BUG: KASAN: slab-out-of-bounds Read in log_replay+0x...
BUG: KASAN: slab-out-of-bounds Read in check_rstbl+0x...

Both reproduce 3/3 (6/6 for #1) with panic_on_warn=1; the unmodified
volume mounts cleanly as a control. Trigger is mount(2) alone; no file
is opened.

## 1. Restart page buffer not resized when sys_page_size is adopted

log_replay() starts with the host page size:

log->page_size = norm_file_page(PAGE_SIZE, &log->l_size, true); /* 4096 */
log->one_page_buf = kmalloc(log->page_size, GFP_NOFS);

The restart page is read while log->page_size is still 4096, so
log->rst_info.r_page is a 4096-byte allocation. Later the on-disk page
size is adopted:

t32 = le32_to_cpu(log->rst_info.r_page->sys_page_size);
if (log->page_size != t32) {
log->page_size = norm_file_page(t32, &log->l_size, ...);
if (log->page_size > old_page_size) {
buf = krealloc(log->one_page_buf, log->page_size, ...);
log->one_page_buf = buf;
}
}

5a35454179 ("fs/ntfs3: resize log->one_page_buf when adopting on-disk
page size") added exactly this krealloc, but it only covers
one_page_buf. rst_info.r_page stays at the old size and is never
re-read, and the restart area is then copied out of it:

ra2 = Add2Ptr(log->rst_info.r_page, ra_off);
log->ra_size = le16_to_cpu(ra2->ra_len); /* raw on-disk u16 */
memcpy(ra, ra2, log->ra_size);

The destination ra is kzalloc(log->restart_size), sized from the
adopted page size and large enough. The source ra2 points into the
still-4096-byte r_page, and ra_len is bounded only by
sys_page_size - ra_off. With sys_page_size = 8192 (a legal power of
two, accepted by norm_file_page) and ra_len just past 4096 - ra_off,
the memcpy reads past the end of the kmalloc-4k object (80 bytes in
our run). The branch at fslog.c:4181

memcpy(ra->clients, Add2Ptr(ra2, t16), le16_to_cpu(ra2->ra_len) - t16);

has the same problem from the other side.

## 2. Restart table redo_off is unbounded

The restart tables (transaction table, dirty page table, open
attribute table) are loaded through a shared path:

t16 = le16_to_cpu(lrh->redo_off);
rt = Add2Ptr(lrh, t16);
t32 = rec_len - t16;
if (!check_rstbl(rt, t32))
goto out;

redo_off is only checked for 8-byte alignment (check_log_rec()), not
against rec_len, and rec_len - t16 is an unsigned subtraction that
wraps when redo_off > rec_len. check_rstbl() then dereferences
rt->size as its first statement - either from the wrapped length or,
when redo_off lands at the end of the record buffer, from memory past
the page buffer entirely.

check_add_overflow() protection added for the record header only
covers client_data_len + rec_hdr_len; these in-record offsets are not
covered. In our run redo_off = 0xFD0 puts rt exactly at the end of the
4 KiB record buffer:

BUG: KASAN: slab-out-of-bounds in check_rstbl+0x...
Read of size 2 ...
check_rstbl -> log_replay -> ntfs_loadlog_and_replay -> ntfs_fill_super
Allocated by: read_log_page -> find_log_rec -> read_log_rec_lcb

All three table load sites share this code, so one forged offset
reaches the transaction table, the dirty page table and the open
attribute table alike.

## Environment common to both

The forged $LogFile is internally consistent - update sequence array,
client list, seq_num_bits - so every existing check passes; only the
missing bounds above are abused. The replay machinery needed two
details in last_log_lsn(): clearing RESTART_SINGLE_PAGE_IO, and giving
the tail page's last_end_lsn a different sequence number so the
major-1 tail-page logic does not rewrite file_off back to the LSN.

The reproducer (one image builder covering both cases, ready-made
images, serial logs) is available on request.

Both code paths are unchanged in mainline as of 2026-11-06.

## Suggested fixes

1. Re-read (or reallocate) rst_info.r_page at the adopted page size
before ra2 is derived from it; alternatively bound ra_len by the
size the page buffer was actually allocated with.
2. In the table load path, validate redo_off <= rec_len before
computing rt, and use check_add_overflow() for rec_len - t16 (or
reject redo_off > rec_len outright), matching what
e2fcb0040b did for client_data_len.

Thanks,
Guikun Yang