* [PATCH] ntfs3: fix OOB write in attr_wof_frame_info()
@ 2026-03-29 11:57 0xkato
2026-03-29 11:59 ` 0x Kato
2026-04-07 17:20 ` Konstantin Komarov
0 siblings, 2 replies; 3+ messages in thread
From: 0xkato @ 2026-03-29 11:57 UTC (permalink / raw)
To: almaz.alexandrovich; +Cc: ntfs3, 0xkato
In attr_wof_frame_info(), the offset-table read range for a nonresident
WofCompressedData stream is:
u64 from = vbo[i] & ~(u64)(PAGE_SIZE - 1);
u64 to = min(from + PAGE_SIZE, wof_size);
...
ntfs_read_run(sbi, run, addr, from, to - from);
A crafted image sets WofCompressedData.nres.data_size to 0xfff while the
file is large enough to request frame 1024 (offset 0x400000). This gives
from=0x1000, to=0xfff. The unsigned (to - from) wraps to 0xffffffffffffffff
and ntfs_read_write_run() overflows the single-page offs_folio via memcpy.
Triggered by pread() on a mounted NTFS image. Depending on adjacent
memory layout at the time of the overflow, KASAN reports this as
slab-out-of-bounds, use-after-free, or slab-use-after-free all at
ntfs_read_write_run(). Secondary corruption/panic paths were also observed.
Reject the read when the offset-table page is outside the stream.
Signed-off-by: 0xkato <0xkkato@gmail.com>
---
Reproducer:
Create the crafted NTFS image:
python3 create_wof_poc.py -o wof-poc.img
Mount it read-only with ntfs3:
sudo mount -t ntfs3 -o loop,ro wof-poc.img /mnt
Build the trigger:
cc -O2 -static wof_offset_table_read_trigger.c -o trigger
Trigger the bug:
./trigger /mnt/poc.bin 0x400000 1
KASAN report on 6.19.10:
==================================================================
BUG: KASAN: slab-out-of-bounds in ntfs_read_write_run+0x321/0x450 [ntfs3]
Write of size 4096 at addr ffff88800353b000 by task trigger-static/55
Call Trace:
__asan_memcpy+0x3c/0x60
ntfs_read_write_run+0x321/0x450 [ntfs3]
attr_wof_frame_info+0x52b/0xbc0 [ntfs3]
ni_read_frame+0x3cc/0xfe0 [ntfs3]
ni_read_folio_cmpr+0x3b9/0x820 [ntfs3]
read_pages+0x58a/0x810
page_cache_ra_unbounded+0x29c/0x5d0
filemap_get_pages+0x2c8/0x1530
filemap_read+0x2e7/0xb80
vfs_read+0x6da/0xa40
__x64_sys_pread64+0x195/0x250
==================================================================
fs/ntfs3/attrib.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/fs/ntfs3/attrib.c b/fs/ntfs3/attrib.c
index 6cb9bc5d6..89921e509 100644
--- a/fs/ntfs3/attrib.c
+++ b/fs/ntfs3/attrib.c
@@ -1576,6 +1576,12 @@ int attr_wof_frame_info(struct ntfs_inode *ni, struct ATTRIB *attr,
u64 from = vbo[i] & ~(u64)(PAGE_SIZE - 1);
u64 to = min(from + PAGE_SIZE, wof_size);
+ if (from >= wof_size) {
+ _ntfs_bad_inode(&ni->vfs_inode);
+ err = -EINVAL;
+ goto out1;
+ }
+
err = attr_load_runs_range(ni, ATTR_DATA, WOF_NAME,
ARRAY_SIZE(WOF_NAME), run,
from, to);
--
2.50.1 (Apple Git-155)
^ permalink raw reply related [flat|nested] 3+ messages in thread
* Re: [PATCH] ntfs3: fix OOB write in attr_wof_frame_info()
2026-03-29 11:57 [PATCH] ntfs3: fix OOB write in attr_wof_frame_info() 0xkato
@ 2026-03-29 11:59 ` 0x Kato
2026-04-07 17:20 ` Konstantin Komarov
1 sibling, 0 replies; 3+ messages in thread
From: 0x Kato @ 2026-03-29 11:59 UTC (permalink / raw)
To: almaz.alexandrovich; +Cc: ntfs3
[-- Attachment #1.1: Type: text/plain, Size: 3163 bytes --]
Attaching the PoC image generator and trigger program referenced in the
reproducer.
On Sun, 29 Mar 2026 at 13:58, 0xkato <0xkkato@gmail.com> wrote:
> In attr_wof_frame_info(), the offset-table read range for a nonresident
> WofCompressedData stream is:
>
> u64 from = vbo[i] & ~(u64)(PAGE_SIZE - 1);
> u64 to = min(from + PAGE_SIZE, wof_size);
> ...
> ntfs_read_run(sbi, run, addr, from, to - from);
>
> A crafted image sets WofCompressedData.nres.data_size to 0xfff while the
> file is large enough to request frame 1024 (offset 0x400000). This gives
> from=0x1000, to=0xfff. The unsigned (to - from) wraps to 0xffffffffffffffff
> and ntfs_read_write_run() overflows the single-page offs_folio via memcpy.
>
> Triggered by pread() on a mounted NTFS image. Depending on adjacent
> memory layout at the time of the overflow, KASAN reports this as
> slab-out-of-bounds, use-after-free, or slab-use-after-free all at
> ntfs_read_write_run(). Secondary corruption/panic paths were also observed.
>
> Reject the read when the offset-table page is outside the stream.
>
> Signed-off-by: 0xkato <0xkkato@gmail.com>
> ---
> Reproducer:
>
> Create the crafted NTFS image:
> python3 create_wof_poc.py -o wof-poc.img
>
> Mount it read-only with ntfs3:
> sudo mount -t ntfs3 -o loop,ro wof-poc.img /mnt
>
> Build the trigger:
> cc -O2 -static wof_offset_table_read_trigger.c -o trigger
>
> Trigger the bug:
> ./trigger /mnt/poc.bin 0x400000 1
>
> KASAN report on 6.19.10:
>
> ==================================================================
> BUG: KASAN: slab-out-of-bounds in ntfs_read_write_run+0x321/0x450 [ntfs3]
> Write of size 4096 at addr ffff88800353b000 by task trigger-static/55
>
> Call Trace:
> __asan_memcpy+0x3c/0x60
> ntfs_read_write_run+0x321/0x450 [ntfs3]
> attr_wof_frame_info+0x52b/0xbc0 [ntfs3]
> ni_read_frame+0x3cc/0xfe0 [ntfs3]
> ni_read_folio_cmpr+0x3b9/0x820 [ntfs3]
> read_pages+0x58a/0x810
> page_cache_ra_unbounded+0x29c/0x5d0
> filemap_get_pages+0x2c8/0x1530
> filemap_read+0x2e7/0xb80
> vfs_read+0x6da/0xa40
> __x64_sys_pread64+0x195/0x250
> ==================================================================
>
> fs/ntfs3/attrib.c | 6 ++++++
> 1 file changed, 6 insertions(+)
>
> diff --git a/fs/ntfs3/attrib.c b/fs/ntfs3/attrib.c
> index 6cb9bc5d6..89921e509 100644
> --- a/fs/ntfs3/attrib.c
> +++ b/fs/ntfs3/attrib.c
> @@ -1576,6 +1576,12 @@ int attr_wof_frame_info(struct ntfs_inode *ni,
> struct ATTRIB *attr,
> u64 from = vbo[i] & ~(u64)(PAGE_SIZE - 1);
> u64 to = min(from + PAGE_SIZE, wof_size);
>
> + if (from >= wof_size) {
> + _ntfs_bad_inode(&ni->vfs_inode);
> + err = -EINVAL;
> + goto out1;
> + }
> +
> err = attr_load_runs_range(ni, ATTR_DATA, WOF_NAME,
> ARRAY_SIZE(WOF_NAME),
> run,
> from, to);
> --
> 2.50.1 (Apple Git-155)
>
>
[-- Attachment #1.2: Type: text/html, Size: 4141 bytes --]
[-- Attachment #2: create_wof_poc.py --]
[-- Type: text/x-python-script, Size: 3676 bytes --]
#!/usr/bin/env python3
"""
Restore the preserved ntfs3 WOF offset-table PoC image from the minimal archive.
The original PoC generator was lost from /tmp, but the known-good lab image and
manifest still exist inside /home/kato/Downloads/ntfs3-wof-poc-minimal.tar.gz.
This script reconstructs the report attachment interface:
python3 create_wof_poc.py -o wof-poc.img
Optionally emit the preserved manifest as well:
python3 create_wof_poc.py -o wof-poc.img --manifest wof-poc.env
"""
from __future__ import annotations
import argparse
import hashlib
import shutil
import sys
import tarfile
from pathlib import Path
ARCHIVE_NAME = "ntfs3-wof-poc-minimal.tar.gz"
IMAGE_MEMBER = "tools/research/ntfs3/lab/wof-poc.img"
MANIFEST_MEMBER = "tools/research/ntfs3/lab/wof-poc.env"
DEFAULT_ARCHIVE = Path(__file__).resolve().parent.parent / ARCHIVE_NAME
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Recreate the ntfs3 WOF offset-table PoC image from the saved archive.",
)
parser.add_argument(
"-o",
"--output",
required=True,
type=Path,
help="Path to write the PoC image to.",
)
parser.add_argument(
"--manifest",
type=Path,
help="Optional path to also write the preserved manifest to.",
)
parser.add_argument(
"--archive",
type=Path,
default=DEFAULT_ARCHIVE,
help=f"Archive containing the saved PoC assets (default: {DEFAULT_ARCHIVE}).",
)
parser.add_argument(
"-f",
"--force",
action="store_true",
help="Overwrite existing output files.",
)
return parser.parse_args()
def ensure_writable(path: Path, force: bool) -> None:
if path.exists() and not force:
raise FileExistsError(f"refusing to overwrite existing file: {path}")
path.parent.mkdir(parents=True, exist_ok=True)
def extract_member(tf: tarfile.TarFile, member_name: str, dst: Path, force: bool) -> int:
ensure_writable(dst, force)
member = tf.getmember(member_name)
src = tf.extractfile(member)
if src is None:
raise FileNotFoundError(f"archive member is not a regular file: {member_name}")
with src, open(dst, "wb") as out:
shutil.copyfileobj(src, out)
return member.size
def sha256sum(path: Path) -> str:
digest = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def main() -> int:
args = parse_args()
archive = args.archive.resolve()
output = args.output.resolve()
manifest = args.manifest.resolve() if args.manifest else None
if not archive.is_file():
print(f"error: archive not found: {archive}", file=sys.stderr)
return 1
try:
with tarfile.open(archive, "r:gz") as tf:
image_size = extract_member(tf, IMAGE_MEMBER, output, args.force)
manifest_size = None
if manifest is not None:
manifest_size = extract_member(tf, MANIFEST_MEMBER, manifest, args.force)
except (FileExistsError, FileNotFoundError, KeyError, tarfile.TarError, OSError) as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
print(f"wrote image: {output} ({image_size} bytes)")
print(f"sha256: {sha256sum(output)}")
if manifest is not None and manifest_size is not None:
print(f"wrote manifest: {manifest} ({manifest_size} bytes)")
print("target file: /poc.bin")
print("trigger offset: 0x400000")
return 0
if __name__ == "__main__":
raise SystemExit(main())
[-- Attachment #3: wof_offset_table_read_trigger.c --]
[-- Type: application/octet-stream, Size: 1678 bytes --]
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
static void usage(const char *prog)
{
fprintf(stderr,
"usage: %s <path> [offset] [length]\n"
" offset defaults to 0x400000\n"
" length defaults to 1\n",
prog);
}
static unsigned long long parse_ull(const char *arg, const char *name)
{
char *end = NULL;
unsigned long long value;
errno = 0;
value = strtoull(arg, &end, 0);
if (errno || !end || *end) {
fprintf(stderr, "invalid %s: %s\n", name, arg);
exit(2);
}
return value;
}
int main(int argc, char **argv)
{
const char *path;
unsigned long long offset = 0x400000ull;
unsigned long long length = 1;
char *buf;
ssize_t ret;
int fd;
if (argc < 2 || argc > 4) {
usage(argv[0]);
return 2;
}
path = argv[1];
if (argc >= 3)
offset = parse_ull(argv[2], "offset");
if (argc >= 4)
length = parse_ull(argv[3], "length");
if (!length || length > (1ull << 20)) {
fprintf(stderr, "length must be between 1 and 1048576\n");
return 2;
}
buf = malloc((size_t)length);
if (!buf) {
fprintf(stderr, "malloc(%llu) failed\n", length);
return 1;
}
fd = open(path, O_RDONLY);
if (fd < 0) {
fprintf(stderr, "open(%s) failed: %s\n", path, strerror(errno));
free(buf);
return 1;
}
ret = pread(fd, buf, (size_t)length, (off_t)offset);
if (ret < 0) {
fprintf(stderr, "pread(%s, 0x%llx, %llu) failed: %s\n",
path, offset, length, strerror(errno));
close(fd);
free(buf);
return 1;
}
printf("pread(%s, 0x%llx, %llu) -> %zd\n", path, offset, length, ret);
close(fd);
free(buf);
return ret > 0 ? 0 : 1;
}
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCH] ntfs3: fix OOB write in attr_wof_frame_info()
2026-03-29 11:57 [PATCH] ntfs3: fix OOB write in attr_wof_frame_info() 0xkato
2026-03-29 11:59 ` 0x Kato
@ 2026-04-07 17:20 ` Konstantin Komarov
1 sibling, 0 replies; 3+ messages in thread
From: Konstantin Komarov @ 2026-04-07 17:20 UTC (permalink / raw)
To: 0xkato; +Cc: ntfs3
On 3/29/26 13:57, 0xkato wrote:
> [You don't often get email from 0xkkato@gmail.com. Learn why this is important at https://aka.ms/LearnAboutSenderIdentification ]
>
> In attr_wof_frame_info(), the offset-table read range for a nonresident
> WofCompressedData stream is:
>
> u64 from = vbo[i] & ~(u64)(PAGE_SIZE - 1);
> u64 to = min(from + PAGE_SIZE, wof_size);
> ...
> ntfs_read_run(sbi, run, addr, from, to - from);
>
> A crafted image sets WofCompressedData.nres.data_size to 0xfff while the
> file is large enough to request frame 1024 (offset 0x400000). This gives
> from=0x1000, to=0xfff. The unsigned (to - from) wraps to 0xffffffffffffffff
> and ntfs_read_write_run() overflows the single-page offs_folio via memcpy.
>
> Triggered by pread() on a mounted NTFS image. Depending on adjacent
> memory layout at the time of the overflow, KASAN reports this as
> slab-out-of-bounds, use-after-free, or slab-use-after-free all at
> ntfs_read_write_run(). Secondary corruption/panic paths were also observed.
>
> Reject the read when the offset-table page is outside the stream.
>
> Signed-off-by: 0xkato <0xkkato@gmail.com>
> ---
> Reproducer:
>
> Create the crafted NTFS image:
> python3 create_wof_poc.py -o wof-poc.img
>
> Mount it read-only with ntfs3:
> sudo mount -t ntfs3 -o loop,ro wof-poc.img /mnt
>
> Build the trigger:
> cc -O2 -static wof_offset_table_read_trigger.c -o trigger
>
> Trigger the bug:
> ./trigger /mnt/poc.bin 0x400000 1
>
> KASAN report on 6.19.10:
>
> ==================================================================
> BUG: KASAN: slab-out-of-bounds in ntfs_read_write_run+0x321/0x450 [ntfs3]
> Write of size 4096 at addr ffff88800353b000 by task trigger-static/55
>
> Call Trace:
> __asan_memcpy+0x3c/0x60
> ntfs_read_write_run+0x321/0x450 [ntfs3]
> attr_wof_frame_info+0x52b/0xbc0 [ntfs3]
> ni_read_frame+0x3cc/0xfe0 [ntfs3]
> ni_read_folio_cmpr+0x3b9/0x820 [ntfs3]
> read_pages+0x58a/0x810
> page_cache_ra_unbounded+0x29c/0x5d0
> filemap_get_pages+0x2c8/0x1530
> filemap_read+0x2e7/0xb80
> vfs_read+0x6da/0xa40
> __x64_sys_pread64+0x195/0x250
> ==================================================================
>
> fs/ntfs3/attrib.c | 6 ++++++
> 1 file changed, 6 insertions(+)
>
> diff --git a/fs/ntfs3/attrib.c b/fs/ntfs3/attrib.c
> index 6cb9bc5d6..89921e509 100644
> --- a/fs/ntfs3/attrib.c
> +++ b/fs/ntfs3/attrib.c
> @@ -1576,6 +1576,12 @@ int attr_wof_frame_info(struct ntfs_inode *ni, struct ATTRIB *attr,
> u64 from = vbo[i] & ~(u64)(PAGE_SIZE - 1);
> u64 to = min(from + PAGE_SIZE, wof_size);
>
> + if (from >= wof_size) {
> + _ntfs_bad_inode(&ni->vfs_inode);
> + err = -EINVAL;
> + goto out1;
> + }
> +
> err = attr_load_runs_range(ni, ATTR_DATA, WOF_NAME,
> ARRAY_SIZE(WOF_NAME), run,
> from, to);
> --
> 2.50.1 (Apple Git-155)
>
Hello,
Your patch is applied. Thank you.
Regards,
Konstantin
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-04-07 17:20 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-29 11:57 [PATCH] ntfs3: fix OOB write in attr_wof_frame_info() 0xkato
2026-03-29 11:59 ` 0x Kato
2026-04-07 17:20 ` Konstantin Komarov
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox