ast10x0-hace: fix DMA safety violations
diff --git a/target/ast10x0/peripherals/hace/aes.rs b/target/ast10x0/peripherals/hace/aes.rs index 19b5221..4c906d1 100644 --- a/target/ast10x0/peripherals/hace/aes.rs +++ b/target/ast10x0/peripherals/hace/aes.rs
@@ -17,7 +17,7 @@ AES_CMD_BASE, HACE_CMD_AES128, HACE_CMD_AES256, HACE_CMD_CBC, HACE_CMD_ECB, HACE_CMD_ENCRYPT, HACE_SG_LAST, POLL_YIELD_NS, }; -use super::context::CryptoContext; +use super::context::{CryptoContext, AES_DATA_CAP}; use super::device::HaceDevice; use super::error::HaceError; use super::helpers::ptr_to_u32; @@ -93,10 +93,13 @@ input: &[u8], output: &mut [u8], ) -> Result<(), HaceError> { - // Enforce block-aligned sizing before programming the engine. + // Enforce block-aligned sizing and DMA staging cap before programming. if input.is_empty() || input.len() % AES_BLOCK != 0 || output.len() < input.len() { return Err(HaceError::InvalidInput); } + if input.len() > AES_DATA_CAP { + return Err(HaceError::InvalidInput); + } let kbits = Self::keylen_bits(key)?; let len = u32::try_from(input.len()).map_err(|_| HaceError::InvalidInput)?; @@ -107,9 +110,17 @@ } self.ctx.ctx[AES_BLOCK..AES_BLOCK + key.len()].copy_from_slice(key); - // SG descriptors: addr = data phys, len = bytes | HACE_SG_LAST. - let in_ptr = ptr_to_u32(input.as_ptr())?; - let out_ptr = ptr_to_u32(output.as_ptr())?; + // DMA safety (D3): copy caller input into the .ram_nc staging buffer. + // The HACE engine reads/writes by physical address; if the caller's + // slice is in flash, rodata, or a cacheable stack frame the engine + // would silently read/write stale physical RAM. Staging through + // ctx.data_in / ctx.data_out (both inside the .ram_nc CryptoContext) + // guarantees the DMA addresses are always in non-cacheable SRAM. + self.ctx.data_in[..input.len()].copy_from_slice(input); + + // SG descriptors: addr = .ram_nc staging buffers, len = bytes | HACE_SG_LAST. + let in_ptr = ptr_to_u32(self.ctx.data_in.as_ptr())?; + let out_ptr = ptr_to_u32(self.ctx.data_out.as_ptr())?; self.ctx.src.addr = in_ptr; self.ctx.src.len = len | HACE_SG_LAST; self.ctx.dst.addr = out_ptr; @@ -141,8 +152,23 @@ self.ctx.ctx = [0u8; 64]; if done { + // Copy result out of .ram_nc staging buffer to caller's output. + let n = input.len(); + output + .get_mut(..n) + .ok_or(HaceError::InvalidInput)? + .copy_from_slice( + self.ctx + .data_out + .get(..n) + .ok_or(HaceError::InvalidInput)?, + ); + // Scrub staging buffers so plaintext/ciphertext doesn't linger. + self.ctx.data_in[..n].fill(0); + self.ctx.data_out[..n].fill(0); Ok(()) } else { + self.ctx.data_in[..input.len()].fill(0); Err(HaceError::Timeout) } }
diff --git a/target/ast10x0/peripherals/hace/context.rs b/target/ast10x0/peripherals/hace/context.rs index c3f08d8..80c23d7 100644 --- a/target/ast10x0/peripherals/hace/context.rs +++ b/target/ast10x0/peripherals/hace/context.rs
@@ -172,6 +172,14 @@ // `#[repr(C, align(64))]`, single-in-flight discipline (and the same // layout-sensitivity caution, goal.md §2.2) as `HashContext`. +/// Maximum AES payload (source + destination) that can be staged through the +/// `.ram_nc` DMA buffers inside [`CryptoContext`]. Operations larger than this +/// return [`super::error::HaceError::InvalidInput`]. +/// +/// 512 bytes = 32 AES blocks, covering all current use-cases (key-unwrap, +/// CFI attestation, etc.). Increase here if larger payloads are ever needed. +pub(crate) const AES_DATA_CAP: usize = 512; + #[repr(C, align(64))] pub(crate) struct CryptoContext { /// Engine context buffer: `[0..16)` IV (CBC), `[16..16+keylen)` raw key @@ -184,6 +192,14 @@ /// Command word composed per goal.md §1.9.2 (unused as engine input — the /// driver writes it to HACE10 directly — kept for parity of layout/debug). pub(crate) cmd: u32, + /// DMA-safe input staging buffer (`.ram_nc`). The caller's plaintext or + /// ciphertext is CPU-copied here before the engine is started, ensuring + /// the DMA source is always in non-cacheable SRAM regardless of where the + /// caller allocated the slice (flash, stack, cacheable SRAM). + pub(crate) data_in: [u8; AES_DATA_CAP], + /// DMA-safe output staging buffer (`.ram_nc`). The engine writes here; + /// the result is CPU-copied to the caller's output slice after completion. + pub(crate) data_out: [u8; AES_DATA_CAP], } impl CryptoContext { @@ -193,6 +209,8 @@ src: Sg::new(), dst: Sg::new(), cmd: 0, + data_in: [0; AES_DATA_CAP], + data_out: [0; AES_DATA_CAP], } } }
diff --git a/target/ast10x0/peripherals/hace/digest.rs b/target/ast10x0/peripherals/hace/digest.rs index 0ea53b4..19fc07c 100644 --- a/target/ast10x0/peripherals/hace/digest.rs +++ b/target/ast10x0/peripherals/hace/digest.rs
@@ -140,6 +140,36 @@ let yield_fn: &'a mut dyn FnMut(u32) = &mut device.yield_fn; Self::new(regs, ctx, poll_budget, yield_fn) } + + /// DMA one full block held in `ctx.buffer` (always `.ram_nc`) to the engine. + /// + /// Called only when `ctx.bufcnt == ctx.block_size`. Resets `bufcnt` to 0 + /// on success so the buffer can be reused for the next block. + fn flush_block(&mut self) -> Result<(), HaceError> { + let buf_ptr = ptr_to_u32(self.ctx.buffer.as_ptr())?; + let bufcnt = self.ctx.bufcnt; + self.ctx.sg[0].addr = buf_ptr; + self.ctx.sg[0].len = bufcnt | HACE_SG_LAST; + + let sg_addr = ptr_to_u32(self.ctx.sg.as_ptr())?; + let digest_addr = ptr_to_u32(self.ctx.digest.as_ptr())?; + let method = self.ctx.method; + + self.regs.clear_hash_intflag(); + self.regs + .program_hash_operation(sg_addr, digest_addr, bufcnt, method); + + for _ in 0..self.poll_budget { + if self.regs.hash_intflag_is_set() { + self.ctx.bufcnt = 0; + return Ok(()); + } + (self.yield_fn)(POLL_YIELD_NS); + } + + self.regs.stop_hash_operation(); + Err(HaceError::Timeout) + } } impl<'a, T: DigestAlgorithm> ErrorType for HaceDigest<'a, T> { @@ -193,68 +223,45 @@ self.ctx.digcnt[1] += 1; } - // If all input fits without filling a complete block, buffer it. - if self.ctx.bufcnt + input_len < self.ctx.block_size { - let start = self.ctx.bufcnt as usize; - let end = start + input_len as usize; - self.ctx.buffer[start..end].copy_from_slice(input); - self.ctx.bufcnt += input_len; - return Ok(()); - } - - // Process one or more full blocks via SG. - let remaining = (input_len + self.ctx.bufcnt) % self.ctx.block_size; - let total_len = (input_len + self.ctx.bufcnt) - remaining; - let mut sg_idx = 0usize; - - // Capture pointers before mutating the SG table. - let buf_ptr = ptr_to_u32(self.ctx.buffer.as_ptr())?; - let input_ptr = ptr_to_u32(input.as_ptr())?; - - if self.ctx.bufcnt != 0 { - self.ctx.sg[0].addr = buf_ptr; - self.ctx.sg[0].len = self.ctx.bufcnt; - if total_len == self.ctx.bufcnt { - // Existing buffer is the only SG entry; input becomes the tail. - self.ctx.sg[0].addr = input_ptr; - self.ctx.sg[0].len |= HACE_SG_LAST; + // Copy all input through ctx.buffer (.ram_nc) one block at a time. + // + // DMA safety (D1/D2): the HACE engine reads data by physical address + // via SG descriptors. If the caller's `input` slice is in flash, rodata, + // or a cacheable stack buffer, the engine may read stale zeros from + // physical RAM. By staging every byte through `ctx.buffer` (which is + // placed in `.ram_nc` by the linker) we guarantee the DMA source is + // always in non-cacheable SRAM. This also covers HMAC's `scratch` + // buffer (D2) since it routes through `update()` via `one_shot!`. + let block_size = self.ctx.block_size as usize; + let mut offset = 0usize; + while offset < input.len() { + let bufcnt = self.ctx.bufcnt as usize; + let space = block_size.saturating_sub(bufcnt); + let chunk_len = core::cmp::min(space, input.len() - offset); + // Invariant: block_size is 64 or 128 (set by init()); if somehow + // zero, bail rather than infinite-loop. + if chunk_len == 0 { + return Err(HaceError::InvalidInput); } - sg_idx += 1; - } - if total_len != self.ctx.bufcnt { - self.ctx.sg[sg_idx].addr = input_ptr; - self.ctx.sg[sg_idx].len = (total_len - self.ctx.bufcnt) | HACE_SG_LAST; - } + let dst = self + .ctx + .buffer + .get_mut(bufcnt..bufcnt.saturating_add(chunk_len)) + .ok_or(HaceError::InvalidInput)?; + let src = input + .get(offset..offset.saturating_add(chunk_len)) + .ok_or(HaceError::InvalidInput)?; + dst.copy_from_slice(src); - let sg_addr = ptr_to_u32(self.ctx.sg.as_ptr())?; - let digest_addr = ptr_to_u32(self.ctx.digest.as_ptr())?; - let method = self.ctx.method; + self.ctx.bufcnt += chunk_len as u32; + offset += chunk_len; - self.regs.clear_hash_intflag(); - self.regs - .program_hash_operation(sg_addr, digest_addr, total_len, method); - - let mut done = false; - for _ in 0..self.poll_budget { - if self.regs.hash_intflag_is_set() { - done = true; - break; + // When the buffer holds exactly one full block, flush it via DMA. + if self.ctx.bufcnt == self.ctx.block_size { + self.flush_block()?; } - (self.yield_fn)(POLL_YIELD_NS); } - if !done { - self.regs.stop_hash_operation(); - return Err(HaceError::Timeout); - } - - // Copy remainder of input into the buffer for the next call. - if remaining != 0 { - let src_start = (total_len - self.ctx.bufcnt) as usize; - self.ctx.buffer[..remaining as usize] - .copy_from_slice(&input[src_start..src_start + remaining as usize]); - } - self.ctx.bufcnt = remaining; Ok(()) }