{"record":{"id":"63269ca94e2094fa","repo":"firecracker-microvm/firecracker","slug":"discard-offset-overflow","errorCode":null,"errorMessage":"discard offset overflow","messagePattern":"discard offset overflow","errorType":"validation","errorClass":"std::io::Error","httpStatus":null,"severity":"error","filePath":"src/vmm/src/devices/virtio/block/virtio/io/sync_io.rs","lineNumber":59,"sourceCode":"    let (offset, len) = range;\n\n    if len == 0 {\n        return Ok(0);\n    }\n    let discarded = len;\n    let len = u64::from(len);\n\n    if file_type.is_block_device() {\n        let mut range = [offset, len];\n        // SAFETY: file is a valid fd, BLKDISCARD expects a pointer to two u64 values\n        // representing byte offset and byte length.\n        let ret = unsafe { libc::ioctl(file.as_raw_fd(), BLKDISCARD, range.as_mut_ptr()) };\n        if ret < 0 {\n            return Err(std::io::Error::last_os_error());\n        }\n    } else {\n        let off = libc::off_t::try_from(offset).map_err(|_| {\n            std::io::Error::new(std::io::ErrorKind::InvalidInput, \"discard offset overflow\")\n        })?;\n        let len = libc::off_t::try_from(len).map_err(|_| {\n            std::io::Error::new(std::io::ErrorKind::InvalidInput, \"discard length overflow\")\n        })?;\n        // SAFETY: file is a valid fd and fallocate does not retain the passed values.\n        let ret = unsafe {\n            libc::fallocate(\n                file.as_raw_fd(),\n                FALLOC_FL_KEEP_SIZE | FALLOC_FL_PUNCH_HOLE,\n                off,\n                len,\n            )\n        };\n        if ret < 0 {\n            return Err(std::io::Error::last_os_error());\n        }\n    }\n","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/firecracker-microvm/firecracker/blob/a5a45f68b862689efa31ea0847695c108d872121/src/vmm/src/devices/virtio/block/virtio/io/sync_io.rs#L41-L77","documentation":"This std::io::Error (InvalidInput) is raised in file_discard_range when the u64 discard offset cannot be converted to libc::off_t. The non-block-device path punches a hole with fallocate(2), which takes a signed off_t; on 64-bit Linux off_t is i64, so any offset above i64::MAX fails TryFrom and the function refuses to issue the ioctl rather than silently truncating. It signals that the requested discard range lies beyond what the syscall ABI can express.","triggerScenarios":"Calling file_discard_range (directly or via SyncFileEngine::discard / the virtio-block DISCARD request path) with range.0 (offset) > i64::MAX on a regular file backend, so libc::off_t::try_from(offset) returns Err.","commonSituations":"A guest issues DISCARD/TRIM with a sector-derived byte offset computed from a corrupt or malicious descriptor; a mis-sized backing file paired with a disk geometry whose total size exceeds i64 bytes (8 EiB); arithmetic overflow in the offset calculation (sector * 512 wrapping to a huge u64); tests constructing synthetic ranges near u64::MAX.","solutions":["Validate the discard offset against the backing file's actual size before calling file_discard_range; reject requests beyond the disk end in the virtio-block layer.","Clamp or bound the guest-provided sector so offset = sector * sector_size <= i64::MAX, returning an error to the guest instead of attempting the discard.","Run on a platform where off_t is 64-bit (64-bit Linux); on 32-bit builds compile with LFS (large file support) so off_t is 64-bit.","If the offset is legitimately huge, split the discard into multiple in-range chunks or return EINVAL to the guest since fallocate cannot address it."],"exampleFix":"// before: driver passes an unvalidated u64 offset\nfile_discard_range(&file, (offset, len))?;\n\n// after: caller validates the offset fits in off_t and the disk before discarding\nconst MAX_OFF: u64 = i64::MAX as u64;\nif offset > MAX_OFF || offset as u64 + len as u64 > disk_size {\n    return Err(io::Error::new(ErrorKind::InvalidInput, \"discard out of range\"));\n}\nfile_discard_range(&file, (offset, len))?;","handlingStrategy":"validation","validationCode":"const OFF_T_MAX: u64 = i64::MAX as u64;\nfn discard_offset_ok(offset: u64, len: u32, disk_size: u64) -> bool {\n    offset <= OFF_T_MAX && offset.saturating_add(len as u64) <= disk_size\n}\n// call before file_discard_range:\nassert!(discard_offset_ok(offset, len, disk_size), \"discard offset out of range\");","typeGuard":"fn fits_off_t(v: u64) -> bool { v <= i64::MAX as u64 }","tryCatchPattern":"match file_discard_range(&file, (offset, len)) {\n    Ok(n) => n,\n    Err(e) if e.kind() == std::io::ErrorKind::InvalidInput && e.to_string().contains(\"offset overflow\") => {\n        // reject the guest request; do not retry\n    }\n    Err(e) => return Err(SyncIoError::Discard(e)),\n}","preventionTips":["Always bounds-check guest-supplied sectors/offsets against the disk size before issuing discard.","Check offset <= i64::MAX on 64-bit targets; on 32-bit targets assume off_t may be 32-bit and check the actual size of libc::off_t.","Log and return an error to the guest (virtio BAD REQUEST) rather than unwrapping or retrying; overflow errors are deterministic, not transient.","Add unit tests for offsets at i64::MAX and u64::MAX boundaries."],"tags":["linux","io","fallocate","integer-overflow","virtio-block"],"backgroundTag":"value-out-of-range","analyzedSha":"a5a45f68b862689efa31ea0847695c108d872121","analyzedAt":"2026-09-10T14:33:13.708Z","contentChangedAt":"2026-09-10T14:33:13.708Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}