From 2b9d2b87fbebfd9d3c39ade8573a0b7075e4ba77 Mon Sep 17 00:00:00 2001 From: Tomasz Leman Date: Thu, 16 Jul 2026 20:48:31 +0200 Subject: [PATCH] audio: copier: reject overflowing gateway config_length Fix a heap out-of-bounds access found by the IPC4 libFuzzer harness (native_sim/x86, ASAN-enabled build with CONFIG_SYS_HEAP_SANITIZER_ASAN). A single 173-byte crash input triggers a use-after-poison READ of size 4 in copier_dai_create(): ==ERROR: AddressSanitizer: use-after-poison on address 0x088cf1c4 READ of size 4 ... in copier_dai_create copier_dai.c:284 #1 copier_init copier.c:220 #2 module_init generic.c:123 #3 module_adapter_new_ext module_adapter.c:307 #4 comp_new_ipc4 ipc4/helper.c:198 #5 ipc4_init_module_instance handler-user.c:763 copier_dai.c:284 reads cd->config.gtw_cfg.config_length, which lands 8 bytes past the end of the under-allocated cd block. Fuzzer payload -------------- The harness frames the raw input as a 2-byte little-endian length prefix followed by an 8-byte IPC4 compact header (primary + extension word) and a payload that is mirrored into MAILBOX_HOSTBOX. The decoded message is an INIT_MODULE_INSTANCE (MODULE_MSG / INIT_INSTANCE) whose module_id resolves to the copier driver, so comp_new_ipc4() -> module_adapter_new() -> copier_init() runs on host-controlled config bytes. The copier's ipc4_copier_module_cfg carried (values from gdb): gtw_cfg.node_id.dw = 0x050012f5 dma_type = 18 (ipc4_alh_uaol_stream_link_ output_class, a non-host ALH/UAOL link gateway), v_index = 245 gtw_cfg.dma_buffer_size = 0xffffffff gtw_cfg.config_length = 0xffffffff <-- the trigger md->cfg.size (payload) = 888 bytes node_id != IPC4_INVALID_NODE_ID selects the gateway-copier path, and the UAOL link class routes into copier_dai_create(). The long run of 0xff bytes at the tail of the input is what supplies config_length = 0xffffffff. libFuzzer discovered the message by mutating the corpus; the crucial field is the all-ones config_length. Root cause ---------- copier_init() sizes the variable-length gateway blob that trails the config struct: if (copier->gtw_cfg.config_length > 1) { gtw_cfg_var_size += (copier->gtw_cfg.config_length - 1) << 2; cfg_total_size += gtw_cfg_var_size; } if (cfg_total_size > md->cfg.size) /* payload guard */ return -EINVAL; cd = mod_zalloc(mod, sizeof(*cd) + gtw_cfg_var_size); config_length is a uint32_t from the untrusted payload. The expression (config_length - 1) << 2 is evaluated in 32-bit arithmetic and wraps: with 0xffffffff it yields 0xfffffff8, i.e. -8 as a size_t. That poisons both dependent computations: * cfg_total_size wraps down to 76 (below even sizeof(*copier) = 84), so the "cfg_total_size > md->cfg.size" guard added earlier does not fire (76 < 888). * mod_zalloc() is called with sizeof(*cd) + (size_t)-8 = 452 - 8 = 444 bytes, while struct copier_data needs 452. gtw_cfg.config_length sits at offset 444, exactly past the truncated allocation. copier_dai_create() then reads that field (and the config_data pointer right after it) out of bounds. size_t is 32-bit on native_sim and on the real Xtensa DSP targets, so this is a genuine firmware bug, not just a host-fuzzer artifact. Fix --- Validate config_length before any size arithmetic is performed on it. The blob is expressed in dwords and must fit within the received init payload, so an upper bound of md->cfg.size / sizeof(uint32_t) both is correct and keeps (config_length - 1) << 2 from overflowing (the mailbox payload is bounded by SOF_IPC_MSG_MAX_SIZE): if (copier->gtw_cfg.config_length > md->cfg.size / sizeof(uint32_t)) { comp_err(dev, "copier_init(): invalid gtw_cfg.config_length %u", copier->gtw_cfg.config_length); return -EINVAL; } The existing precise "cfg_total_size > md->cfg.size" check is retained as a secondary guard for the non-overflowing mismatch case. Verification ------------ - The crash input now executes cleanly: config_length is rejected with -EINVAL, exit code 0, no ASAN report. The new bound branch is confirmed hit (config_length 0xffffffff vs bound 222). - Full IPC4 corpus replay: 92,780 inputs, 0 crashes (-runs=0), coverage unchanged (cov 1331 / ft 5270), confirming the check does not over-restrict legitimate copier init payloads. Found-by: IPC4 libFuzzer (ASAN, native_sim) Signed-off-by: Tomasz Leman --- src/audio/copier/copier.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/audio/copier/copier.c b/src/audio/copier/copier.c index 3117ee5ea444..33c404a52404 100644 --- a/src/audio/copier/copier.c +++ b/src/audio/copier/copier.c @@ -143,6 +143,21 @@ __cold static int copier_init(struct processing_module *mod) assert_can_be_cold(); if (copier->gtw_cfg.config_length > 1) { + /* + * config_length is host-controlled and expressed in dwords. A + * huge value overflows the 32-bit size arithmetic below: the + * "<< 2" wraps around, which shrinks both cfg_total_size and the + * mod_zalloc() size to a small value and defeats the payload + * check further down. The gateway blob must fit within the + * received init payload, so reject anything larger than that + * before doing any size arithmetic on it. + */ + if (copier->gtw_cfg.config_length > md->cfg.size / sizeof(uint32_t)) { + comp_err(dev, "copier_init(): invalid gtw_cfg.config_length %u", + copier->gtw_cfg.config_length); + return -EINVAL; + } + /* one word already included in gateway_cfg struct hence subtraction */ gtw_cfg_var_size += (copier->gtw_cfg.config_length - 1) << 2; cfg_total_size += gtw_cfg_var_size;