mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH] fs/ntfs3: validate dlen before copying filename dup in do_action()
@ 2026-11-13 16:46 Junyoung Jang
  0 siblings, 0 replies; only message in thread
From: Junyoung Jang @ 2026-11-13 16:46 UTC (permalink / raw)
  To: Konstantin Komarov; +Cc: Junyoung Jang, ntfs3, linux-kernel

In log_replay(), data and dlen are derived from the on-disk redo_off/len
and undo_off/len fields. When handling UpdateFileNameRoot and
UpdateFileNameAllocation in do_action(), the code unconditionally copies
sizeof(fname->dup) (56 bytes) from data:

    memmove(&fname->dup, data, sizeof(fname->dup));

Neither handler checks that dlen is at least sizeof(fname->dup), and the
generic log record validation does not enforce this operation-specific
minimum.

A crafted journal record with client_data_len=0x98, redo_off=0x90,
redo_len=1, and the multi-page flag set passes the record range check
(0x90 + 1 <= 0x98), allocates a 152-byte buffer in find_log_rec(), and
triggers a 48-byte KASAN slab-out-of-bounds read past the end of that
buffer during a read-write mount:

  BUG: KASAN: slab-out-of-bounds in do_action.isra.0+0x353f/0x83c0
  Read of size 56 at addr ffff888007d9bc90 by task busybox/67
  Call Trace:
   <TASK>
   __asan_memmove+0x23/0x60
   do_action.isra.0+0x353f/0x83c0
   find_log_rec+0x277/0x500
   ...
  The buggy address belongs to the object at ffff888007d9bc00
   allocated 152-byte region [ffff888007d9bc00, ffff888007d9bc98)

Validate that dlen is at least sizeof(fname->dup) before copying in both
UpdateFileNameRoot and UpdateFileNameAllocation, jumping to dirty_vol on
a short payload.

Fixes: b46acd6a6a62 ("fs/ntfs3: Add NTFS journal")
Cc: stable@vger.kernel.org
Signed-off-by: Junyoung Jang <graypanda.inzag@gmail.com>
---
Notes / Reproducer:

- Tested on Linux 7.4.0-rc2 (067c5c24cef2) x86-64 in QEMU/KVM with
  CONFIG_NTFS3_FS=y, CONFIG_KASAN=y, CONFIG_KASAN_INLINE=y, and
  CONFIG_FORTIFY_SOURCE=n (patched kernel mounts cleanly with 0 KASAN
  splats).
- Note that previously posted patches bounding the destination index
  entry or checking off + len <= client_data_len do not catch this case,
  because the destination entry is valid and 0x90 + 1 <= 0x98 holds.
- Disclosure: Assisted by a custom LLM-based tool; all results,
  including the root-cause analysis, reproducer, and patch, were
  manually reviewed and verified by a human.

To reproduce:
1. Fetch windows-interrupted-1.img.gz from:
   https://raw.githubusercontent.com/antimatter-studios/rust-fs-ntfs/main/test-disks/windows-interrupted-1.img.gz
2. Patch 3 bytes using poc.py below (`python3 poc.py windows-interrupted-1.img.gz short-source.img`)
   and mount read-write (`mount -t ntfs3 -o force short-source.img /mnt`).

Reproducer script (poc.py):

  #!/usr/bin/env python3
  # SPDX-License-Identifier: GPL-2.0-only
  import argparse, gzip, hashlib
  from pathlib import Path

  BASE_SHA256 = "c45659e95104eca5f793c3f21061f42705a01783e62d1c7a46c63e4089db0b9a"
  TRIGGER_SHA256 = "0e176bdd374e73dc3f40163320d6d69bfe36fe664735a50001a3bcbcad2ec2e4"
  CHANGES = (
      (0x292F878, 0x00, 0x01),  # LFS_RECORD_HDR.flags: separate allocation
      (0x292F884, 0x28, 0x90),  # LOG_REC_HDR.redo_off low byte
      (0x292F886, 0x38, 0x01),  # LOG_REC_HDR.redo_len low byte
  )

  parser = argparse.ArgumentParser()
  parser.add_argument("fixture_gz", type=Path)
  parser.add_argument("output_img", type=Path)
  args = parser.parse_args()

  image = bytearray(gzip.decompress(args.fixture_gz.read_bytes()))
  assert hashlib.sha256(image).hexdigest() == BASE_SHA256
  for offset, old, new in CHANGES:
      assert image[offset] == old
      image[offset] = new
  assert hashlib.sha256(image).hexdigest() == TRIGGER_SHA256
  args.output_img.write_bytes(image)

 fs/ntfs3/fslog.c | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/fs/ntfs3/fslog.c b/fs/ntfs3/fslog.c
index bb8d40bb5869..29840814c82e 100644
--- a/fs/ntfs3/fslog.c
+++ b/fs/ntfs3/fslog.c
@@ -3568,6 +3568,8 @@ static int do_action(struct ntfs_log *log, struct OPEN_ATTR_ENRTY *oe,
 
 		e = Add2Ptr(attr, le16_to_cpu(lrh->attr_off));
 		fname = (struct ATTR_FILE_NAME *)(e + 1);
+		if (dlen < sizeof(fname->dup))
+			goto dirty_vol;
 		memmove(&fname->dup, data, sizeof(fname->dup)); //
 		mi->dirty = true;
 		break;
@@ -3753,6 +3755,8 @@ static int do_action(struct ntfs_log *log, struct OPEN_ATTR_ENRTY *oe,
 		}
 
 		fname = (struct ATTR_FILE_NAME *)(e + 1);
+		if (dlen < sizeof(fname->dup))
+			goto dirty_vol;
 		memmove(&fname->dup, data, sizeof(fname->dup));
 
 		a_dirty = true;
-- 
2.43.0

^ permalink raw reply	[flat|nested] only message in thread

only message in thread, other threads:[~2026-11-13 16:46 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-13 16:46 [PATCH] fs/ntfs3: validate dlen before copying filename dup in do_action() Junyoung Jang

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®