|
| 1 | +# SPI Flash Write Protection with flashprog |
| 2 | + |
| 3 | +This document covers how to inspect and configure SPI flash write protection on |
| 4 | +heads-supported hardware using the `flashprog wp` subcommands. |
| 5 | + |
| 6 | +## Background |
| 7 | + |
| 8 | +Write protection prevents firmware regions from being overwritten, forming the |
| 9 | +hardware basis of a Static Root of Trust (SRTM). On Intel PCH platforms |
| 10 | +(Skylake and later), write protection is enforced by **Protected Range Registers |
| 11 | +(PRRs)** in the PCH SPI BAR rather than by the flash chip's own STATUS register. |
| 12 | +This matters because: |
| 13 | + |
| 14 | +- On PCH100+ (Meteor Lake etc.) the flash is accessed via hardware sequencing |
| 15 | + (hwseq); the chip's STATUS register is not directly addressable with standard |
| 16 | + SPI opcodes. |
| 17 | +- Protection is only meaningful when the SPI configuration is locked |
| 18 | + (`FLOCKDN`). coreboot pre-programs PRR0 with `WP=1` as preparation for the |
| 19 | + kexec lockdown, but that bit is cleared by flashprog on every init when |
| 20 | + `FLOCKDN=0`. Until `lock_chip` is called (just before kexec), write |
| 21 | + protection is **not enforced** regardless of what the PRR registers show. |
| 22 | + |
| 23 | +The patched heads flashprog correctly accounts for this: `wp status` reports |
| 24 | +`disabled` when `FLOCKDN=0` and `hardware` only when `FLOCKDN=1` and at least |
| 25 | +one PRR has `WP=1` with a non-empty range. |
| 26 | + |
| 27 | +## Programmer Options |
| 28 | + |
| 29 | +All heads boards use `--programmer internal`. The `wp` subcommands do **not** |
| 30 | +accept layout flags (`--ifd`, `--image`, `-i`); those must be omitted. |
| 31 | + |
| 32 | +If `CONFIG_FLASH_OPTIONS` is set in the environment, the `wp-test` and |
| 33 | +`wp-debug` scripts strip layout flags automatically. |
| 34 | + |
| 35 | +## Commands |
| 36 | + |
| 37 | +### Check current protection status |
| 38 | + |
| 39 | +```sh |
| 40 | +flashprog wp status --programmer internal |
| 41 | +``` |
| 42 | + |
| 43 | +**Before `lock_chip`** (FLOCKDN=0 — heads runtime, pre-kexec): |
| 44 | + |
| 45 | +```text |
| 46 | +Protection range: start=0x00000000 length=0x00000000 (none) |
| 47 | +Protection mode: disabled |
| 48 | +``` |
| 49 | + |
| 50 | +coreboot has already written `WP=1` to PRR0, but `ichspi_lock` (FLOCKDN) is |
| 51 | +not yet set. flashprog clears the WP bit during init, so the PRR is not |
| 52 | +enforced and `wp status` correctly reports `disabled`. |
| 53 | + |
| 54 | +**After `lock_chip`** (FLOCKDN=1 — SPI configuration locked, kexec imminent): |
| 55 | + |
| 56 | +```text |
| 57 | +Protection range: start=0x00000000 length=0x02000000 (all) |
| 58 | +Protection mode: hardware |
| 59 | +``` |
| 60 | + |
| 61 | +FLOCKDN is set; `ich9_set_pr` cannot clear WP bits. PRR0 covers the full |
| 62 | +32 MB chip (base=0x00000, limit=0x01fff in 4 KB units → 0x00000000–0x01ffffff). |
| 63 | +flashprog also emits a warning at init time: |
| 64 | + |
| 65 | +```text |
| 66 | +SPI Configuration is locked down. |
| 67 | +PR0: Warning: 0x00000000-0x01ffffff is read-only. |
| 68 | +At least some flash regions are write protected. For write operations, |
| 69 | +you should use a flash layout and include only writable regions. See |
| 70 | +manpage for more details. |
| 71 | +``` |
| 72 | + |
| 73 | +Add `--verbose` to see individual PRR register values, FLOCKDN state, DLOCK, |
| 74 | +and all FREG entries: |
| 75 | + |
| 76 | +```sh |
| 77 | +flashprog wp status --programmer internal --verbose |
| 78 | +``` |
| 79 | + |
| 80 | +Before `lock_chip` the verbose output includes: |
| 81 | + |
| 82 | +```text |
| 83 | +HSFS: FDONE=0, FCERR=0, AEL=0, SCIP=0, PRR34_LOCKDN=0, WRSDIS=0, FDOPSS=1, FDV=1, FLOCKDN=0 |
| 84 | +DLOCK: BMWAG_LOCKDN=0, BMRAG_LOCKDN=0, SBMWAG_LOCKDN=0, SBMRAG_LOCKDN=0, |
| 85 | + PR0_LOCKDN=0, PR1_LOCKDN=0, PR2_LOCKDN=0, PR3_LOCKDN=0, PR4_LOCKDN=0, |
| 86 | + SSEQ_LOCKDN=0 |
| 87 | +ich_hwseq_wp_read_cfg: FLOCKDN not set, PRR protection not enforced |
| 88 | +``` |
| 89 | + |
| 90 | +After `lock_chip`: |
| 91 | + |
| 92 | +```text |
| 93 | +HSFS: FDONE=0, FCERR=0, AEL=0, SCIP=0, PRR34_LOCKDN=1, WRSDIS=1, FDOPSS=1, FDV=1, FLOCKDN=1 |
| 94 | +DLOCK: BMWAG_LOCKDN=0, BMRAG_LOCKDN=0, SBMWAG_LOCKDN=0, SBMRAG_LOCKDN=0, |
| 95 | + PR0_LOCKDN=1, PR1_LOCKDN=1, PR2_LOCKDN=1, PR3_LOCKDN=1, PR4_LOCKDN=1, |
| 96 | + SSEQ_LOCKDN=0 |
| 97 | +PRR0: 0x9fff0000 (WP=1 RP=0 base=0x00000 limit=0x01fff) |
| 98 | +PRR1: 0x00000000 (WP=0 RP=0 base=0x00000 limit=0x00000) |
| 99 | +PRR2: 0x00000000 (WP=0 RP=0 base=0x00000 limit=0x00000) |
| 100 | +PRR3: 0x00000000 (WP=0 RP=0 base=0x00000 limit=0x00000) |
| 101 | +PRR4: 0x00000000 (WP=0 RP=0 base=0x00000 limit=0x00000) |
| 102 | +PRR5: 0x00000000 (WP=0 RP=0 base=0x00000 limit=0x00000) |
| 103 | +``` |
| 104 | + |
| 105 | +`DLOCK.PR0_LOCKDN=1` through `PR4_LOCKDN=1` means the PRR registers themselves |
| 106 | +are frozen; even writing 0 to them fails. |
| 107 | + |
| 108 | +### List available protection ranges |
| 109 | + |
| 110 | +```sh |
| 111 | +flashprog wp list --programmer internal |
| 112 | +``` |
| 113 | + |
| 114 | +Returns the no-protection entry, power-of-2 top-aligned fractions from 4 KB up |
| 115 | +to half the chip, and full-chip protection. On a 32 MB chip: |
| 116 | + |
| 117 | +```text |
| 118 | +Available protection ranges: |
| 119 | + start=0x00000000 length=0x00000000 (none) |
| 120 | + start=0x01fff000 length=0x00001000 (upper 1/8192) |
| 121 | + start=0x01ffe000 length=0x00002000 (upper 1/4096) |
| 122 | + start=0x01ffc000 length=0x00004000 (upper 1/2048) |
| 123 | + start=0x01ff8000 length=0x00008000 (upper 1/1024) |
| 124 | + start=0x01ff0000 length=0x00010000 (upper 1/512) |
| 125 | + start=0x01fe0000 length=0x00020000 (upper 1/256) |
| 126 | + start=0x01fc0000 length=0x00040000 (upper 1/128) |
| 127 | + start=0x01f80000 length=0x00080000 (upper 1/64) |
| 128 | + start=0x01f00000 length=0x00100000 (upper 1/32) |
| 129 | + start=0x01e00000 length=0x00200000 (upper 1/16) |
| 130 | + start=0x01c00000 length=0x00400000 (upper 1/8) |
| 131 | + start=0x01800000 length=0x00800000 (upper 1/4) |
| 132 | + start=0x01000000 length=0x01000000 (upper 1/2) |
| 133 | + start=0x00000000 length=0x02000000 (all) |
| 134 | +``` |
| 135 | + |
| 136 | +`wp list` works in both locked and unlocked states. |
| 137 | + |
| 138 | +### Disable write protection |
| 139 | + |
| 140 | +```sh |
| 141 | +flashprog wp disable --programmer internal |
| 142 | +``` |
| 143 | + |
| 144 | +Clears the `WP` bit on all writable PRR registers. Returns exit code 0 on |
| 145 | +success: |
| 146 | + |
| 147 | +```text |
| 148 | +Disabled hardware protection |
| 149 | +``` |
| 150 | + |
| 151 | +If `FLOCKDN=1`, the registers are frozen and the command fails: |
| 152 | + |
| 153 | +```text |
| 154 | +ich_hwseq_wp_write_cfg: SPI configuration is locked (FLOCKDN); cannot modify protected ranges |
| 155 | +Failed to apply new WP settings: failed to write the new WP configuration |
| 156 | +``` |
| 157 | + |
| 158 | +### Set a protection range and enable |
| 159 | + |
| 160 | +Set the range first, then enable: |
| 161 | + |
| 162 | +```sh |
| 163 | +# Protect the top 4 MB of a 32 MB chip |
| 164 | +flashprog wp range --programmer internal 0x1c00000,0x400000 |
| 165 | +flashprog wp enable --programmer internal |
| 166 | +``` |
| 167 | + |
| 168 | +`wp range` encodes the address and length into PRR0. `wp enable` sets the `WP` |
| 169 | +bit. Both commands require 4 KB-aligned start and length values. On success: |
| 170 | + |
| 171 | +```text |
| 172 | +Configured protection range: start=0x01c00000 length=0x00400000 (upper 1/8) |
| 173 | +``` |
| 174 | + |
| 175 | +```text |
| 176 | +Enabled hardware protection |
| 177 | +``` |
| 178 | + |
| 179 | +If `FLOCKDN=1`, both commands fail with the same locked-down error as `wp disable`. |
| 180 | + |
| 181 | +**Persistence note:** When `FLOCKDN=0` (heads runtime before kexec), the PRR |
| 182 | +write takes effect for the current flashprog session but is not persistent. On |
| 183 | +the next invocation, `ich9_set_pr` clears the WP bit again because FLOCKDN is |
| 184 | +not set. Persistent, hardware-enforced protection is only active after |
| 185 | +`lock_chip` sets `FLOCKDN=1`. |
| 186 | + |
| 187 | +## Pre-flash WP check in heads |
| 188 | + |
| 189 | +Before writing firmware, heads checks whether the target region is protected. |
| 190 | +If `wp status` reports `hardware` mode with a range that overlaps the write |
| 191 | +target, the flash operation is refused. This guards against accidentally |
| 192 | +overwriting a PRR-protected area on a system where `lock_chip` has already run |
| 193 | +(post-kexec or externally locked). |
| 194 | + |
| 195 | +## Testing tools |
| 196 | + |
| 197 | +Two shell scripts under `initrd/tests/wp/` are provided for hardware validation. |
| 198 | + |
| 199 | +### wp-test |
| 200 | + |
| 201 | +Runs a sequence of functional tests and prints `PASS`/`FAIL`/`SKIP` per test: |
| 202 | + |
| 203 | +```sh |
| 204 | +initrd/tests/wp/wp-test [flashprog-programmer-opts] |
| 205 | +``` |
| 206 | + |
| 207 | +With no arguments the script uses `CONFIG_FLASH_OPTIONS` from the environment |
| 208 | +(stripping layout flags), or falls back to `--programmer internal`. |
| 209 | + |
| 210 | +Tests performed: |
| 211 | + |
| 212 | +| # | Description | |
| 213 | +| --- | --- | |
| 214 | +| 1 | `wp status` exits with code 0 | |
| 215 | +| 2 | `wp status` output contains a `Protection mode:` field | |
| 216 | +| 3 | `wp list` exits with code 0 | |
| 217 | +| 4 | `wp list` returns more than 2 ranges | |
| 218 | +| 5 | `FLOCKDN` state detected via verbose output | |
| 219 | +| 6 | `wp status` mode matches `FLOCKDN` state (disabled when unlocked) | |
| 220 | +| 7 | `wp disable` exits code 0 — skipped if `FLOCKDN=1` | |
| 221 | +| 8 | `wp range` + `wp enable` exit code 0 — skipped if `FLOCKDN=1` | |
| 222 | + |
| 223 | +**Expected results before `lock_chip`** (novacustom-v560tu, Meteor Lake, FLOCKDN=0): |
| 224 | + |
| 225 | +```text |
| 226 | +Results: PASS=8 FAIL=0 SKIP=0 |
| 227 | +``` |
| 228 | + |
| 229 | +Tests 7 and 8 pass because PRR registers are writable when FLOCKDN=0. |
| 230 | + |
| 231 | +**Expected results after `lock_chip`** (same hardware, FLOCKDN=1): |
| 232 | + |
| 233 | +```text |
| 234 | +Results: PASS=6 FAIL=0 SKIP=2 |
| 235 | +``` |
| 236 | + |
| 237 | +Tests 7 and 8 are skipped because FLOCKDN=1 freezes the PRR registers. The |
| 238 | +two skips are not failures: the hardware is operating correctly. |
| 239 | + |
| 240 | +### wp-debug |
| 241 | + |
| 242 | +Collects diagnostic output for analysis or bug reports: |
| 243 | + |
| 244 | +```sh |
| 245 | +initrd/tests/wp/wp-debug [flashprog-programmer-opts] |
| 246 | +``` |
| 247 | + |
| 248 | +Runs `wp status`, `wp list`, `wp status --verbose`, reads the PCH SPI BAR base |
| 249 | +via `setpci` (if available), dumps `/proc/mtd`, and filters relevant `dmesg` |
| 250 | +lines. Paste the output when filing a flash write protection issue. |
| 251 | + |
| 252 | +## PCH100+ notes (Meteor Lake and newer) |
| 253 | + |
| 254 | +On PCH100+ the SPI BAR layout changed: |
| 255 | + |
| 256 | +- PRR registers start at offset `0x84` (`PCH100_REG_FPR0`). |
| 257 | +- There are 6 registers (PRR0–PRR5); the last (`GPR0`/PRR5) is chipset-controlled |
| 258 | + and is not written by flashprog. |
| 259 | +- After `lock_chip`, `DLOCK` bits `PR0_LOCKDN` through `PR4_LOCKDN` are all set, |
| 260 | + freezing the five OS-accessible PRRs. |
| 261 | +- The chip is fully opaque (hardware sequencer only); there is no direct SPI |
| 262 | + STATUS register access via software sequencing. |
| 263 | + |
| 264 | +The patched flashprog adds `read_register` / `write_register` hooks to the hwseq |
| 265 | +opaque master so that STATUS register reads/writes go through hardware sequencer |
| 266 | +cycle types 8 (RD_STATUS) and 7 (WR_STATUS). The `wp_read_cfg`, |
| 267 | +`wp_write_cfg`, and `wp_get_ranges` hooks implement PRR-based write protection. |
| 268 | + |
| 269 | +## Credits |
| 270 | + |
| 271 | +Write-protection infrastructure for opaque/hwseq programmers (patches 0100, |
| 272 | +0300, 0400, and the STATUS register read/write functions in 0200) was developed |
| 273 | +by the Dasharo/3mdeb team (SergiiDmytruk, Pokisiekk, macpijan, krystian-hebel |
| 274 | +and others) and upstreamed to flashrom. These patches backport that work to |
| 275 | +flashprog 1.5, adapting for API differences between the two projects. |
| 276 | + |
| 277 | +The PRR-based WP functions (`ich_hwseq_wp_read_cfg`, `ich_hwseq_wp_write_cfg`, |
| 278 | +`ich_hwseq_wp_get_ranges`) and the FLOCKDN-aware enforcement logic are original |
| 279 | +heads contributions. |
| 280 | + |
| 281 | +- Dasharo WP work tracking: <https://github.com/linuxboot/heads/issues/1741> |
| 282 | +- Upstream flashrom review: <https://review.coreboot.org/c/flashrom/+/68179> |
| 283 | +- Dasharo flashrom fork: <https://github.com/Dasharo/flashrom> |
| 284 | +- Upstream flashrom: <https://github.com/flashrom/flashrom> |
| 285 | + |
| 286 | +## Reference |
| 287 | + |
| 288 | +- `patches/flashprog-*/0100-opaque-master-wp-callbacks.patch` — opaque_master struct extension (backport) |
| 289 | +- `patches/flashprog-*/0200-ichspi-hwseq-status-register-rw.patch` — hwseq STATUS r/w (backport) + PRR WP (original) |
| 290 | +- `patches/flashprog-*/0300-writeprotect-bus-prog-dispatch.patch` — BUS_PROG WP dispatch (backport) |
| 291 | +- `patches/flashprog-*/0400-libflashprog-opaque-wp-dispatch.patch` — opaque WP dispatch (backport) |
| 292 | +- Intel PCH SPI Programming Guide, chapter "Protected Range Registers" |
0 commit comments