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(())
     }