Skip to main content

vk/mem/
guard.rs

1//! Opt-in VMA 3.4.0 debug fill and corruption guards (license/vma/MIT.txt).
2//! Donor: `IsCorruptionDetectionEnabled`, `WriteMagicValueAfterAllocation`,
3//! `ValidateMagicValueAfterAllocation`, `CheckCorruption` and `FillAllocation`.
4//! Adaptation: explicit settings/errors, balanced map holds, no assertion abort
5//! on corruption and no hidden GPU completion. Only coherent pooled storage has guards.
6use super::*;
7use crate::mem::DebugMemorySettings;
8
9const MAGIC: u32 = 0x7F84_E666;
10
11pub(super) fn eligible(flags: vk::MemoryPropertyFlags) -> bool {
12	let required =
13		vk::MemoryPropertyFlagBits::HOST_VISIBLE_BIT | vk::MemoryPropertyFlagBits::HOST_COHERENT_BIT;
14	flags.as_raw() & required.as_raw() == required.as_raw()
15}
16
17/// Configure debug instrumentation before native allocations or pools exist.
18/// # Errors
19/// Rejects live domains, non-four-byte guard margins or missing required map PFNs.
20pub fn set_debug_memory_settings(
21	allocator: &mut Allocator<'_>,
22	settings: DebugMemorySettings,
23) -> Result<(), AllocError> {
24	allocator.ensure_global()?;
25	allocator.ensure_open()?;
26	if allocator.native_count != 0 || !allocator.live.is_empty() {
27		return Err(AllocError::LiveAllocations);
28	}
29	if !allocator.pools.is_empty() {
30		return Err(AllocError::LivePools);
31	}
32	if !settings.margin.is_multiple_of(4) {
33		return Err(AllocError::InvalidRequest(
34			"debug guard margin must be a multiple of four",
35		));
36	}
37	if settings.margin != 0 || settings.initialize_allocations || settings.poison_frees {
38		require(allocator.device.fp().map_memory.is_some(), "vkMapMemory")?;
39		require(
40			allocator.device.fp().unmap_memory.is_some(),
41			"vkUnmapMemory",
42		)?;
43		if settings.initialize_allocations || settings.poison_frees {
44			require(
45				allocator.device.fp().flush_mapped_memory_ranges.is_some(),
46				"vkFlushMappedMemoryRanges",
47			)?;
48		}
49	}
50	allocator.debug_settings = settings;
51	Ok(())
52}
53
54unsafe fn fill(
55	allocator: &mut Allocator<'_>,
56	allocation: &Allocation,
57	byte: u8,
58) -> Result<(), AllocError> {
59	let length = usize::try_from(allocation.size).map_err(|_| AllocError::InvalidRange)?;
60	// SAFETY: caller supplies synchronized host-visible bytes and no conflicting Rust reference.
61	let pointer = unsafe { map(allocator, allocation) }?;
62	// SAFETY: map validates complete native host extent and requested bytes are within the live allocation.
63	unsafe { pointer.cast::<u8>().write_bytes(byte, length) };
64	// SAFETY: the checked hold covers every initialized allocation byte.
65	let flushed = unsafe { flush(allocator, allocation, 0, vk::WHOLE_SIZE) };
66	// SAFETY: no helper pointer uses remain; preserve persistent and other explicit holds.
67	let cleanup = unsafe { unmap(allocator, allocation) };
68	flushed.and(cleanup)
69}
70
71pub(super) unsafe fn initialize(
72	allocator: &mut Allocator<'_>,
73	allocation: &Allocation,
74) -> Result<(), AllocError> {
75	let settings = allocator.debug_settings;
76	if settings.margin == 0 && !settings.initialize_allocations && !settings.poison_frees {
77		return Ok(());
78	}
79	let live = allocator.validate(allocation)?;
80	if live.imported {
81		return Ok(());
82	}
83	let guard_size = live.guard_size;
84	let guard_offset = usize::try_from(live.guard_offset).map_err(|_| AllocError::InvalidRange)?;
85	if allocator.debug_settings.initialize_allocations && allocation.is_host_visible() {
86		// SAFETY: new unpublished unbound storage; no consumer or resource can access it yet.
87		unsafe { fill(allocator, allocation, 0xDC) }?;
88	}
89	if guard_size != 0 {
90		let length = usize::try_from(guard_size).map_err(|_| AllocError::InvalidRange)?;
91		// SAFETY: guard reservation was checked in native placement; new storage has no conflicting uses.
92		let pointer = unsafe { map(allocator, allocation) }?;
93		for offset in (0..length).step_by(4) {
94			// SAFETY: every four-byte word fits the separately reserved coherent guard tail.
95			unsafe {
96				pointer
97					.cast::<u8>()
98					.add(guard_offset + offset)
99					.cast::<u32>()
100					.write_unaligned(MAGIC)
101			};
102		}
103		// SAFETY: coherent guard writes need no cache flush; helper pointer uses are complete.
104		unsafe { unmap(allocator, allocation) }?;
105	}
106	allocator
107		.live
108		.get_mut(&allocation.serial)
109		.expect("live initialized allocation")
110		.debug_ready = true;
111	Ok(())
112}
113
114pub(super) unsafe fn check_one(
115	allocator: &mut Allocator<'_>,
116	allocation: &Allocation,
117) -> Result<(), AllocError> {
118	let live = allocator.validate(allocation)?;
119	if !live.debug_ready || live.guard_size == 0 {
120		return Ok(());
121	}
122	let guard_offset = usize::try_from(live.guard_offset).map_err(|_| AllocError::InvalidRange)?;
123	let length = usize::try_from(live.guard_size).map_err(|_| AllocError::InvalidRange)?;
124	// SAFETY: caller establishes completion and coherent host visibility; guard bytes were initialized before publication.
125	let pointer = unsafe { map(allocator, allocation) }?;
126	let mut failure = None;
127	for offset in (0..length).step_by(4) {
128		// SAFETY: initialized four-byte words remain inside the live reserved native guard tail.
129		let value = unsafe {
130			pointer
131				.cast::<u8>()
132				.add(guard_offset + offset)
133				.cast::<u32>()
134				.read_unaligned()
135		};
136		if value != MAGIC {
137			failure = Some(AllocError::CorruptionDetected {
138				allocation: allocation.serial,
139				offset: u64::try_from(guard_offset + offset).expect("mapped native host extent"),
140			});
141			break;
142		}
143	}
144	// SAFETY: every helper read is complete; release only this helper's explicit hold.
145	let cleanup = unsafe { unmap(allocator, allocation) };
146	if let Some(error) = failure {
147		Err(error)
148	} else {
149		cleanup
150	}
151}
152
153pub(super) unsafe fn before_free(
154	allocator: &mut Allocator<'_>,
155	allocation: &Allocation,
156) -> Result<(), AllocError> {
157	if allocator.debug_settings.margin == 0 && !allocator.debug_settings.poison_frees {
158		return Ok(());
159	}
160	if !allocator.validate(allocation)?.debug_ready {
161		return Ok(());
162	}
163	// SAFETY: the release contract ends all resource/host/GPU uses before debug reads/writes.
164	unsafe { check_one(allocator, allocation) }?;
165	if allocator.debug_settings.poison_frees && allocation.is_host_visible() {
166		// SAFETY: caller ended all accesses and destroyed resources; no live Rust borrow may conflict.
167		unsafe { fill(allocator, allocation, 0xEF) }?;
168	}
169	Ok(())
170}
171
172unsafe fn check(
173	allocator: &mut Allocator<'_>,
174	bits: u32,
175	pool: Option<u64>,
176) -> Result<(), AllocError> {
177	allocator.ensure_open()?;
178	if allocator.debug_settings.margin == 0
179		|| !(0..allocator.selector.properties.memory_type_count)
180			.any(|index| bits & (1 << index) != 0 && eligible(memory_flags(allocator, index)))
181	{
182		return Err(AllocError::UnsupportedCapability(
183			"debug corruption detection is not enabled for these memory types",
184		));
185	}
186	let mut selected = HostVec::new(allocator.host);
187	selected
188		.try_reserve_exact(allocator.live.len())
189		.map_err(|_| AllocError::HostAllocationFailed)?;
190	for (&serial, live) in &allocator.live {
191		if bits & (1 << live.memory_type_index) != 0
192			&& live.guard_size != 0
193			&& (pool.is_none() || live.pool == pool)
194		{
195			selected.push_reserved(serial);
196		}
197	}
198	selected.sort_unstable();
199	for &serial in &selected {
200		let allocation = allocator.token(serial)?;
201		// SAFETY: caller supplies completion/synchronization for every selected live guard.
202		unsafe { check_one(allocator, &allocation) }?;
203	}
204	Ok(())
205}
206
207/// Check enabled coherent pooled guard tails without waiting for GPU work.
208/// # Errors
209/// Returns unsupported debug configuration, identity/map or corruption diagnostics.
210/// A corrupt region remains allocated; no memory/resource is destroyed on failure.
211/// # Safety
212/// Complete and synchronize every selected region's GPU/host writes before checking.
213pub unsafe fn check_corruption(
214	allocator: &mut Allocator<'_>,
215	memory_type_bits: u32,
216) -> Result<(), AllocError> {
217	// SAFETY: caller supplies the all-selected-region completion/access contract.
218	unsafe { check(allocator, memory_type_bits, None) }
219}
220
221/// Check coherent guard tails belonging to one checked custom pool.
222/// # Safety
223/// Same completion/coherent-access requirements as `check_corruption`.
224pub unsafe fn check_pool_corruption(
225	allocator: &mut Allocator<'_>,
226	pool: &Pool,
227) -> Result<(), AllocError> {
228	let index = pool::validate(allocator, pool)?.info.memory_type_index;
229	// SAFETY: caller supplies the selected pool's completion/access contract.
230	unsafe { check(allocator, 1 << index, Some(pool.serial)) }
231}
232
233impl Allocator<'_> {
234	/// Configure opt-in guard/fill instrumentation before acquiring memory/pools.
235	pub fn set_debug_memory_settings(
236		&mut self,
237		settings: DebugMemorySettings,
238	) -> Result<(), AllocError> {
239		set_debug_memory_settings(self, settings)
240	}
241
242	/// Explicitly inspect enabled coherent pooled debug guards after completion.
243	/// # Safety
244	/// Same all-selected-region access contract as `mem::check_corruption`.
245	pub unsafe fn check_corruption(&mut self, bits: u32) -> Result<(), AllocError> {
246		// SAFETY: caller supplies the canonical completion/visibility contract.
247		unsafe { check_corruption(self, bits) }
248	}
249
250	/// Inspect one custom pool's enabled coherent debug guards.
251	/// # Safety
252	/// Same pool completion/access contract as `mem::check_pool_corruption`.
253	pub unsafe fn check_pool_corruption(&mut self, pool: &Pool) -> Result<(), AllocError> {
254		// SAFETY: caller supplies the canonical completion/visibility contract.
255		unsafe { check_pool_corruption(self, pool) }
256	}
257}