Skip to content

Commit 9dd69c8

Browse files
committed
doc: add flashprog WP usage guide
Documents the 'flashprog wp' subcommands for heads-supported Intel PCH hardware: wp status, wp list, wp disable, wp range + wp enable. Explains the FLOCKDN/lock_chip lifecycle and PCH100+ PRR mechanics. Includes real hardware output from novacustom-v560tu before and after lock_chip, and documents the wp-test and wp-debug scripts with expected results. Credits the Dasharo/3mdeb team for the upstream flashrom WP infrastructure that these patches backport. Signed-off-by: Thierry Laurion <insurgo@riseup.net>
1 parent 8f9dcb0 commit 9dd69c8

File tree

1 file changed

+292
-0
lines changed

1 file changed

+292
-0
lines changed

doc/flashprog-wp.md

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
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

Comments
 (0)