opentitanlib/test_utils/
fpga_backdoor.rs

1// Copyright lowRISC contributors (OpenTitan project).
2// Licensed under the Apache License, Version 2.0, see LICENSE for details.
3// SPDX-License-Identifier: Apache-2.0
4
5use anyhow::{Context, Result, bail};
6use clap::Args;
7use crc::{CRC_32_ISO_HDLC, Crc};
8use std::convert::From;
9use std::fs;
10use std::path::PathBuf;
11use std::str::FromStr;
12use thiserror::Error;
13
14use crate::app::TransportWrapper;
15use crate::io::fpga_backdoor::{
16    Backdoor, BackdoorParams, BackdoorTarget, BackdoorTargetInfo, enter_backdoor_loader,
17};
18use crate::io::jtag::JtagParams;
19use crate::util::vmem::{Section, Vmem, Word};
20
21// Reset the SoC and enter the backdoor loader, initializing a connection via JTAG.
22pub fn enter_backdoor(transport: &TransportWrapper, jtag_params: &JtagParams) -> Result<Backdoor> {
23    enter_backdoor_loader(transport)?;
24    let backdoor_params = BackdoorParams {
25        jtag: jtag_params.clone(),
26    };
27    backdoor_params.create(transport)?.connect(true)
28}
29
30// Normalize a word to a given number of bits.
31fn normalize_word(word: &mut Word, bits_per_word: usize) {
32    let bytes_per_word = bits_per_word.div_ceil(8);
33    if word.bytes.len() > bytes_per_word {
34        let start = word.bytes.len() - bytes_per_word;
35        word.bytes.drain(..start);
36    } else if word.bytes.len() < bytes_per_word {
37        let mut padded = vec![0u8; bytes_per_word - word.bytes.len()];
38        padded.append(&mut word.bytes);
39        word.bytes = padded;
40    }
41
42    let extra_bits = bits_per_word % 8;
43    if extra_bits > 0 {
44        word.bytes[0] &= 0xFF >> (8 - extra_bits);
45    }
46}
47
48/// A genuine content mismatch found by `verify_readback`, as opposed to some other failure
49/// (e.g. a JTAG/OpenOCD communication error) that can occur while a write is being verified.
50/// Kept distinct so callers can tell "verification ran and found a real mismatch" apart from
51/// "verification didn't get a conclusive answer at all".
52#[derive(Debug, Error)]
53#[error("Read verification at word {offset} failed. Expected: {expected}, Got: {actual}")]
54pub struct VerifyMismatch {
55    offset: u32,
56    expected: String,
57    actual: String,
58}
59
60// Check that words read from target memory match input words.
61pub fn verify_readback(
62    input: &mut [Word],
63    readback: &mut [Word],
64    bits_per_word: usize,
65    mut offset: u32,
66) -> Result<()> {
67    for (write_word, read_word) in readback.iter_mut().zip(input) {
68        normalize_word(write_word, bits_per_word);
69        normalize_word(read_word, bits_per_word);
70        if write_word != read_word {
71            return Err(VerifyMismatch {
72                offset,
73                expected: hex::encode(write_word.bytes.clone()),
74                actual: hex::encode(read_word.bytes.clone()),
75            }
76            .into());
77        }
78
79        offset += 1;
80    }
81
82    Ok(())
83}
84
85pub fn write_to_target(
86    target: &mut BackdoorTarget,
87    target_id: &str,
88    data: Vec<Section>,
89    verify: bool,
90    clearing: bool,
91) -> Result<()> {
92    // Zero the hash before writing: if this write is interrupted, or the content ends up
93    // different from what's expected, a stale hash must never survive to let a later
94    // `check_hash` run wrongly skip preloading now-different (or corrupted) content.
95    target
96        .write_hash(0)
97        .context("failed to clear HASH_LAST_LOADED before write")?;
98
99    // Perform the write(s)
100    if clearing {
101        log::info!("Clearing the {}...", target_id);
102    } else {
103        log::info!("Writing to the {}...", target_id);
104    }
105    for mut section in data {
106        log::debug!(
107            "Writing section of size {} to word {} of target {}",
108            section.data.len(),
109            section.addr,
110            target_id
111        );
112
113        // Special case - if we're just trying to clear the entire target memory,
114        // instead use the fast clearing operation.
115        let first_word = &section.data.first().clone();
116        if section.addr == 0
117            && section.data.len() == target.info.depth as usize
118            && section.data.iter().all(|w| w == first_word.unwrap())
119        {
120            log::debug!(
121                "Clearing target {} with word {}",
122                target_id,
123                hex::encode(first_word.unwrap().bytes.clone())
124            );
125            let start = std::time::Instant::now();
126            target.clear(first_word.unwrap(), true)?;
127            log::debug!(
128                "Cleared {} word(s) of target {} in {:?}",
129                section.data.len(),
130                target_id,
131                start.elapsed(),
132            );
133        } else {
134            let start = std::time::Instant::now();
135            target.write(section.addr, &section.data, false, true)?;
136            log::debug!(
137                "Wrote {} word(s) to target {} in {:?}",
138                section.data.len(),
139                target_id,
140                start.elapsed(),
141            );
142        }
143
144        // If requested, read back the data and verify against written contents
145        if verify {
146            let start = std::time::Instant::now();
147            let mut readback = target.read(section.addr, section.data.len() as u32, false)?;
148            verify_readback(
149                &mut section.data,
150                &mut readback,
151                target.info.width as usize,
152                section.addr,
153            )?;
154            log::debug!(
155                "Verified {} word(s) of target {} in {:?}",
156                section.data.len(),
157                target_id,
158                start.elapsed(),
159            );
160        }
161    }
162
163    Ok(())
164}
165
166#[derive(Debug, Clone)]
167pub struct TargetWrite {
168    pub target: String,
169    pub path: PathBuf,
170    pub offset: Option<u32>,
171}
172
173impl FromStr for TargetWrite {
174    type Err = anyhow::Error;
175
176    fn from_str(s: &str) -> Result<Self, Self::Err> {
177        let (target_path, offset) = s
178            .split_once('@')
179            .map(|(x, y)| (x, y.parse::<u32>().ok()))
180            .unwrap_or((s, None));
181
182        let (target, path) = target_path
183            .split_once('=')
184            .context("expected input like TARGET=FILE[@OFFSET], but no '=' was seen")?;
185        if target.is_empty() {
186            bail!("target name cannot be empty");
187        }
188        if path.is_empty() {
189            bail!("file path cannot be empty");
190        }
191
192        Ok(TargetWrite {
193            target: target.to_string(),
194            path: PathBuf::from(path),
195            offset,
196        })
197    }
198}
199
200impl TargetWrite {
201    pub fn load_data(&self) -> Result<Vec<Section>> {
202        log::info!("Loading VMEM file: {}", self.path.display());
203        let vmem_content = fs::read_to_string(&self.path)?;
204        let mut vmem = Vmem::from_str(&vmem_content, None)?;
205        vmem.merge_sections(None);
206        let mut sections: Vec<Section> = vmem.sections().cloned().collect();
207
208        // If an offset is given, all sections must be offset by that amount.
209        if let Some(offset) = self.offset {
210            for section in &mut sections {
211                section.addr += offset;
212            }
213        }
214
215        Ok(sections)
216    }
217
218    /// CRC32 (ISO-HDLC, the same variant used elsewhere in this crate, e.g.
219    /// `util::usr_access`/`test_utils::load_sram_program`) of the raw VMEM file content, used
220    /// to decide whether this exact content is already preloaded on the target (see
221    /// `backdoor_write` and the target's `HASH_LAST_LOADED` register).
222    fn file_hash(&self) -> Result<u32> {
223        let bytes = fs::read(&self.path)
224            .with_context(|| format!("failed to read VMEM file: {}", self.path.display()))?;
225        Ok(Crc::<u32>::new(&CRC_32_ISO_HDLC).checksum(&bytes))
226    }
227
228    /// Write this file to `backdoor`'s target.
229    ///
230    /// If `check_hash` is set, the preload is skipped entirely when the target's
231    /// `HASH_LAST_LOADED` register already reports a (non-zero) hash matching this file's
232    /// content -- `HASH_LAST_LOADED` survives the backdoor loader's own resets (see
233    /// `Backdoor::read_target_hash`), so this holds across consecutive tool invocations as
234    /// long as the FPGA itself isn't power-cycled/reflashed. When `check_hash` is clear, the
235    /// preload always happens unconditionally. Either way, the new hash is only stamped once
236    /// the write itself completes without error.
237    pub fn backdoor_write(
238        &self,
239        backdoor: &mut Backdoor,
240        verify: bool,
241        check_hash: bool,
242    ) -> Result<()> {
243        let mut target = backdoor
244            .target_by_id_str(&self.target)?
245            .context(format!("FPGA target '{}' not found", &self.target))?;
246
247        let new_hash = if check_hash {
248            let new_hash = self.file_hash()?;
249            let old_hash = target.read_hash()?;
250            // A stored hash of 0 never matches: it is both the register's power-on value
251            // (fresh bitstream) and the "unknown content" marker `write_to_target` stamps
252            // before any unhashed write, so it always forces a preload.
253            if old_hash == new_hash && old_hash != 0 {
254                log::info!(
255                    "Skipping preload of target {}: content hash unchanged (0x{new_hash:08x})",
256                    self.target,
257                );
258                return Ok(());
259            }
260            log::info!(
261                "Preloading target {}: content hash changed (old=0x{old_hash:08x}, new=0x{new_hash:08x})",
262                self.target,
263            );
264            Some(new_hash)
265        } else {
266            None
267        };
268
269        let data = self.load_data()?;
270        if let Err(err) = write_to_target(&mut target, &self.target, data, verify, false) {
271            // Only a genuine content mismatch from `verify_readback`
272            if verify && err.downcast_ref::<VerifyMismatch>().is_some() {
273                // Re-assert the clear explicitly here, before propagating the error, so it
274                // happens unconditionally regardless of how the caller handles the error: this
275                // target's memory no longer matches what was written, so no future run may
276                // trust a previously-stamped hash and skip re-preloading it.
277                target
278                    .write_hash(0)
279                    .context("failed to clear HASH_LAST_LOADED after integrity error")?;
280                return Err(err.context(format!(
281                    "integrity error: write verification failed for target {} \
282                     (HASH_LAST_LOADED has been cleared)",
283                    self.target
284                )));
285            }
286            return Err(err);
287        }
288        if let Some(new_hash) = new_hash {
289            target.write_hash(new_hash)?;
290        }
291
292        Ok(())
293    }
294}
295
296#[derive(Debug, Clone)]
297pub struct TargetClear {
298    pub target: String,
299    pub num_words: Option<u32>,
300    pub offset: Option<u32>,
301}
302
303impl FromStr for TargetClear {
304    type Err = anyhow::Error;
305
306    fn from_str(s: &str) -> Result<Self, Self::Err> {
307        let (target_words, offset) = s
308            .split_once('@')
309            .map(|(x, y)| (x, y.parse::<u32>().ok()))
310            .unwrap_or((s, None));
311
312        let (target, num_words) = target_words
313            .split_once('=')
314            .context("expected input like TARGET=NUM_WORDS[@OFFSET], but no '=' was seen")?;
315        if target.is_empty() {
316            bail!("target name cannot be empty");
317        }
318        if num_words.is_empty() {
319            bail!("num_words cannot be empty");
320        }
321        let num_words = if num_words.to_lowercase() == "all" {
322            None
323        } else {
324            Some(num_words.parse::<u32>()?)
325        };
326
327        Ok(TargetClear {
328            target: target.to_string(),
329            num_words,
330            offset,
331        })
332    }
333}
334
335impl TargetClear {
336    pub fn as_sections(&self, target_info: &BackdoorTargetInfo) -> Vec<Section> {
337        let num_bytes = target_info.width.div_ceil(8) as usize;
338        let remaining_words = target_info.depth - self.offset.unwrap_or(0);
339        let num_words = self.num_words.unwrap_or(remaining_words);
340        let data = vec![Word::new(vec![0x00; num_bytes]); num_words as usize];
341        vec![Section {
342            addr: self.offset.unwrap_or(0),
343            data,
344        }]
345    }
346
347    pub fn backdoor_write(&self, backdoor: &mut Backdoor, verify: bool) -> Result<()> {
348        let mut target = backdoor
349            .target_by_id_str(&self.target)?
350            .context(format!("FPGA target '{}' not found", &self.target))?;
351
352        let data = self.as_sections(&target.info);
353        write_to_target(&mut target, &self.target, data, verify, true)
354    }
355}
356
357// Write FPGA memories after loading the bitstream
358#[derive(Debug, Args)]
359pub struct LoadMemories {
360    /// Memories to be cleared / zeroed. All clears apply before writes.
361    #[arg(long = "clear-memory", value_name = "TARGET=NUM_WORDS[@OFFSET]")]
362    pub target_clears: Vec<TargetClear>,
363
364    /// Memories to be written, mapping VMEM files to FPGA target memories.
365    #[arg(long = "load-memory", value_name = "TARGET=FILE[@OFFSET]")]
366    pub target_writes: Vec<TargetWrite>,
367
368    /// Targets (matching a name given to `--load-memory`) for which the memory file's hash is
369    /// checked against the target's HASH_LAST_LOADED register before writing, skipping the
370    /// preload if it's unchanged since the last time this target was written. Only targets
371    /// named here are checked; any other `--load-memory` target is always preloaded
372    /// unconditionally.
373    #[arg(long = "check-memory-hash", value_name = "TARGET")]
374    pub check_memory_hash: Vec<String>,
375}
376
377impl LoadMemories {
378    pub fn init(&self, transport: &TransportWrapper, jtag_params: &JtagParams) -> Result<()> {
379        if self.target_clears.is_empty() && self.target_writes.is_empty() {
380            return Ok(());
381        }
382
383        let mut backdoor = enter_backdoor(transport, jtag_params)?;
384        self.target_clears
385            .iter()
386            .try_for_each(|t| t.backdoor_write(&mut backdoor, false))?;
387        self.target_writes.iter().try_for_each(|t| {
388            let check_hash = self.check_memory_hash.contains(&t.target);
389            t.backdoor_write(&mut backdoor, false, check_hash)
390        })?;
391        backdoor.set_done()?;
392
393        Ok(())
394    }
395}
396
397// Unit tests for `TargetWrite::file_hash`, the VMEM-file content hashing used by the
398// `--check-memory-hash` skip-preload feature. These only exercise the pure hashing logic
399// against local temp files, so they don't require real hardware/JTAG.
400#[cfg(test)]
401mod tests {
402    use super::*;
403    use std::io::{Seek, SeekFrom, Write};
404
405    #[test]
406    fn file_hash_matches_known_crc32_iso_hdlc_check_value() {
407        // "123456789" is the standard CRC-32/ISO-HDLC ("check") test vector; its checksum is
408        // published as 0xcbf43926, independent of this crate's implementation.
409        let mut file = tempfile::NamedTempFile::new().unwrap();
410        file.write_all(b"123456789").unwrap();
411
412        let target_write = TargetWrite {
413            target: "TEST".to_string(),
414            path: file.path().to_path_buf(),
415            offset: None,
416        };
417        assert_eq!(target_write.file_hash().unwrap(), 0xcbf43926);
418    }
419
420    #[test]
421    fn file_hash_changes_with_content() {
422        let mut file = tempfile::NamedTempFile::new().unwrap();
423        file.write_all(b"hello").unwrap();
424        let target_write = TargetWrite {
425            target: "TEST".to_string(),
426            path: file.path().to_path_buf(),
427            offset: None,
428        };
429        let hash_a = target_write.file_hash().unwrap();
430
431        file.as_file_mut().set_len(0).unwrap();
432        file.seek(SeekFrom::Start(0)).unwrap();
433        file.write_all(b"world").unwrap();
434        let hash_b = target_write.file_hash().unwrap();
435
436        assert_ne!(hash_a, hash_b);
437    }
438}