Bug 2474403 (CVE-2026-110036) - CVE-2026-110036 device-mapper-persistent-data: device-mapper-persistent-data: Denial of Service via crafted block indices
Summary: CVE-2026-110036 device-mapper-persistent-data: device-mapper-persistent-data:...
Keywords:
Status: NEW
Alias: CVE-2026-110036
Product: Security Response
Classification: Other
Component: vulnerability
Version: unspecified
Hardware: All
OS: Linux
low
low
Target Milestone: ---
Assignee: Product Security
QA Contact:
URL:
Whiteboard:
Depends On: 2550678
Blocks:
TreeView+ depends on / blocked
 
Reported: 2026-05-11 21:52 UTC by OSIDB Bzimport
Modified: 2026-10-13 16:41 UTC (History)
18 users (show)

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


Attachments (Terms of Use)

Description OSIDB Bzimport 2026-05-11 21:52:14 UTC
AI_ONLY_REPORT
package: device-mapper-persistent-data-1.3.0-1.el10
------
Summary: Reachable DoS in sync read path from `u64::MAX` block index  
(`find_runs` overflow -> invalid run -> panic): a crafted metadata-derived  
block index can terminate affected tooling by reaching an empty vectored  
read in the synchronous batching path.
Requirements to exploit: An attacker needs to supply, or induce a user to  
process, crafted metadata or an equivalent block list containing `u64::MAX`  
so that the package reaches the synchronous `read_many_` batching path.  
This is a local, input-driven denial of service; no privilege escalation,  
confidentiality impact, or code execution is demonstrated.
Component affected: `device-mapper-persistent-data`, specifically the  
synchronous read path through `src/io_engine/gaps.rs` (`find_runs`),  
`src/io_engine/sync.rs` (`SyncIoEngine::read_many_`), and  
`src/io_engine/utils.rs` (`VectoredBlockIo::read_blocks`).
Version affected: `device-mapper-persistent-data-1.3.0-1.el10`
Patch available: no released package fix established; proposed patch  
included below
Version fixed: unknown
Upstream coordination: Not notified.
CVSS: CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H - 5.5 (MEDIUM)
AV:L - the attacker needs local access to provide crafted metadata or a  
malicious image to the affected tooling.
AC:L - the trigger condition is a single malformed block index reaching  
an otherwise normal batching path.
PR:N - no privileges in the vulnerable component are required beyond  
supplying the crafted input.
UI:R - a user or automation must invoke an affected tool on the crafted  
input.
S:U - the impact is limited to the vulnerable process.
C:N - no confidentiality impact is demonstrated.
I:N - no integrity impact is demonstrated.
A:H - the process can panic and terminate, preventing the requested  
operation from completing.
Impact: Moderate. Under Red Hat's severity guidance, this is a real  
availability issue in a reachable code path, but it does not rise to  
Important or Critical because the demonstrated attack is local and  
input-driven rather than a remote service compromise or system compromise.  
It is more than Low because successful exploitation directly terminates the  
affected process once the vulnerable path is reached.
Embargo: no
Reason: this is a local, crafted-input denial of service with  
straightforward mitigation and no demonstrated confidentiality, integrity,  
privilege, or code-execution impact.
Acknowledgement: Aisle Research
Vulnerability Details: The synchronous batched-read path constructs  
end-exclusive runs with unchecked `+ 1` arithmetic in  
`src/io_engine/gaps.rs`:
```rust
for b in blocks {
if let Some((begin, end)) = last {
#[allow(clippy::comparison_chain)]
if *b > end {
let len = b - end;
if len > gap_threshold {
runs.push(Run(begin, end));
last = Some((*b, *b + 1));
} else {
runs.push(Run(begin, end));
runs.push(Gap(end, *b));
last = Some((*b, *b + 1));
}
} else if *b == end {
last = Some((begin, b + 1));
} else {
runs.push(Run(begin, end));
last = Some((*b, b + 1));
}
} else {
last = Some((*b, b + 1));
}
}
```
When `b == u64::MAX`, release builds can wrap this into an invalid  
end-exclusive range such as `Run(u64::MAX, 0)`. In  
`SyncIoEngine::read_many_`, `RunOp::Run(b, e)` populates buffers by  
iterating `for b in *b..*e`; for `Run(u64::MAX, 0)`, that range is empty,  
so no buffers are pushed even though the batch start is still tracked from  
the run start. The later vectored read sink in `src/io_engine/utils.rs`  
then dereferences the first buffer unconditionally:
```rust
fn read_blocks(&self, buffers: &mut [&mut [u8]], mut pos: u64) ->  
Result<Vec<Result<()>>> {
let block_size = buffers[0].len();
```
This produces a process-terminating panic. The available evidence supports  
reachability through real metadata-derived block lists passed into  
`read_many`, although not every caller or I/O engine is shown to behave  
identically. This report establishes the synchronous path. In non-release  
builds, the same input may instead panic earlier on integer overflow, but  
the availability impact remains a crash.
Steps to reproduce:
1. Add the following regression test to `src/io_engine/sync/tests.rs`:
```rust
#[test]
#[should_panic]
fn panic_on_max_block_index() {
let mut v = MockVio::new();
let _ = SyncIoEngine::read_many_(v.into(), &[u64::MAX]).unwrap();
}
```
2. Run:
```bash
cargo test --release panic_on_max_block_index – --nocapture
```
3. Observe a panic originating from `src/io_engine/utils.rs` when  
`VectoredBlockIo::read_blocks` indexes `buffers[0]`.
Without `--release`, the same input may panic earlier on the unchecked  
overflow in `find_runs`, which still demonstrates a denial of service.
Mitigation: Until a fix is applied, avoid processing untrusted metadata  
with affected builds, and reject block indices equal to `u64::MAX` or  
otherwise outside the valid device block range before they reach  
`generate_runs` or `read_many_`. Caller-side validation is a useful  
short-term defense, but it should not replace fixing the underlying  
arithmetic and empty-buffer handling.
Proposed Fix: A minimal fix is to use checked arithmetic when constructing  
end-exclusive runs and to return early from empty vectored reads.
```diff
diff --git a/src/io_engine/gaps.rs b/src/io_engine/gaps.rs
@@
fn find_runs(blocks: &[u64], gap_threshold: u64) -> Vec<RunOp> {
@@
for b in blocks {
+        let next = match b.checked_add(1) {
+            Some => n,
+            None => {
+                if let Some((begin, end)) = last.take() {
+                    runs.push(Run(begin, end));
+                }
+                continue;
+            }
+        };
if let Some((begin, end)) = last {
@@
                   last = Some((*b, *b + 1));
+                    last = Some((*b, next));
@@

                   last = Some((*b, *b + 1));
+                    last = Some((*b, next));
@@

               last = Some((begin, b + 1));
+                last = Some((begin, next));
@@

               last = Some((*b, b + 1));
+                last = Some((*b, next));
              }
          } else {

           last = Some((*b, b + 1));
+            last = Some((*b, next));
          }
      }
diff --git a/src/io_engine/utils.rs b/src/io_engine/utils.rs
@@
      fn read_blocks(&self, buffers: &mut [&mut [u8]], mut pos: u64) ->  
Result<Vec<Result<()>>> {
+        if buffers.is_empty() {
+            return Ok(Vec::new());
+        }
          let block_size = buffers[0].len();
```


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