From nobody Mon Jun 15 18:03:32 2026 Received: from mail-oi1-f177.google.com (mail-oi1-f177.google.com [209.85.167.177]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 42CFF17DFE7 for ; Mon, 13 Apr 2026 01:08:33 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.167.177 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1776042515; cv=none; b=oilv/1cp5a8qdU7I7jhIWQY88ENT7lCiecBClcbnCY4sT6iXlxQK8ZDTLnjT+nZ/sxATiT5ilrKIuF2BS8IRuIWWFCYc+d2tGAXgoNp6I/oBOYLlgeyi8sMUu1/7AfQldwqPLINBITsWtzgEpsAjUjo3wghJuk23798zp2jaHts= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1776042515; c=relaxed/simple; bh=O8sZ1M5cVLmT2YEIzM72jussPXtRqxJFuivlzSLELmk=; h=From:To:Cc:Subject:Date:Message-ID:MIME-Version; b=fvyHwrA6wXfNAZukENSWyuKbSszc9+Pi2YWfx/6V38J0fG4cJRKgC9kVUcmcD9scYirc3fRQWlUpR/+K4ypOUqO3YUSaFC/u3SAFf8YG3fQ8t4gyh9KzcYhUD6dMJisVOivSdgohL02WHO5nunAKGXEu8kJoWnMihTKXV0+Wrao= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=neuling.org; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=TKDFpE32; arc=none smtp.client-ip=209.85.167.177 Authentication-Results: smtp.subspace.kernel.org; dmarc=none (p=none dis=none) header.from=neuling.org Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="TKDFpE32" Received: by mail-oi1-f177.google.com with SMTP id 5614622812f47-47018d341f8so2484912b6e.3 for ; Sun, 12 Apr 2026 18:08:33 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1776042512; x=1776647312; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:sender:from:to:cc:subject:date:message-id:reply-to; bh=bx/cx+/nSyPU4u4kSElxKqXthbDFkqX08J/TpBLSL1A=; b=TKDFpE32wRsO+sZsk8PnYl8CqLb5l4dfDGf7nFwfOLxJFyuLPKCEyOaW23x/KuZ9Zw 2DeRaXk9JH797g+Rw7vFCICy70jn+AZ2bRQ5A/Bs0+RXgeZljSL2B1A1nuMdA2o3HleS XBJGgBEtbN0HG5KNWC7yQPT8P6hGx1dUNMSS1umcBGMlwoVtHoqk3jk4A6F+PpsphKJw 7g39ut/oDBCoTvioA/tvlfS/IBq1WgHwdQsqsWE0H3zmovz2xg6SC7DSdEwdYD5BAg/h +rPmeb4S8AREhnfH8qiX7xywlf/odUtMnx/ve7zWSNFSilnuMDzqXH2cqf8dLz9Zl/TN Iowg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1776042512; x=1776647312; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:sender:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=bx/cx+/nSyPU4u4kSElxKqXthbDFkqX08J/TpBLSL1A=; b=doskmriqoIKpnQzoe5lrbIiMu4OdFJxeUhwS8XyRRnzbkFc41GZIMX9Cw5dYVKVv1S ZYwk3/QM75N5yB3doaYCobfV5yhHVkHdBt2TkUQqRdwUzyXu8meLWh0cQjLNAXdPPby/ auNv0oTw6XcjgK28LColfLf85c+ASqmRHVZ8l2jg4gpfnRbY4dnSHBesy3QYCfb80Dia wnfkG3PMR72aUAE8f1iJ18YuCMPkH9dT/2T2YTe1Sdly+FDA0FGpUvrZBK2VlImbQjpo Pthv5QqWTq/ebuhjsTANha1CYCXkU8EMRAsGDLfaxNleO3i3Dw/gJ33QXZGcDLWvBdoR XOYQ== X-Forwarded-Encrypted: i=1; AJvYcCVlpaF+DeOSeNsPpXuvPVtjn2Br3/GXfW123znUq8fjG+USf110JwzJtqOZHeiO4Is884yjy5cuIpoIWXI=@vger.kernel.org X-Gm-Message-State: AOJu0YxM+cmSZiYoOd6VP97KaTxkMEQgw5yri0+abq7x2a9JKMRgg0yF i7+Hz7/Geu/LP5OzWGl0yGsS7CxfLvf7qAfJOXXxWyTgo8sdx4/KVTIQOF/O/kvOfQs= X-Gm-Gg: AeBDies8/DQs87KxvLfdhNCcnTnQgVfXyVGRkDfL+dJFab7KHIyRL1ajYvYE/MAz14o YRJ7Wq5fwoXSNZrQ9ef1+2foVIESjw+ov7ieeaW9vbf/gz1PVFakZiVF4lrqINSYDeGyZ5XCm69 3TcR7kpHVkohlXr4IxA12zG7qbPVqELpHU+0sL0Ro0tIrz8iTj7W20PoWS2c24y4HwHQLGzappl KiTbKkepUm8Uo07obNUh8xQElUkawAozjcPdCRhu7yS57EM0Ai/BCn8ETNoGWsosR3Fu3B2Jdvg BYZbWZHPsxO+2A6fToeHFILA0yAVBrJ/uLg7V7jeH9kp1msFbpB2ev5rfQutWQWBCxUDao8ewKn Ejisc+Hfy1H7Uyk7g5slG9F3x3JqPZzcj08XGqCcqzTDSZXdlUOHoXYXhE630MeKSaNMEgBpD8Q X3fgSx6xqO8L4vMjAR/2Q8WUyhsCKbiGfb8UQMKSIXabEX X-Received: by 2002:a05:6808:4f0e:b0:467:1212:46fd with SMTP id 5614622812f47-4789ff0e572mr5642029b6e.33.1776042512098; Sun, 12 Apr 2026 18:08:32 -0700 (PDT) Received: from ird-aus2.tenstorrent.com ([38.104.49.66]) by smtp.gmail.com with ESMTPSA id 5614622812f47-478a2f557b5sm5556350b6e.11.2026.04.12.18.08.30 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sun, 12 Apr 2026 18:08:31 -0700 (PDT) Sender: Michael Neuling From: Michael Neuling To: jiangfeng@kylinos.cn, pjw@kernel.org, palmer@dabbelt.com, aou@eecs.berkeley.edu, alex@ghiti.fr Cc: linux-riscv@lists.infradead.org, linux-kernel@vger.kernel.org, Michael Neuling Subject: [PATCH] riscv: lib: Fix ZBB strnlen reading past count boundary Date: Mon, 13 Apr 2026 01:07:38 +0000 Message-ID: <20260413010738.1622423-1-mikey@neuling.org> X-Mailer: git-send-email 2.43.0 Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" The ZBB-optimized strnlen loop loads one word ahead before checking the aligned boundary: REG_L t1, SZREG(t0) // load next word addi t0, t0, SZREG // advance orc.b t1, t1 bgeu t0, t4, 4f // boundary check AFTER load where t4 =3D (s + count) & -SZREG. When s is aligned and count is a multiple of SZREG, t4 equals s + count and the loop loads a full word starting at exactly s + count. If s + count falls on a page boundary with the next page unmapped, this faults. Fix by computing the aligned boundary from the last valid byte (s + count - 1) instead of s + count. This makes the loop stop at the word containing the last valid byte rather than potentially loading the word after it. The count =3D=3D 0 case is already handled by the beqz early exit. Also add a pre-loop guard (bgeu t0, t4) for the case where all valid bytes fit within the first word. With the adjusted boundary, t4 can equal t0, and entering the loop with stale register state from the first-word processing would produce incorrect results. The final minu clamp ensures the result is still correct when the last loaded word extends past s + count - 1 within the same aligned word. Fixes: 5ba15d419fab ("riscv: lib: add strnlen() implementation") Signed-off-by: Michael Neuling Assisted-by: Claude Opus4.6 High Thinking --- Here is a test case that demonstrates the bug. The kernel code is pulled ou= t=20 into a standalone file for testing. % cat test-strnlen-single.c // SPDX-License-Identifier: GPL-2.0 /* * Minimal test: one strnlen call that triggers the ZBB over-read. * Run under GDB to single-step the fault. */ #include #include #include #include #include extern size_t kernel_strnlen(const char *s, size_t count); int main(void) { size_t page_size =3D sysconf(_SC_PAGESIZE); char *region, *start; size_t count =3D 16; /* multiple of SZREG=3D8, triggers the bug */ size_t ret; /* Map one page, guard page after it is unmapped */ region =3D mmap(NULL, 2 * page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); munmap(region + page_size, page_size); /* Fill with non-NUL, no terminator within count */ memset(region, 'A', page_size); /* Aligned start, count reaches exactly to page end */ start =3D region + page_size - count; printf("page=3D%p start=3D%p count=3D%zu end=3D%p (page_end=3D%p)\n", region, start, count, start + count, region + page_size); printf("Calling kernel_strnlen...\n"); /* This will fault on the buggy ZBB path */ ret =3D kernel_strnlen(start, count); printf("Result: %zu (expected %zu)\n", ret, count); munmap(region, page_size); return 0; } % cat test-strnlen-zbb.S /* SPDX-License-Identifier: GPL-2.0-only */ /* * Standalone copy of the kernel's ZBB strnlen for userspace testing. * Extracted from arch/riscv/lib/strnlen.S */ #define SZREG 8 #define REG_L ld .text .global kernel_strnlen .type kernel_strnlen, @function kernel_strnlen: /* Jump straight to ZBB path (we know we have it) */ j strnlen_zbb /* * Non-ZBB fallback (byte-at-a-time) */ addi t1, a0, -1 add t2, a0, a1 1: addi t1, t1, 1 beq t1, t2, 2f lbu t0, 0(t1) bnez t0, 1b 2: sub a0, t1, a0 ret strnlen_zbb: # define CZ ctz # define SHIFT srl .option push .option arch,+zbb /* If maxlen is 0, return 0. */ beqz a1, 3f /* Number of irrelevant bytes in the first word. */ andi t2, a0, SZREG-1 /* Align pointer. */ andi t0, a0, -SZREG li t3, SZREG sub t3, t3, t2 slli t2, t2, 3 /* Aligned boundary. */ add t4, a0, a1 andi t4, t4, -SZREG /* Get the first word. */ ld t1, 0(t0) /* * Shift away the partial data we loaded to remove the irrelevant bytes * preceding the string with the effect of adding NUL bytes at the * end of the string's first word. */ srl t1, t1, t2 /* Convert non-NUL into 0xff and NUL into 0x00. */ orc.b t1, t1 /* Convert non-NUL into 0x00 and NUL into 0xff. */ not t1, t1 /* * Search for the first set bit (corresponding to a NUL byte in the * original chunk). */ ctz t1, t1 /* * The first chunk is special: compare against the number * of valid bytes in this chunk. */ srli a0, t1, 3 /* Limit the result by maxlen. */ minu a0, a0, a1 bgtu t3, a0, 2f /* Prepare for the word comparison loop. */ addi t2, t0, SZREG li t3, -1 /* * Our critical loop is 4 instructions and processes data in * 4 byte or 8 byte chunks. */ .p2align 3 1: ld t1, SZREG(t0) addi t0, t0, SZREG orc.b t1, t1 bgeu t0, t4, 4f beq t1, t3, 1b 4: not t1, t1 ctz t1, t1 srli t1, t1, 3 /* Get number of processed bytes. */ sub t2, t0, t2 /* Add number of characters in the first word. */ add a0, a0, t2 /* Add number of characters in the last word. */ add a0, a0, t1 /* Ensure the final result does not exceed maxlen. */ minu a0, a0, a1 2: ret 3: mv a0, a1 ret .option pop .size kernel_strnlen, .-kernel_strnlen % riscv64-linux-gnu-gcc -march=3Drv64gc_zbb -O0 -g -static -o test-strnlen-= single-rv64 test-strnlen-single.c test-strnlen-zbb.S % qemu-riscv64 -cpu rv64,zbb=3Dtrue ./test-strnlen-single-rv64 page=3D0x7ff6d698c000 start=3D0x7ff6d698cff0 count=3D16 end=3D0x7ff6d698d00= 0 (page_end=3D0x7ff6d698d000) Calling kernel_strnlen... Segmentation fault (core dumped) % arch/riscv/lib/strnlen.S | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/arch/riscv/lib/strnlen.S b/arch/riscv/lib/strnlen.S index 53afa7b5b3..a8911605c2 100644 --- a/arch/riscv/lib/strnlen.S +++ b/arch/riscv/lib/strnlen.S @@ -83,8 +83,13 @@ strnlen_zbb: sub t3, t3, t2 slli t2, t2, 3 =20 - /* Aligned boundary. */ + /* + * Aligned boundary. Use the address of the last valid byte + * (s + count - 1) to avoid loading a word past the count + * boundary in the loop below. count =3D=3D 0 is handled above. + */ add t4, a0, a1 + addi t4, t4, -1 andi t4, t4, -SZREG =20 /* Get the first word. */ @@ -120,6 +125,9 @@ strnlen_zbb: =20 bgtu t3, a0, 2f =20 + /* All remaining bytes are in the first word, no loop needed. */ + bgeu t0, t4, 2f + /* Prepare for the word comparison loop. */ addi t2, t0, SZREG li t3, -1 --=20 2.43.0