Hi Andrew,
Happy to. Short version: ast2600-evb can't hit the SMP timing window,
so I reproduce each missing piece deliberately. The driver code under
test is unmodified -- only the stimulus and the post-race state are
injected. Stock qemu-system-arm (Debian 8.2.2), no QEMU changes.
Three obstacles, and what I did about each:
1. No BIOS to emit POST codes -- an injection module stages bytes into
the snoop registers via the LPC syscon regmap (SNPWDR + the HICR6
data-ready bit).
2. QEMU doesn't raise the snoop IRQ for those writes -- after staging,
the module dispatches it in software with
generic_handle_irq_safe(sdev->irq), which runs the driver's real
aspeed_lpc_snoop_irq() -> put_fifo_with_discard() path.
3. The SMP race won't trigger under TCG -- so I reconstruct its outcome
instead: force the channel-0 kfifo to in=4097, out=1, i.e.
(in - out) = 4096 > the 2048-byte ring, the exact state a reader
observes inside the window.
One caveat so it isn't misread: step 3 writes in/out directly, so it
bypasses the new lock. The patched run therefore shows the read path no
longer turns a corrupt (in - out) into a usercopy overflow; the lock's
job of preventing that state is the SPSC argument from the commit, which
TCG can't exercise.
== Tree / config ==
base: v7.1-rc7
clang: 22.1.7 (LLVM=-22; needed for the context-analysis check)
ARM multi_v7_defconfig + CONFIG_CC_IS_CLANG, WARN_CONTEXT_ANALYSIS,
SMP, ASPEED_LPC_SNOOP, HARDENED_USERCOPY, PROVE_LOCKING,
DEBUG_ATOMIC_SLEEP.
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- LLVM=-22 O=build \
-j$(nproc) zImage
== Run ==
The injection module (full source below the sign-off) creates two
write-only sysfs knobs under /sys/kernel/snoop_test/. The init loads it,
then:
echo 4096 > /sys/kernel/snoop_test/generate # fill via the real
# IRQ path
echo 1 > /sys/kernel/snoop_test/adjust_ptrs # force in=4097/out=1
read(fd, buf, 4096) from /dev/aspeed-lpc-snoop0 # the overflowing read
Build it against the same tree (-DSNOOP_PATCHED for v6, which mirrors the
spinlock the fix adds ahead of @fifo; omit it for the unpatched build).
qemu-system-arm -M ast2600-evb -smp 2 \
-kernel build/arch/arm/boot/zImage \
-dtb build/arch/arm/boot/dts/aspeed/aspeed-ast2600-evb.dtb \
-initrd repro.cpio.gz \
-append "console=ttyS4,115200 panic=-1" -nographic -no-reboot
== Result ==
Unpatched, read(4096) with in=4097/out=1:
usercopy: Kernel memory exposure attempt detected from SLUB object
'kmalloc-2k' (offset 0, size 2049)!
kfifo_copy_to_user / __kfifo_to_user / snoop_file_read / vfs_read
Kernel panic - not syncing: Fatal exception
Patched: read() returns 2048, no panic; no lockdep or atomic-sleep
splats.
The init is just: mount proc/sysfs/devtmpfs, finit_module() the .ko,
write the two knobs above, then read(4096) from the char device. Full
injection module follows.
Thanks,
Karthikeyan
------ snoop_test_inject.c (build as an out-of-tree module) ------
// SPDX-License-Identifier: GPL-2.0
/*
* Reproduce the aspeed-lpc-snoop kfifo SPSC-violation post-race state
* deterministically under QEMU. Two write-only sysfs knobs under
* /sys/kernel/snoop_test/:
*
* generate <count> Push <count> POST-code bytes through the *real*
* driver IRQ path: write SNPWDR + HICR6 via the LPC
* syscon regmap, then dispatch the snoop IRQ so
* aspeed_lpc_snoop_irq() -> put_fifo_with_discard()
* runs.
*
* adjust_ptrs <1> Force the channel-0 kfifo into the state a reader
* observes inside the race window: in = 4097,
* out = 1, so (in - out) = 4096 > the 2048-byte ring.
*
* The driver's private channel is reached through a mirror struct whose
* layout must match drivers/soc/aspeed/aspeed-lpc-snoop.c. The v6 fix
* inserts a spinlock_t ahead of @fifo -- build with -DSNOOP_PATCHED to
* mirror that, otherwise the &fifo offset is wrong.
*/
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/version.h>
#include <linux/fs.h>
#include <linux/file.h>
#include <linux/kfifo.h>
#include <linux/miscdevice.h>
#include <linux/regmap.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/irqdesc.h>
#include <linux/device.h>
#include <linux/kobject.h>
#include <linux/sysfs.h>
#include <linux/wait.h>
#include <linux/spinlock.h>
#include <linux/bitops.h>
#define HICR6 0x84
#define HICR6_STR_SNP0W BIT(0)
#define SNPWDR 0x94
#define SNOOP_DEV "/dev/aspeed-lpc-snoop0"
#define RACE_OUT 1u
#define RACE_IN 4097u
struct snoop_chan_mirror {
const void *cfg;
bool enabled;
#ifdef SNOOP_PATCHED
spinlock_t lock; /* added by the fix */
#endif
struct kfifo fifo;
wait_queue_head_t wq;
struct miscdevice miscdev;
};
struct snoop_dev_mirror {
struct regmap *regmap;
int irq;
/* struct clk *clk; struct aspeed_lpc_snoop_channel chan[2]; follow */
};
static struct file *snoop_open(struct snoop_chan_mirror **chan_out)
{
struct file *filp;
struct miscdevice *md;
filp = filp_open(SNOOP_DEV, O_RDONLY | O_NONBLOCK, 0);
if (IS_ERR(filp))
return filp;
md = filp->private_data;
if (!md) {
filp_close(filp, NULL);
return ERR_PTR(-ENODEV);
}
*chan_out = container_of(md, struct snoop_chan_mirror, miscdev);
return filp;
}
static ssize_t generate_store(struct kobject *kobj, struct kobj_attribute *attr,
const char *buf, size_t len)
{
struct snoop_chan_mirror *chan;
struct snoop_dev_mirror *sdev;
struct file *filp;
unsigned int count, i;
int rc;
rc = kstrtouint(buf, 0, &count);
if (rc)
return rc;
filp = snoop_open(&chan);
if (IS_ERR(filp))
return PTR_ERR(filp);
sdev = dev_get_drvdata(chan->miscdev.parent);
if (!sdev || !sdev->regmap || sdev->irq <= 0) {
filp_close(filp, NULL);
return -ENODEV;
}
for (i = 0; i < count; i++) {
/* Stage the snoop'ed byte and the data-ready status bit. */
regmap_write(sdev->regmap, SNPWDR, (u32)(i & 0xff));
regmap_write(sdev->regmap, HICR6, HICR6_STR_SNP0W);
/*
* Dispatch IRQ -> aspeed_lpc_snoop_irq() ->
* put_fifo_with_discard(). generic_handle_irq_safe() copes
* with the GIC requiring the handler to run with IRQs off.
*/
generic_handle_irq_safe(sdev->irq);
}
filp_close(filp, NULL);
return len;
}
static ssize_t adjust_ptrs_store(struct kobject *kobj, struct kobj_attribute
*attr,
const char *buf, size_t len)
{
struct snoop_chan_mirror *chan;
struct file *filp;
struct __kfifo *kf;
unsigned int val;
int rc;
rc = kstrtouint(buf, 0, &val);
if (rc)
return rc;
if (val != 1)
return -EINVAL;
filp = snoop_open(&chan);
if (IS_ERR(filp))
return PTR_ERR(filp);
kf = &chan->fifo.kfifo;
/* Reproduce the race outcome: fresh 'in', stale 'out'. */
WRITE_ONCE(kf->out, RACE_OUT);
WRITE_ONCE(kf->in, RACE_IN);
pr_info("snoop_test: in=%u out=%u (in-out=%u, size=%u)\n",
kf->in, kf->out, kf->in - kf->out, kf->mask + 1);
filp_close(filp, NULL);
return len;
}
static struct kobj_attribute generate_attr =
__ATTR(generate, 0220, NULL, generate_store);
static struct kobj_attribute adjust_ptrs_attr =
__ATTR(adjust_ptrs, 0220, NULL, adjust_ptrs_store);
static struct attribute *snoop_attrs[] = {
&generate_attr.attr,
&adjust_ptrs_attr.attr,
NULL,
};
static const struct attribute_group snoop_group = { .attrs = snoop_attrs };
static struct kobject *snoop_kobj;
static int __init snoop_test_init(void)
{
int rc;
snoop_kobj = kobject_create_and_add("snoop_test", kernel_kobj);
if (!snoop_kobj)
return -ENOMEM;
rc = sysfs_create_group(snoop_kobj, &snoop_group);
if (rc) {
kobject_put(snoop_kobj);
return rc;
}
return 0;
}
static void __exit snoop_test_exit(void)
{
sysfs_remove_group(snoop_kobj, &snoop_group);
kobject_put(snoop_kobj);
}
module_init(snoop_test_init);
module_exit(snoop_test_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("aspeed-lpc-snoop kfifo race post-state reproducer");