ntfs3: log replay reads past the restart page buffer when the on-disk sys_page_size is larger - slab-out-of-bounds in log_replay()

From: Guikun Yang

Date: Fri Nov 06 2026 - 09:02:17 EST


Hi,

Mounting an NTFS volume whose restart page declares a sys_page_size
larger than the host page size ends in a slab-out-of-bounds read inside
log_replay(), during mount:

BUG: KASAN: slab-out-of-bounds Read in log_replay+0x...
Read of size 80 at addr ... (kmalloc-4k)
Call Trace:
log_replay fs/ntfs3/fslog.c
ntfs_load_log_and_replay
ntfs_fill_super

## The gap

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 by log_read_rst() -> read_log_page() while
log->page_size is still 4096, so the buffer holding it
(log->rst_info.r_page) is a 4096-byte allocation.

Later, log_replay() adopts the on-disk page size:

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 is left allocated at the old page size
and is never re-read:

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); /* fs/ntfs3/fslog.c:4176 */

The destination ra is kzalloc(log->restart_size) - sized from the
adopted page size, so it is large enough. The source ra2 points into
the still-4096-byte r_page, and ra_len is a raw on-disk field 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 = ra_off + 80,
the memcpy reads 80 bytes past the end of the kmalloc-4k object.

The branch at fslog.c:4181 has the same problem from the other side:

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

same source, same unbounded length.

## Observed

Deterministic, 6/6 runs with panic_on_warn=1; the unmodified volume
mounts cleanly as a control. Trigger is mount(2) alone - no file is
opened. The forged restart page carries a valid update sequence array,
a consistent client list and matching seq_num_bits, so every existing
check passes; only the page-size adoption is abused.

sys_page_size = 8192, ra_len chosen just past 4096 - ra_off gives the
reported 80-byte overread; larger ra_len values read further.

## Suggested fix

Re-read (and reallocate) the restart page at the adopted page size, or
grow rst_info.r_page alongside one_page_buf in the same branch, before
ra2 is derived from it:

if (log->page_size > old_page_size) {
...
log->one_page_buf = buf;
+ /* r_page was allocated with the old page size */
+ kfree(log->rst_info.r_page);
+ log->rst_info.r_page = kmalloc(log->page_size, GFP_NOFS);
+ /* re-read the restart page at the new size */
...
}

Alternatively, validating that ra_len fits within the buffer the page
was actually allocated with would bound the memcpy directly.

The reproducer (image builder that walks $MFT/$LogFile runlists and
writes the restart page with USA and client-list consistency, plus a
ready-made image and serial log) is available on request.

Tested on 7.3.0 (ntfs3 as a module); the adoption branch, ra_len
handling and the memcpy are unchanged in current mainline as of
2026-11-06.

Thanks,
Guikun Yang