Skip to main content

rpi_loader_ota/
apply.rs

1//! Installing a validated bundle onto a FAT volume.
2//!
3//! This is the half that knows what a boot partition is. It takes the
4//! entries [`Bundle::parse`] handed back and writes them where they say,
5//! in an order chosen so that a failure part-way through leaves a board
6//! that still boots.
7//!
8//! # What the caller still supplies
9//!
10//! The transport and the reboot: how a bundle arrived is application
11//! shaped, and a crate that took it would be choosing the web framework.
12//! And the clock — [`Progress`] reports what happened and the caller
13//! decides what to time, which is what keeps an async runtime's clock and
14//! whatever counts device commands out of this crate. The timing itself is
15//! [`crate::measure::Measure`], a `Progress` that takes the clock as a
16//! function, for a caller that wants what every board measures.
17//!
18//! # Why it writes the way it does
19//!
20//! Two rules below look like implementation detail and are not: they are
21//! the difference between an update that takes seconds and one that takes
22//! minutes. The figures come from a Pi 2 writing a 1.6 MB kernel to an SD
23//! card, and they are here rather than in an application because the rules
24//! they justify are here.
25//!
26//! **The card charges per command, not per block.** A single-block write
27//! cost 17.5 ms on that card; a 128-block write about 26 ms — one and a
28//! half times the cost for a hundred and twenty-eight times the data, so
29//! 62× cheaper per block. Nothing about the card changed between those two
30//! numbers. It was being asked the wrong way.
31//!
32//! So: **one `write_file` per entry, with the length known up front.** The
33//! chain is then allocated in one go and the file comes out contiguous,
34//! which is what lets the layer underneath issue one long transfer instead
35//! of thousands of short ones. A file grown a write at a time gets whatever
36//! clusters happen to be spare at each step, and no amount of care further
37//! down recovers from that. Through a filesystem that wrote single blocks,
38//! the same kernel took 99,683 ms and 5,703 card commands; written this
39//! way, 928 ms and 36. That is the whole of the 107×.
40//!
41//! And: **the read-back buffer is 64 KiB**, which is 128 blocks a command —
42//! the same argument from the reading side.
43//!
44//! **Skipping costs a read, and a read is much cheaper than a write.**
45//! Reading an entry back to decide whether to write it sounds like added
46//! work and is how a bundle carrying 3 MB of Raspberry Pi firmware — which
47//! changes about once a year — stays affordable to ship every time. A
48//! 6-entry, 4.8 MB bundle whose contents the card already held cost 1055 ms
49//! and **no write commands at all**, against 3724 ms to write the same
50//! thing in full.
51//!
52//! **What is left is a floor of roughly 250 ms per entry, whatever its
53//! size**, nearly all of it filesystem bookkeeping rather than data: a
54//! rewrite frees the old chain and allocates a new one, each of which
55//! flushes the allocation table, and FAT32 keeps two copies of it. So the
56//! cost of an update scales with the *number* of entries far more than with
57//! their bytes. Six files is nothing; several hundred small assets would be
58//! the thing to think about.
59
60use resident_fat::{BlockDevice, FileSystem};
61
62use crate::bundle::{self, Bundle, Checksum, Entry, Format, Role};
63
64/// Bytes read at a time when checking what is already on the card.
65///
66/// Not a stack buffer: this is allocated once per [`apply`] call and reused,
67/// because the size is the whole point. A card charges per command far more
68/// than per block, so reading a 3 MB file 512 bytes at a time would cost
69/// thousands of commands and defeat the comparison it exists to make cheap.
70/// 64 KiB is 128 blocks a call — enough that the per-command cost stops
71/// mattering, small enough to be nothing beside the bundle already in
72/// memory.
73const SCRATCH: usize = 64 * 1024;
74
75/// What installing a bundle touched.
76#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
77pub struct Report {
78    /// Size of the kernel installed, when the bundle carried one.
79    ///
80    /// `None` is not a failure: a bundle with no kernel is how a website or
81    /// a settings file is updated without rewriting an image that has not
82    /// changed.
83    pub kernel_len: Option<u32>,
84    /// Entries written.
85    pub written: u32,
86    /// Entries the card already held, and which were therefore not written.
87    pub skipped: u32,
88}
89
90/// Told what the installer is doing, so that timing stays with the caller.
91///
92/// Every method does nothing by default, so an implementation names only
93/// what it cares about, and `()` implements the whole thing for a caller
94/// that cares about none of it.
95///
96/// The boundaries are where they are so that a caller can time the write
97/// and the read-back separately. Those are not the same operation and do
98/// not have the same fix — a write is erase-block program cycles and a read
99/// is not — so a single figure covering both would hide which of them an
100/// improvement had touched.
101pub trait Progress {
102    /// About to consider `entry`; nothing has been read or written yet.
103    fn starting(&mut self, entry: Entry<'_>) {
104        let _ = entry;
105    }
106
107    /// `entry` has been written and is about to be read back.
108    fn wrote(&mut self, entry: Entry<'_>) {
109        let _ = entry;
110    }
111
112    /// `entry` has been read back and matched.
113    fn verified(&mut self, entry: Entry<'_>) {
114        let _ = entry;
115    }
116
117    /// The card already held `entry`, so nothing was written. It has still
118    /// been read in full — that is how this was established.
119    fn skipped(&mut self, entry: Entry<'_>) {
120        let _ = entry;
121    }
122}
123
124impl Progress for () {}
125
126/// Why an install was refused or failed.
127///
128/// Generic over the device error, unlike [`bundle::Error`], which is why the
129/// two are separate types: a host that only ever builds bundles would
130/// otherwise have to name a block-device error it does not have.
131#[derive(Debug)]
132pub enum Error<E> {
133    /// The bundle was rejected before anything was written.
134    Bundle(bundle::Error),
135    /// The card or the filesystem failed.
136    Storage(resident_fat::Error<E>),
137    /// A file read back after writing did not match what was written.
138    VerifyFailed,
139}
140
141impl<E> Error<E> {
142    /// The HTTP status to answer this with, for a device that took the
143    /// bundle over HTTP.
144    ///
145    /// A rejected bundle is the sender's fault and a card that failed
146    /// mid-write is not, and answering the same to both sends whoever is
147    /// updating to look at the wrong thing.
148    pub fn http_status(&self) -> u16 {
149        match self {
150            Error::Bundle(error) => error.http_status(),
151            Error::Storage(_) | Error::VerifyFailed => 500,
152        }
153    }
154}
155
156impl<E> From<bundle::Error> for Error<E> {
157    fn from(error: bundle::Error) -> Error<E> {
158        Error::Bundle(error)
159    }
160}
161
162impl<E> From<resident_fat::Error<E>> for Error<E> {
163    fn from(error: resident_fat::Error<E>) -> Error<E> {
164        Error::Storage(error)
165    }
166}
167
168impl<E: core::fmt::Debug> core::fmt::Display for Error<E> {
169    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
170        match self {
171            Error::Bundle(error) => write!(f, "{error}"),
172            Error::Storage(error) => write!(f, "{error}"),
173            Error::VerifyFailed => f.write_str("write verification failed"),
174        }
175    }
176}
177
178impl<E: core::fmt::Debug> core::error::Error for Error<E> {}
179
180/// Installs `bundle` onto `volume`.
181///
182/// Does **not** reboot. The caller answers whoever sent the bundle first,
183/// because a failure that nobody is told about is worse than a slow one.
184///
185/// # Order
186///
187/// Entries are written in role order rather than the order they were
188/// packed, and the crate imposes it rather than trusting the bundle — the
189/// same reasoning that made the kernel a role instead of a position:
190///
191/// 1. [`Role::File`] — assets, settings, blobs. Nothing here is loaded by
192///    the firmware, so a failure leaves a board that still boots what it
193///    booted last time.
194/// 2. [`Role::Firmware`] — `start*.elf`, `fixup*.dat`, `bootcode.bin`.
195/// 3. [`Role::Config`] — `config.txt`, which the firmware does read, but
196///    which cannot on its own stop a board booting: lose it and the
197///    firmware falls back to loading `kernel7.img`/`kernel8.img`.
198/// 4. [`Role::Kernel`] — last, because with one boot image this write **is**
199///    the commit: the board boots whatever it leaves behind.
200///
201/// Steps 3 and 4 swap once there are two kernel slots to write to. The
202/// kernel then goes to whichever is not running — which is no longer the
203/// live image, and no longer the commit — and rewriting `config.txt`'s
204/// `kernel=` line takes both jobs over.
205///
206/// # `bootcode.bin` has no safety net
207///
208/// The ROM loads it by name, so there is no slot to write it to and no
209/// `config.txt` line to point elsewhere: a failed write there means the
210/// card comes out. Three things keep that acceptable rather than merely
211/// unavoidable — it is about 50 KB, it changes almost never, and on the
212/// Pi 4 it lives in SPI EEPROM and is not on the card at all. Every other
213/// boot file has a way out.
214pub fn apply<D, P>(
215    volume: &mut FileSystem<D>,
216    format: &Format,
217    bundle: &[u8],
218    progress: &mut P,
219) -> Result<Report, Error<D::Error>>
220where
221    D: BlockDevice,
222    P: Progress,
223{
224    // Every check that is a statement about the bundle happens here, before
225    // a byte reaches the card.
226    let parsed = Bundle::parse(format, bundle)?;
227    let installed = install(volume, &parsed, progress);
228
229    // Unconditional, and outside `install` so a failure syncs too. The
230    // allocation table lives in memory, so nothing written above has
231    // necessarily reached the card in full; what a failed install did manage
232    // to write is on the card either way, and leaving the table behind is
233    // the one thing that turns a half-finished update into a corrupt volume.
234    let synced = volume.sync();
235    installed.and_then(|report| synced.map(|()| report).map_err(Error::from))
236}
237
238/// The install itself, so [`apply`] can sync after it either way.
239fn install<D, P>(
240    volume: &mut FileSystem<D>,
241    bundle: &Bundle<'_>,
242    progress: &mut P,
243) -> Result<Report, Error<D::Error>>
244where
245    D: BlockDevice,
246    P: Progress,
247{
248    let mut scratch = alloc::vec![0u8; SCRATCH];
249    let mut report = Report::default();
250
251    for role in [Role::File, Role::Firmware, Role::Config] {
252        for entry in bundle.iter().filter(|entry| entry.role == role) {
253            record(
254                &mut report,
255                install_entry(volume, entry, progress, &mut scratch)?,
256            );
257        }
258    }
259
260    if let Some(entry) = bundle.kernel() {
261        report.kernel_len = Some(entry.data.len() as u32);
262        record(
263            &mut report,
264            install_entry(volume, entry, progress, &mut scratch)?,
265        );
266    }
267
268    Ok(report)
269}
270
271/// Counts one entry as written or skipped.
272fn record(report: &mut Report, written: bool) {
273    if written {
274        report.written += 1;
275    } else {
276        report.skipped += 1;
277    }
278}
279
280/// Puts one entry where its path says. Returns whether it had to be written.
281fn install_entry<D, P>(
282    volume: &mut FileSystem<D>,
283    entry: Entry<'_>,
284    progress: &mut P,
285    scratch: &mut [u8],
286) -> Result<bool, Error<D::Error>>
287where
288    D: BlockDevice,
289    P: Progress,
290{
291    progress.starting(entry);
292
293    // Asked before writing, because the answer is often yes and a read costs
294    // a fraction of a write. A bundle that carries the Raspberry Pi firmware
295    // carries about 3 MB of it, and that changes roughly once a year.
296    if on_card(volume, entry.path, entry.data, scratch)? {
297        progress.skipped(entry);
298        return Ok(false);
299    }
300
301    create_parents(volume, entry.path)?;
302    // One call, and that is the point: the length is known before a byte is
303    // written, so the whole cluster chain is allocated at once and the file
304    // comes out contiguous. A file grown a write at a time gets whatever the
305    // allocator had spare each time, which is how a transfer ends up as one
306    // device command per fragment.
307    volume.write_file(entry.path, entry.data)?;
308    progress.wrote(entry);
309
310    // The read-back is the point of the whole exercise: a write the card
311    // accepted but did not durably store produces a board that will not
312    // boot, discovered on the reboot that was supposed to complete the
313    // update. No `sync` first -- what this crate keeps in memory is the
314    // allocation table and the directories, and file data goes to the card
315    // inside `write_file`, so a read comes off the card either way.
316    if !on_card(volume, entry.path, entry.data, scratch)? {
317        return Err(Error::VerifyFailed);
318    }
319    progress.verified(entry);
320    Ok(true)
321}
322
323/// Whether the card already holds exactly `data` at `path`.
324///
325/// Used for both halves of the same question: before a write it decides
326/// whether to write at all, and after one it is the verification. Sharing
327/// the code is not only tidiness — it means a skipped entry has been
328/// checked exactly as strictly as a written one.
329///
330/// The comparison is a checksum rather than the byte-for-byte compare an
331/// in-memory copy would allow. That is a real if small weakening, and it
332/// buys the ability to check a file of any size against a fixed scratch
333/// buffer: the alternative is holding a 3 MB read-back beside the 3 MB
334/// already in the bundle.
335fn on_card<D>(
336    volume: &mut FileSystem<D>,
337    path: &str,
338    data: &[u8],
339    scratch: &mut [u8],
340) -> Result<bool, Error<D::Error>>
341where
342    D: BlockDevice,
343{
344    let file = match volume.open(path) {
345        Ok(file) => file,
346        // Absent is an answer, not a failure. Any other error is the card
347        // and must not be reported as "different".
348        Err(resident_fat::Error::NotFound { .. }) => return Ok(false),
349        Err(error) => return Err(Error::Storage(error)),
350    };
351
352    // Length first, because it is free and settles most of the cases a
353    // checksum would have to read a whole file to settle.
354    if file.len() as usize != data.len() {
355        return Ok(false);
356    }
357
358    let mut checksum = Checksum::new();
359    let mut at = 0u64;
360    while at < file.len() as u64 {
361        let read = volume.read_at(&file, at, scratch)?;
362        if read == 0 {
363            // Short of the length the directory entry claims. Whatever that
364            // is, it is not the file being asked about.
365            return Ok(false);
366        }
367        checksum.update(&scratch[..read]);
368        at += read as u64;
369    }
370
371    Ok(checksum.finish() == bundle::checksum(data))
372}
373
374/// Creates every directory above `path` that is not already there.
375///
376/// A path in a bundle can be nested, and `write_file` resolves a parent
377/// rather than making one, so a first update onto a card that has never held
378/// the directory would otherwise fail at the first asset. One level at a
379/// time, because that is what `create_dir` does.
380fn create_parents<D>(volume: &mut FileSystem<D>, path: &str) -> Result<(), Error<D::Error>>
381where
382    D: BlockDevice,
383{
384    let mut at = 0;
385    while let Some(offset) = path[at..].find('/') {
386        let end = at + offset;
387        let directory = &path[..end];
388        if volume.open_dir(directory).is_err() {
389            volume.create_dir(directory)?;
390        }
391        at = end + 1;
392    }
393    Ok(())
394}