Bug 2474403 (CVE-2026-110036)

Summary: CVE-2026-110036 device-mapper-persistent-data: device-mapper-persistent-data: Denial of Service via crafted block indices
Product: [Other] Security Response Reporter: OSIDB Bzimport <bzimport>
Component: vulnerabilityAssignee: Product Security <prodsec-ir-bot>
Status: NEW --- QA Contact:
Severity: low Docs Contact:
Priority: low    
Version: unspecifiedCC: akhatavk, aos-team-art-private, asdas, dpaolell, jdelft, jupierce, lgarciaa, mbiarnes, ppalepu, ppostler, prdhamdh, rhel-process-autobot, security-response-team, sghai, sidsharm, suppawar, vlaad, watson-tool-maintainers
Target Milestone: ---Keywords: Security
Target Release: ---   
Hardware: All   
OS: Linux   
Whiteboard:
Fixed In Version: Doc Type: ---
Doc Text:
A flaw was found in device-mapper-persistent-data. A local attacker can cause a Denial of Service (DoS) by inducing a user or system tool to process crafted device metadata containing an excessively large block index. An arithmetic error in the synchronous block reading logic causes the application to panic and unexpectedly terminate.
Story Points: ---
Clone Of: Environment:
Last Closed: Type: ---
Regression: --- Mount Type: ---
Documentation: --- CRM:
Verified Versions: Category: ---
oVirt Team: --- RHEL 7.3 requirements from Atomic Host:
Cloudforms Team: --- Target Upstream Version:
Embargoed:
Bug Depends On: 2550678    
Bug Blocks:    

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