diff --git a/dstack/dstack-util/src/system_setup.rs b/dstack/dstack-util/src/system_setup.rs index 66839b4fa..31d42216c 100644 --- a/dstack/dstack-util/src/system_setup.rs +++ b/dstack/dstack-util/src/system_setup.rs @@ -3346,7 +3346,8 @@ fn validate_single_luks2_header(mut reader: impl std::io::Read, hdr_ind: u64) -> // Pin where the encrypted key material is read from. The binary area // must sit between the two header copies and the encrypted payload; // otherwise a host with raw disk access could redirect it elsewhere. - if area.offset() < 2 * hdr_size || area.offset() + area.size() > PAYLOAD_OFFSET { + let area_end = area.offset().checked_add(area.size()); + if area.offset() < 2 * hdr_size || area_end.is_none_or(|end| end > PAYLOAD_OFFSET) { bail!( "Invalid LUKS keyslot area: offset={} size={}", area.offset(), @@ -3462,22 +3463,52 @@ fn test_validate_luks2_header_rejects_out_of_range_keyslot_area() { // so the surrounding header stays intact; "00768" parses to 768, which is // inside the header copies (< 2 * hdr_size) rather than the metadata gap. let mut header = include_bytes!("../tests/fixtures/luks_header_good").to_vec(); - let needle = br#""offset":"32768""#; - let replacement = br#""offset":"00768""#; + let patched = patch_luks_json(&mut header, br#""offset":"32768""#, br#""offset":"00768""#); + assert_eq!(patched, 2, "expected to patch both header copies"); + let error = validate_luks2_headers(&mut &header[..]).unwrap_err(); + assert!(error.to_string().contains("Invalid LUKS keyslot area")); +} + +/// Replace `needle` with `replacement` inside every LUKS JSON region, keeping +/// each region the same length by giving back trailing NUL padding. +#[cfg(test)] +fn patch_luks_json(header: &mut [u8], needle: &[u8], replacement: &[u8]) -> usize { + assert!(replacement.len() >= needle.len()); + let grow = replacement.len() - needle.len(); let mut patched = 0; let mut i = 0; while i + needle.len() <= header.len() { - if &header[i..i + needle.len()] == needle { - header[i..i + needle.len()].copy_from_slice(replacement); - patched += 1; - i += needle.len(); - } else { + if &header[i..i + needle.len()] != needle { i += 1; + continue; } - } + let tail = &mut header[i..]; + let end = tail.iter().position(|b| *b == 0).expect("NUL padding"); + assert!(end + grow < tail.len(), "not enough NUL padding"); + tail.copy_within(needle.len()..end, replacement.len()); + tail[..replacement.len()].copy_from_slice(replacement); + patched += 1; + i += replacement.len(); + } + patched +} + +#[test] +fn test_validate_luks2_header_rejects_keyslot_area_that_overflows() { + let mut header = include_bytes!("../tests/fixtures/luks_header_good").to_vec(); + // 2**64 - 32768 + 1000: added to the accepted offset 32768 it wraps to + // 1000, which is below PAYLOAD_OFFSET. + let patched = patch_luks_json( + &mut header, + br#""size":"258048""#, + br#""size":"18446744073709519848""#, + ); assert_eq!(patched, 2, "expected to patch both header copies"); let error = validate_luks2_headers(&mut &header[..]).unwrap_err(); - assert!(error.to_string().contains("Invalid LUKS keyslot area")); + assert!( + error.to_string().contains("Invalid LUKS keyslot area"), + "{error:#}" + ); } #[cfg(test)]