iSCSI CHAP: Heap Buffer Overflow in the Linux Kernel

Lobsters Hottest News

Summary

A heap buffer overflow vulnerability in the Linux kernel's iSCSI target authentication code (BASE64 CHAP decoding) allows unauthenticated remote attackers to overflow fixed-size buffers, potentially leading to code execution. The vulnerability was introduced in a 2022 commit and affects systems with iSCSI target support.

<p><a href="https://lobste.rs/s/exezj2/iscsi_chap_heap_buffer_overflow_linux">Comments</a></p>
Original Article
View Cached Full Text

Cached at: 06/01/26, 08:35 PM

# iSCSI CHAP: Heap Buffer Overflow in the Linux Kernel Source: [https://ahossu.ro/blog/iscsi-chap-base64-overflow](https://ahossu.ro/blog/iscsi-chap-base64-overflow) ←[Back to blog](https://ahossu.ro/blog/index.html) On this page - [1\. Background](https://ahossu.ro/blog/iscsi-chap-base64-overflow#background) - [2\. The vulnerable code](https://ahossu.ro/blog/iscsi-chap-base64-overflow#vulnerable-code) - [3\. How far it overflows](https://ahossu.ro/blog/iscsi-chap-base64-overflow#math) - [4\. Reachability](https://ahossu.ro/blog/iscsi-chap-base64-overflow#reachability) - [5\. KASAN confirmation](https://ahossu.ro/blog/iscsi-chap-base64-overflow#kasan) - [6\. Exploitation primitive](https://ahossu.ro/blog/iscsi-chap-base64-overflow#exploitation) - [7\. The fix](https://ahossu.ro/blog/iscsi-chap-base64-overflow#fix) - [8\. Patch and other kernel work](https://ahossu.ro/blog/iscsi-chap-base64-overflow#patch) ## 1\. Background I was reading through the iSCSI target authentication code in`drivers/target/iscsi/iscsi\_target\_auth\.c`, looking at what an unauthenticated initiator can reach before credentials are validated\. The 2022 commit`1e5733883421`\("scsi: target: iscsi: Support base64 in CHAP"\) stood out — it extended the login path with a new decoding branch, so I read through`chap\_server\_compute\_hash\(\)`to see how the new BASE64 case compared to the existing HEX branch\. The difference was immediate\. The HEX branch validates length before touching the destination\. The BASE64 branch does not\. ## 2\. The vulnerable code Inside`chap\_server\_compute\_hash\(\)`, after extracting the client's CHAP response into`chap\_r`, the code branches on encoding type\. The HEX branch checks the length first: ``` case HEX: if (strlen(chap_r) != chap->digest_size * 2) { pr_err("Malformed CHAP_R\n"); goto out; } if (hex2bin(client_digest, chap_r, chap->digest_size) < 0) { pr_err("Malformed CHAP_R: invalid HEX\n"); goto out; } break; ``` The BASE64 branch, added in that 2022 commit, does not: ``` /* iscsi_target_auth.c:343 */ case BASE64: if (chap_base64_decode(client_digest, chap_r, strlen(chap_r)) != chap->digest_size) { pr_err("Malformed CHAP_R: invalid BASE64\n"); goto out; } break; ``` `client\_digest`is allocated at line 276 as`kzalloc\(chap\-\>digest\_size, GFP\_KERNEL\)`\. For SHA\-256 that is 32 bytes; for MD5 it is 16 bytes\.`chap\_base64\_decode\(\)`writes into that buffer character by character with no bounds check on the destination: ``` /* iscsi_target_auth.c:212 */ static int chap_base64_decode(u8 *dst, const char *src, size_t len) { int i, bits = 0, ac = 0; u8 *cp = dst; for (i = 0; i < len; i++) { ... if (bits >= 8) { *cp++ = (ac >> (bits - 8)) & 0xff; /* no bounds check */ ... } } return cp - dst; } ``` The length check at line 344 fires on the return value — after the write has already happened\. By the time the comparison`\!= chap\-\>digest\_size`is evaluated, the destination buffer has already been overflowed\. ## 3\. How far it overflows `MAX\_RESPONSE\_LENGTH`is 128\. The`extract\_param\(\)`call at line 326 strips the`"0b"`prefix from a BASE64\-encoded value and returns the rest in`chap\_r`, so up to 127 characters can reach`chap\_base64\_decode\(\)`\. Each base64 character contributes 6 bits\. A byte is written to the destination whenever the accumulator holds at least 8 bits, so 4 characters produce exactly 3 bytes\. For 127 characters: ``` 127 chars * 6 bits = 762 bits 762 / 8 = 95 bytes written ``` Against a SHA\-256 target \(`digest\_size = 32`\), the overflow is 63 bytes past the end of a`kmalloc\-32`object\. Against an MD5 target \(`digest\_size = 16`\), it is 79 bytes past the end of a`kmalloc\-16`object\. The HEX branch would have caught the same input immediately:`strlen\(chap\_r\) \!= digest\_size \* 2`rejects anything that is not exactly`2 \* digest\_size`characters\. The BASE64 branch has no equivalent guard\. ## 4\. Reachability The overflow is reachable before password validation completes\. Looking at the call order inside`chap\_server\_compute\_hash\(\)`: - Line 318: username check \-`strncmp\(chap\_n, auth\-\>userid, compare\_len\)` - Line 326:`extract\_param\(\)`reads`CHAP\_R`from the login PDU - Line 344:`chap\_base64\_decode\(\)`\- the overflow happens here - Line 402: password check \-`memcmp\(server\_digest, client\_digest, chap\-\>digest\_size\)` The username is checked first and must match a configured CHAP user\. That is the only gate before the overflow\. An attacker who knows the CHAP username on the target can send any value for`CHAP\_R`and trigger the write at line 344\. The password hash comparison at line 402 never executes \- the`goto out`at line 347 fires first, or the overflow corrupts memory before control reaches it\. iSCSI targets that are exposed on the network with CHAP enabled and a known or guessable username are directly reachable\. The overflow happens during the login phase, before a session is established\. ## 5\. KASAN confirmation I tested on linux\-next`next\-20260508`\(7\.1\.0\-rc2\) with a minimal LIO target configured via configfs and a Python script that goes through the full iSCSI login sequence:`SecurityNegotiation`to negotiate`CHAP\_A=7`, then an authentication PDU with`CHAP\_R = "0b" \+ "A" \* 127`\. ``` BUG: KASAN: slab-out-of-bounds in chap_base64_decode+0x18c/0x1a0 Write of size 1 at addr ffff8880099b83e0 by task kworker/1:2/60 CPU: 1 PID: 60 Comm: kworker/1:2 Not tainted 7.1.0-rc2-next-20260508 Call Trace: chap_base64_decode+0x18c/0x1a0 chap_server_compute_hash+0x4c1/0x8e0 chap_main_loop+0x2b3/0x520 iscsi_target_do_login+0x1f3/0x420 iscsi_target_do_login_rx+0x89/0x100 ``` The target responds with a 0x2/0x1 \(authentication failure\) PDU\. The write has already happened at that point\. I also wrote a userspace reproducer \(`vuln\_sim\.c`\) that replicates the allocation and the decode call in isolation\. Compiled with`\-fsanitize=address`it reports a heap\-buffer\-overflow write of size 1, 0 bytes past a 16\-byte region, which is the MD5 case: ``` ==ASAN: heap-buffer-overflow WRITE of size 1 #0 chap_base64_decode vuln_sim.c:36 #1 main vuln_sim.c:49 allocated 16-byte region here: #0 calloc #1 main vuln_sim.c:44 ``` ## 6\. Exploitation primitive The overflow writes 95 bytes starting at`client\_digest`, which comes from`kmalloc\-16`or`kmalloc\-32`depending on the hash algorithm\. Anything allocated from the same slab cache that lands adjacent in memory gets overwritten\. Another`iscsi\_chap`structure from a concurrent connection is the natural spray target — multiple login attempts from separate connections can be used to position allocations\. To demonstrate the exploitation primitive I wrote`rce\_demo\.c`, a userspace program that places a`client\_digest`\-sized buffer and a victim struct containing a function pointer in contiguous memory \(mimicking the kernel slab layout\), then triggers the overflow with a crafted base64 payload that encodes the address of a target function at the right offset\. The corrupted pointer is called after the overflow: ``` fn_ptr corrupted, calling 0x55a1b2c3d4e0 executing shell command... uid=1000(alex) hostname 6.19.11-arch1-1 ``` The allocation in`rce\_demo\.c`is a single contiguous`calloc\(\)`covering the digest buffer, the victim struct, and enough extra space to absorb the full 95\-byte write without hitting glibc chunk metadata\. Two separate allocations do not work — the chunk header between them gets corrupted at the 17th byte of overflow\. ## 7\. The fix The HEX branch already shows the right approach: check the length before calling the decoder\. The maximum number of base64 data characters that can decode to exactly`digest\_size`bytes is`DIV\_ROUND\_UP\(digest\_size \* 4, 3\)`, matching the convention used by`BASE64\_CHARS\(\)`in`include/linux/base64\.h`\. Trailing`'='`padding characters are stripped before the comparison so that both padded and unpadded encodings are handled correctly\.`chap\_base64\_decode\(\)`already returns early on`'='`, so the full original string is still passed to the decoder unchanged\. ``` - case BASE64: + case BASE64: { + size_t r_len = strlen(chap_r); + + while (r_len > 0 && chap_r[r_len - 1] == '=') + r_len--; + if (r_len > DIV_ROUND_UP(chap->digest_size * 4, 3)) { + pr_err("Malformed CHAP_R: base64 payload too long\n"); + goto out; + } if (chap_base64_decode(client_digest, chap_r, strlen(chap_r)) != chap->digest_size) { pr_err("Malformed CHAP_R: invalid BASE64\n"); goto out; } break; + } ``` For SHA\-256 \(`digest\_size = 32`\) the data\-character limit is 43 — a padded encoding adds one trailing`'='`\(44 chars total\) which is stripped before the check\. For MD5 \(`digest\_size = 16`\) the limit is 22 data characters, with up to two trailing`'='`stripped\. Any attacker\-supplied input whose data length exceeds the limit is rejected before the decoder is called\. All actively supported trees are affected: linux\-next, 6\.14, 6\.12, 6\.6, 6\.1\. The BASE64 branch was introduced in 2022 and has been present in every kernel release since\. The patch was submitted with a`Cc: stable@vger\.kernel\.org`tag to cover the LTS trees\. ## 8\. Patch and other kernel work The fix was sent as`\[PATCH\] scsi: target: iscsi: validate CHAP\_R length before base64 decode`to Martin K\. Petersen, with copies to Bart Van Assche,`target\-devel@vger\.kernel\.org`,`linux\-scsi@vger\.kernel\.org`, and`stable@vger\.kernel\.org`\. The commit includes a`Fixes: 1e5733883421`tag pointing back to the commit that introduced the BASE64 branch\. Following review from David Disseldorp \(SUSE\), a v2 was submitted replacing the inline ceiling formula with`DIV\_ROUND\_UP\(chap\-\>digest\_size \* 4, 3\)`to match the kernel base64 convention\. Maurizio Lombardi then pointed out that the check would incorrectly reject legitimately padded responses — for SHA\-256, a padded encoding is 44 characters but`DIV\_ROUND\_UP\(32 \* 4, 3\) = 43`\. A v3 was submitted that strips trailing`'='`characters before the comparison, so both padded and unpadded encodings pass the check correctly\. David Disseldorp requested one more change in v4: a comment at the mutual CHAP call site explaining why no equivalent overflow check is needed there \(`initiatorchg\_binhex`is`CHAP\_CHALLENGE\_STR\_LEN`bytes and`extract\_param\(\)`caps the input to`CHAP\_CHALLENGE\_STR\_LEN`characters, so the decoded output is bounded\)\. David Disseldorp gave`Reviewed\-by`on v4, and Martin K\. Petersen applied it to`7\.1/scsi\-fixes`\([85db7391310b](https://git.kernel.org/mkp/scsi/c/85db7391310b)\)\. Other recent kernel work: three patch series for the rtl8723bs staging WiFi driver covering ten functions \(`OnAuthClient\(\)`,`OnAuth\(\)`,`HT\_caps\_handler\(\)`,`OnAssocRsp\(\)`,`update\_beacon\_info\(\)`,`bwmode\_update\_check\(\)`,`issue\_assocreq\(\)`,`join\_cmd\_hdl\(\)`,`rtw\_get\_wps\_ie\(\)`,`rtw\_cfg80211\_set\_wpa\_ie\(\)`\), documented separately on this blog\. Before that, patches to the staging tree for NULL pointer dereferences in tegra\-video and max96712, a use\-after\-free and teardown ordering issue in nvec, and a double\-free in the ipu7 camera driver\.

Similar Articles

I found a KVM guest-to-host heap corruption bug and someone else got there first

Lobsters Hottest

Blog post detailing the discovery of CVE-2026-53360, a heap out-of-bounds read/write in KVM's SEV-SNP Page State Change handler that lets a malicious guest corrupt host kernel memory. The author discusses the bug, his incorrect fix, the better fix from a duplicate reporter, and provides a CTF challenge.

CVE-2026-31431: Copy Fail

Lobsters Hottest

CVE-2026-31431 (Copy Fail) is a local privilege escalation vulnerability in the Linux kernel affecting all major distributions since 2017, allowing unprivileged users to gain root shell access through a deterministic 4-byte write to any readable file's page cache via the AF_ALG crypto subsystem.

A Linux Kernel 0-day Journey - From a limited UAF to Physical Memory R/W

Lobsters Hottest

This article details the discovery and exploitation of a Linux kernel 0-day vulnerability in the network scheduler subsystem (red scheduler), turning a limited slab use-after-free into full physical memory read/write, ultimately achieving privilege escalation to root. The vulnerability existed for 2.5 years and was fixed in June 2026.