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}