Bug 2462114 (CVE-2026-71226) - CVE-2026-71226 libkcapi: Memory corruption via uncanceled AIO requests on error in libkcapi's one-shot AIO path
Summary: CVE-2026-71226 libkcapi: Memory corruption via uncanceled AIO requests on err...
Keywords:
Status: NEW
Alias: CVE-2026-71226
Product: Security Response
Classification: Other
Component: vulnerability
Version: unspecified
Hardware: All
OS: Linux
medium
medium
Target Milestone: ---
Assignee: Product Security DevOps Team
QA Contact:
URL:
Whiteboard:
Depends On: 2511484
Blocks:
TreeView+ depends on / blocked
 
Reported: 2026-04-26 18:17 UTC by OSIDB Bzimport
Modified: 2026-08-05 12:24 UTC (History)
3 users (show)

Fixed In Version:
Clone Of:
Environment:
Last Closed:
Embargoed:


Attachments (Terms of Use)

Description OSIDB Bzimport 2026-04-26 18:17:33 UTC
AI_ONLY_REPORT
package: libkcapi-1.5.0-3.el10
------
Summary: Memory Corruption via Uncanceled AIO Requests on Error: libkcapi's  
one-shot AIO path can return an error before all submitted IOCBs are  
drained, allowing later kernel writes into caller-owned output buffers.
Requirements to exploit: A consumer must use the one-shot AIO interfaces  
with `KCAPI_INIT_AIO` so the real AIO backend is active, hit an error after  
one or more IOCBs have been submitted or completed, and then free or reuse  
the referenced `outiov` buffers before all kernel completions finish.
Component affected: `libkcapi-1.5.0-3.el10`, `lib/kcapi-kernel-if.c`,  
`_kcapi_aio_read_all()`, `_kcapi_aio_read_iov()`,  
`_kcapi_cipher_crypt_aio()` in the one-shot AIO path
Version affected: `libkcapi-1.5.0-3.el10` when the handle is initialized  
with `KCAPI_INIT_AIO` and libkcapi enables its real kernel AIO backend
Patch available: no released package fix established; proposed patch  
included below
Version fixed: unknown
Upstream coordination: Unknown.
CVSS: CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:H - 6.8 (MEDIUM)
AV:L - exploitation is local to the process using libkcapi.
AC:L - once the real AIO path is active, the unsafe behavior is reached  
directly through documented error paths after request submission.
PR:N - no elevated privileges are required beyond the ability to  
exercise the affected API in the local process.
UI:N - no separate user interaction is required.
S:U - the impact stays within the security scope of the calling process.
C:L - late writes into reused buffers can expose limited process data.
I:L - late kernel writes can alter caller memory after the library has  
reported failure.
A:H - heap corruption or process termination is a realistic outcome.
Impact: Moderate. This flaw can cause real process memory corruption, but  
it depends on an opt-in AIO configuration, kernel AIO availability, and an  
error path after submission rather than a default or easily reachable  
execution path. Under Red Hat's severity guidance, that is more consistent  
with Moderate impact than Important or Critical.
Embargo: no
Reason: the issue is local and configuration-dependent, requires  
explicit AIO use plus an error-path trigger, and does not present a  
straightforward remote compromise scenario.
Acknowledgement: Aisle Research
Vulnerability Details: In the affected one-shot AIO path, libkcapi can  
return failure before all submitted kernel requests are drained.  
`_kcapi_aio_read_all()` returns immediately on the first negative  
completion result, and `_kcapi_aio_read_iov()` returns immediately on a  
short `io_submit()` result even if some IOCBs were already submitted.  
`_kcapi_cipher_crypt_aio()` then propagates that error directly to the  
caller. If the caller treats the operation as failed and promptly frees or  
reuses `outiov[*].iov_base`, later kernel completions may still write into  
those buffers.
```c
if (events[i].res < 0) {
handle->aio.iocb_ret[events[i].data] =
events[i].res;
return (int)events[i].res;
}
...
ret = io_submit(handle->aio.aio_ctx, (long)iovlen, handle->aio.ciopp);
if ((uint32_t)ret != iovlen) {
if (ret < 0) {
ret = -errno;
kcapi_dolog(KCAPI_LOG_ERR, "io_read Error: %d\n", ret);
return ret;
} else {
kcapi_dolog(KCAPI_LOG_ERR,
"Could not sumbit AIO read\n");
return -EIO;
}
}
```
```c
ret = _kcapi_aio_read_iov(handle, outiov, process);
if (ret < 0)
return ret;
```
The issue is not established for the default synchronous path. Reachability  
depends on the caller enabling `KCAPI_INIT_AIO` so libkcapi uses the real  
kernel AIO backend.
Steps to reproduce:
1. Build an ASAN-enabled harness that calls `kcapi_cipher_encrypt_aio()` or  
`kcapi_aead_decrypt_aio()` with multiple output `iovec` entries.
2. Initialize the handle with `KCAPI_INIT_AIO` so the real AIO path is  
active.
3. Force a deterministic short-submit path with an `LD_PRELOAD` wrapper  
around `syscall(__NR_io_submit, ...)` that returns `0 < ret < iovlen` after  
forwarding only the first `N` IOCBs.
4. Optionally fault-inject `io_getevents()` so one returned event has `res  
< 0` before all completions are consumed.
5. When the API returns an error, immediately free or reallocate the  
`outiov[*].iov_base` buffers.
6. Observe delayed writes into freed or reused memory, for example as an  
ASAN use-after-free, heap corruption, or a canary mismatch.
Mitigation: Until a fix is available, avoid initializing affected handles  
with `KCAPI_INIT_AIO` and prefer the synchronous interfaces. If the real  
AIO path must remain enabled, applications should not immediately free or  
reuse `outiov` buffers after an error return.
Proposed Fix: Drain all submitted requests before returning an error. A  
minimal approach is to record the first completion error, continue draining  
outstanding completions, and drain any successfully submitted subset after  
a short `io_submit()` result.
```diff
diff --git a/lib/kcapi-kernel-if.c b/lib/kcapi-kernel-if.c
— a/lib/kcapi-kernel-if.c
+++ b/lib/kcapi-kernel-if.c
@@ -420,6 +420,7 @@ int _kcapi_aio_read_all(struct kcapi_handle *handle,  
size_t toread,
struct timespec *timeout)
{
+	int first_err = 0;
if (toread > KCAPI_AIO_CONCURRENT)
return -EINVAL;
@@ -440,8 +441,10 @@ int _kcapi_aio_read_all(struct kcapi_handle *handle,  
size_t toread,
if (events[i].res < 0) {
handle->aio.iocb_ret[events[i].data] =
events[i].res;
-				return (int)events[i].res;
+				if (!first_err)
+					first_err = (int)events[i].res;
+				continue;
}
@@ -467,7 +470,7 @@ int _kcapi_aio_read_all(struct kcapi_handle *handle,  
size_t toread,
toread -= (uint32_t)rc;
}
-	return 0;
+	return first_err;
}
@@ -534,15 +537,20 @@ int _kcapi_aio_read_iov(struct kcapi_handle *handle,
ret = io_submit(handle->aio.aio_ctx, (long)iovlen, handle->aio.ciopp);
if ((uint32_t)ret != iovlen) {
+		size_t submitted = (ret > 0) ? (size_t)ret : 0;
+		int submit_err = (ret < 0) ? -errno : -EIO;
if (ret < 0) {
-			ret = -errno;
kcapi_dolog(KCAPI_LOG_ERR, "io_read Error: %d\n", ret);
-			return ret;
} else {
kcapi_dolog(KCAPI_LOG_ERR,
"Could not sumbit AIO read\n");
-			return -EIO;
}
+		if (submitted) {
+			int drain = _kcapi_aio_read_all(handle, submitted, NULL);
+			if (submit_err == 0 && drain < 0)
+				submit_err = drain;
+		}
+		return submit_err;
}
```
(Equivalent fix using `io_cancel` + confirmed drain is also acceptable.)
------
This report was generated using AI technology. Always review AI-generated  
content prior to use


Note You need to log in before you can comment on or make changes to this bug.