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\.