Skip to content

Commit 3284a4a

Browse files
committed
patches/flashprog: add WP support for internal Intel SPI (opaque) programmer
Enables all 'flashprog wp' subcommands on PCH100+ chipsets (Meteor Lake etc.) where flash is exposed via hardware sequencing (hwseq/opaque master) and the chip's STATUS register is not directly addressable. Patches 0100, 0300, 0400 and the ich_hwseq_read_status / ich_hwseq_write_status functions in 0200 are backported from upstream flashrom (https://github.com/flashrom/flashrom), where this infrastructure was contributed by the Dasharo/3mdeb team (SergiiDmytruk, Pokisiekk, macpijan, krystian-hebel and others). See: https://review.coreboot.org/c/flashrom/+/68179 #1741 The PRR-based WP functions in 0200 (ich_hwseq_wp_read_cfg, ich_hwseq_wp_write_cfg, ich_hwseq_wp_get_ranges) are original heads work and are not present in upstream flashrom or Dasharo's fork. Protection is enforced only when FLOCKDN is set at lock_chip/kexec time. coreboot pre-programs PRR0 with WP=1 as preparation, but ich9_set_pr() clears those bits when FLOCKDN=0, so protection is not actually enforced during heads execution. wp status correctly reports 'disabled' in this state and 'hardware' only when FLOCKDN=1 with active PRR WP bits. Four patches applied in order to flashprog 1.5: 0100 - include/programmer.h: add read_register, write_register, wp_read_cfg, wp_write_cfg, wp_get_ranges callbacks to struct opaque_master so programmers can provide WP operations directly. [backport] 0200 - ichspi.c: ich_hwseq_read_status / ich_hwseq_write_status for STATUS register access via hwseq cycle types 8/7 [backport]; plus ich_hwseq_wp_read_cfg (FLOCKDN-aware PRR read), ich_hwseq_wp_write_cfg (PRR encode + WP bit), ich_hwseq_wp_get_ranges (power-of-2 top fractions). All hooks wired into opaque_master_ich_hwseq. [original] 0300 - writeprotect.c, include/writeprotect.h: dispatch register reads/writes through opaque master callbacks on BUS_PROG chips. Add and export wp_operations_available(). [backport] 0400 - libflashprog.c: try programmer-level WP override before chip-level and generic SPI paths. Previously all WP calls returned CHIP_UNSUPPORTED for opaque chips. [backport] Tested on novacustom-v560tu (Intel Meteor Lake, PCH100+): Before lock_chip (FLOCKDN=0): wp status=disabled; PASS=8 FAIL=0 SKIP=0 After lock_chip (FLOCKDN=1): wp status=hardware; PASS=6 FAIL=0 SKIP=2 Signed-off-by: Thierry Laurion <insurgo@riseup.net>
1 parent 7e967be commit 3284a4a

4 files changed

Lines changed: 525 additions & 0 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Backported from upstream flashrom (https://github.com/flashrom/flashrom),
2+
# where this infrastructure was contributed by the Dasharo team to enable
3+
# write-protect operations on opaque (hardware-sequenced) programmers.
4+
# Adapted for flashprog 1.5 (flashprog_wp_result / flashprog_wp_cfg types).
5+
--- a/include/programmer.h 2026-02-13 13:14:40.000000000 -0500
6+
+++ b/include/programmer.h 2026-03-18 17:09:05.983978890 -0400
7+
@@ -421,6 +421,17 @@
8+
int (*read) (struct flashctx *flash, uint8_t *buf, unsigned int start, unsigned int len);
9+
int (*write) (struct flashctx *flash, const uint8_t *buf, unsigned int start, unsigned int len);
10+
int (*erase) (struct flashctx *flash, unsigned int blockaddr, unsigned int blocklen);
11+
+ /*
12+
+ * Callbacks for accessing flash registers. An opaque programmer must
13+
+ * provide these for writeprotect operations to be available, unless it
14+
+ * provides custom wp operations instead.
15+
+ */
16+
+ int (*read_register)(const struct flashctx *flash, enum flash_reg reg, uint8_t *value);
17+
+ int (*write_register)(const struct flashctx *flash, enum flash_reg reg, uint8_t value);
18+
+ /* Optional overrides for default writeprotect operations. */
19+
+ enum flashprog_wp_result (*wp_write_cfg)(struct flashctx *, const struct flashprog_wp_cfg *);
20+
+ enum flashprog_wp_result (*wp_read_cfg)(struct flashprog_wp_cfg *, struct flashctx *);
21+
+ enum flashprog_wp_result (*wp_get_ranges)(struct flashprog_wp_ranges **, struct flashctx *);
22+
int (*shutdown)(void *data);
23+
void *data;
24+
};
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
# ich_hwseq_read_status / ich_hwseq_write_status: backported from upstream
2+
# flashrom (https://github.com/flashrom/flashrom), originally contributed by
3+
# the Dasharo/3mdeb team (SergiiDmytruk et al.) for Meteor Lake hwseq support.
4+
# See: https://review.coreboot.org/c/flashrom/+/68179
5+
# https://github.com/linuxboot/heads/issues/1741
6+
# Adapted for flashprog 1.5 (direct REGREAD/REGWRITE instead of
7+
# ich_exec_sync_hwseq_xfer; flashprog_wp_result types).
8+
#
9+
# ich_hwseq_wp_read_cfg / ich_hwseq_wp_write_cfg / ich_hwseq_wp_get_ranges:
10+
# original heads work. PRR-based, FLOCKDN-aware WP for PCH100+ opaque chips.
11+
# Not present in upstream flashrom or Dasharo's fork.
12+
--- a/ichspi.c 2026-02-13 13:14:40.000000000 -0500
13+
+++ b/ichspi.c 2026-03-19 16:10:21.470110288 -0400
14+
@@ -1177,6 +1177,8 @@
15+
uint32_t addr_mask;
16+
bool only_4k;
17+
uint32_t hsfc_fcycle;
18+
+ size_t reg_pr0; /* offset of PR0 in ich_spibar; needed by wp_read_cfg */
19+
+ unsigned int num_pr; /* number of PR registers */
20+
} hwseq_data;
21+
22+
/* Sets FLA in FADDR to (addr & hwseq_data.addr_mask) without touching other bits. */
23+
@@ -1688,13 +1690,302 @@
24+
.probe_opcode = ich_spi_probe_opcode,
25+
};
26+
27+
+/*
28+
+ * Read write protection status from the PCH's Protected Range Registers (PRR).
29+
+ *
30+
+ * On PCH100+ (Meteor Lake etc.) the flash chip is exposed as an opaque master
31+
+ * via hardware sequencing, so SPI STATUS register-based WP decoding is not
32+
+ * possible without knowing the chip's bit layout. Instead, we read the
33+
+ * hardware-enforced PRR registers directly from the SPI BAR.
34+
+ *
35+
+ * Protection is only reported as active when FLOCKDN (ichspi_lock) is set.
36+
+ * When FLOCKDN=0, ich9_set_pr() already cleared the WP bits so they are
37+
+ * not actually enforced; coreboot pre-programs PRRs with WP=1 as preparation
38+
+ * for the kexec lockdown, but until FLOCKDN is set those bits do not prevent
39+
+ * writes. Reading them as "hardware" mode would be misleading.
40+
+ *
41+
+ * Multiple active PRRs are merged into a single bounding range.
42+
+ */
43+
+static enum flashprog_wp_result ich_hwseq_wp_read_cfg(struct flashprog_wp_cfg *cfg,
44+
+ struct flashctx *flash)
45+
+{
46+
+ unsigned int i;
47+
+ uint32_t range_start = UINT32_MAX;
48+
+ uint32_t range_end = 0;
49+
+ bool wp_active = false;
50+
+
51+
+ /* When FLOCKDN is not set, ich9_set_pr() already cleared all WP bits.
52+
+ * PRR writes are not enforced; report disabled immediately. */
53+
+ if (!ichspi_lock) {
54+
+ cfg->mode = FLASHPROG_WP_MODE_DISABLED;
55+
+ cfg->range.start = 0;
56+
+ cfg->range.len = 0;
57+
+ msg_pdbg("%s: FLOCKDN not set, PRR protection not enforced\n", __func__);
58+
+ return FLASHPROG_WP_OK;
59+
+ }
60+
+
61+
+ /* FLOCKDN is set: read live PRR values. ich9_set_pr() could not clear
62+
+ * them, so whatever WP bits remain are truly hardware-enforced. */
63+
+ for (i = 0; i < hwseq_data.num_pr; i++) {
64+
+ uint32_t pr = mmio_readl(ich_spibar + hwseq_data.reg_pr0 + i * 4);
65+
+
66+
+ msg_pdbg("PRR%u: 0x%08x (WP=%u RP=%u base=0x%05x limit=0x%05x)\n",
67+
+ i, pr,
68+
+ (pr >> PR_WP_OFF) & 1,
69+
+ (pr >> PR_RP_OFF) & 1,
70+
+ ICH_FREG_BASE(pr) >> 12,
71+
+ ICH_FREG_LIMIT(pr) >> 12);
72+
+
73+
+ if (!(pr & (1u << PR_WP_OFF)))
74+
+ continue;
75+
+
76+
+ uint32_t base = ICH_FREG_BASE(pr);
77+
+ uint32_t limit = ICH_FREG_LIMIT(pr);
78+
+
79+
+ if (limit < base)
80+
+ continue;
81+
+
82+
+ wp_active = true;
83+
+ if (base < range_start)
84+
+ range_start = base;
85+
+ if (limit > range_end)
86+
+ range_end = limit;
87+
+ }
88+
+
89+
+ if (!wp_active) {
90+
+ cfg->mode = FLASHPROG_WP_MODE_DISABLED;
91+
+ cfg->range.start = 0;
92+
+ cfg->range.len = 0;
93+
+ } else {
94+
+ cfg->mode = FLASHPROG_WP_MODE_HARDWARE;
95+
+ cfg->range.start = range_start;
96+
+ cfg->range.len = (size_t)range_end - range_start + 1;
97+
+ }
98+
+
99+
+ return FLASHPROG_WP_OK;
100+
+}
101+
+
102+
+/*
103+
+ * Write WP configuration via PCH Protected Range Registers.
104+
+ *
105+
+ * DISABLE: clears WP bit on all PRR registers.
106+
+ * HARDWARE: encodes the requested range into the first free PRR slot.
107+
+ * The range must be 4KB-aligned. Fails if PRRs are locked (FLOCKDN set)
108+
+ * or no free slot is available.
109+
+ *
110+
+ * Other modes (POWER_CYCLE, PERMANENT) are not supported via PRR.
111+
+ */
112+
+static enum flashprog_wp_result ich_hwseq_wp_write_cfg(struct flashctx *flash,
113+
+ const struct flashprog_wp_cfg *cfg)
114+
+{
115+
+ unsigned int i;
116+
+
117+
+ if (ichspi_lock) {
118+
+ msg_perr("%s: SPI configuration is locked (FLOCKDN); "
119+
+ "cannot modify protected ranges\n", __func__);
120+
+ return FLASHPROG_WP_ERR_WRITE_FAILED;
121+
+ }
122+
+
123+
+ if (cfg->mode == FLASHPROG_WP_MODE_DISABLED) {
124+
+ /* Clear WP bit on all writable PRR registers.
125+
+ * GPR0 (last slot on PCH100+) is chipset-controlled; skip it. */
126+
+ for (i = 0; i < hwseq_data.num_pr; i++) {
127+
+ if (ich_generation >= SPI_ENGINE_PCH100 &&
128+
+ i == hwseq_data.num_pr - 1)
129+
+ continue; /* GPR0: not OS-writable */
130+
+ msg_pdbg("Clearing PRR%u\n", i);
131+
+ mmio_writel(0, ich_spibar + hwseq_data.reg_pr0 + i * 4);
132+
+ }
133+
+ return FLASHPROG_WP_OK;
134+
+ }
135+
+
136+
+ if (cfg->mode != FLASHPROG_WP_MODE_HARDWARE)
137+
+ return FLASHPROG_WP_ERR_MODE_UNSUPPORTED;
138+
+
139+
+ /* Validate alignment: PRRs work at 4KB granularity */
140+
+ if ((cfg->range.start & 0xFFF) || (cfg->range.len & 0xFFF)) {
141+
+ msg_perr("%s: range start/length must be 4KB-aligned "
142+
+ "(start=0x%zx len=0x%zx)\n",
143+
+ __func__, cfg->range.start, cfg->range.len);
144+
+ return FLASHPROG_WP_ERR_RANGE_UNSUPPORTED;
145+
+ }
146+
+
147+
+ uint32_t base_4k = (uint32_t)(cfg->range.start >> 12) & 0x7FFF;
148+
+ uint32_t limit_4k = (uint32_t)((cfg->range.start + cfg->range.len - 1) >> 12) & 0x7FFF;
149+
+
150+
+ /* Encode: bits[14:0]=base, bit[15]=RP=0, bits[30:16]=limit, bit[31]=WP */
151+
+ uint32_t pr_val = base_4k | ((uint32_t)limit_4k << 16) | (1u << PR_WP_OFF);
152+
+
153+
+ msg_pdbg("WP enable: base_4k=0x%04x limit_4k=0x%04x pr_val=0x%08x\n",
154+
+ base_4k, limit_4k, pr_val);
155+
+
156+
+ /* Clear all writable PRR slots to avoid stale-slot accumulation.
157+
+ * GPR0 (last slot on PCH100+) is chipset-controlled; skip it. */
158+
+ for (i = 0; i < hwseq_data.num_pr; i++) {
159+
+ if (ich_generation >= SPI_ENGINE_PCH100 &&
160+
+ i == hwseq_data.num_pr - 1)
161+
+ continue; /* GPR0: not OS-writable */
162+
+ msg_pdbg("Clearing PRR%u\n", i);
163+
+ mmio_writel(0, ich_spibar + hwseq_data.reg_pr0 + i * 4);
164+
+ }
165+
+
166+
+ /* Write the new range into slot 0 */
167+
+ msg_pdbg("Writing PRR0 = 0x%08x\n", pr_val);
168+
+ mmio_writel(pr_val, ich_spibar + hwseq_data.reg_pr0);
169+
+
170+
+ /* Verify the write took effect */
171+
+ uint32_t readback = mmio_readl(ich_spibar + hwseq_data.reg_pr0);
172+
+ msg_pdbg("PRR0 readback = 0x%08x\n", readback);
173+
+ if (readback != pr_val) {
174+
+ msg_perr("%s: PRR0 write failed (wrote 0x%08x, read back 0x%08x)\n",
175+
+ __func__, pr_val, readback);
176+
+ return FLASHPROG_WP_ERR_VERIFY_FAILED;
177+
+ }
178+
+
179+
+ return FLASHPROG_WP_OK;
180+
+}
181+
+
182+
+/*
183+
+ * Enumerate possible WP ranges for PCH Protected Range Registers.
184+
+ *
185+
+ * PRRs can protect any 4KB-aligned range, so a complete enumeration is not
186+
+ * practical. Instead this function returns the conventional set: no-protection
187+
+ * plus power-of-2 fractions from the top of the flash chip (the typical pattern
188+
+ * used to protect BIOS regions), ending with full-chip protection.
189+
+ *
190+
+ * The total chip size is derived from the flash descriptor component density
191+
+ * values read during init.
192+
+ */
193+
+static enum flashprog_wp_result ich_hwseq_wp_get_ranges(struct flashprog_wp_ranges **list,
194+
+ struct flashctx *flash)
195+
+{
196+
+ size_t total_size = hwseq_data.size_comp0 + hwseq_data.size_comp1;
197+
+ size_t count, frac, idx;
198+
+
199+
+ if (!total_size)
200+
+ total_size = (size_t)flash->chip->total_size * 1024;
201+
+
202+
+ /* count: (0,0) + top power-of-2 fracs [4K..total_size/2] + (0,total) */
203+
+ count = 2;
204+
+ for (frac = 4096; frac < total_size; frac <<= 1)
205+
+ count++;
206+
+
207+
+ *list = calloc(1, sizeof(struct flashprog_wp_ranges));
208+
+ if (!*list)
209+
+ return FLASHPROG_WP_ERR_OTHER;
210+
+
211+
+ (*list)->ranges = calloc(count, sizeof(struct wp_range));
212+
+ if (!(*list)->ranges) {
213+
+ free(*list);
214+
+ *list = NULL;
215+
+ return FLASHPROG_WP_ERR_OTHER;
216+
+ }
217+
+ (*list)->count = count;
218+
+
219+
+ idx = 0;
220+
+
221+
+ /* No protection */
222+
+ (*list)->ranges[idx].start = 0;
223+
+ (*list)->ranges[idx].len = 0;
224+
+ idx++;
225+
+
226+
+ /* Top power-of-2 fractions: 4K, 8K, ..., total_size/2 */
227+
+ for (frac = 4096; frac < total_size; frac <<= 1) {
228+
+ (*list)->ranges[idx].start = total_size - frac;
229+
+ (*list)->ranges[idx].len = frac;
230+
+ idx++;
231+
+ }
232+
+
233+
+ /* Full chip */
234+
+ (*list)->ranges[idx].start = 0;
235+
+ (*list)->ranges[idx].len = total_size;
236+
+
237+
+ return FLASHPROG_WP_OK;
238+
+}
239+
+
240+
+/*
241+
+ * Read flash STATUS1 register via hardware sequencer RD_STATUS cycle (type 8).
242+
+ * This is needed on PCH100+ (e.g. Meteor Lake) where software sequencing is
243+
+ * locked out and regular SPI STATUS opcodes are unavailable.
244+
+ */
245+
+static int ich_hwseq_read_status(const struct flashctx *flash, enum flash_reg reg, uint8_t *value)
246+
+{
247+
+ uint16_t hsfc;
248+
+ const int len = 1;
249+
+
250+
+ if (reg != STATUS1) {
251+
+ msg_pdbg("%s: only STATUS1 is supported\n", __func__);
252+
+ return SPI_INVALID_OPCODE;
253+
+ }
254+
+
255+
+ /* clear FDONE, FCERR, AEL by writing 1 to them (if set) */
256+
+ REGWRITE16(ICH9_REG_HSFS, REGREAD16(ICH9_REG_HSFS));
257+
+
258+
+ hsfc = REGREAD16(ICH9_REG_HSFC);
259+
+ hsfc &= ~hwseq_data.hsfc_fcycle; /* clear cycle type */
260+
+ hsfc |= (0x8 << HSFC_FCYCLE_OFF); /* RD_STATUS cycle */
261+
+ hsfc &= ~HSFC_FDBC; /* clear byte count */
262+
+ hsfc |= (((len - 1) << HSFC_FDBC_OFF) & HSFC_FDBC);
263+
+ hsfc |= HSFC_FGO; /* start */
264+
+ REGWRITE16(ICH9_REG_HSFC, hsfc);
265+
+
266+
+ if (ich_hwseq_wait_for_cycle_complete(len))
267+
+ return -1;
268+
+
269+
+ ich_read_data(value, len, ICH9_REG_FDATA0);
270+
+ return 0;
271+
+}
272+
+
273+
+/*
274+
+ * Write flash STATUS1 register via hardware sequencer WR_STATUS cycle (type 7).
275+
+ */
276+
+static int ich_hwseq_write_status(const struct flashctx *flash, enum flash_reg reg, uint8_t value)
277+
+{
278+
+ uint16_t hsfc;
279+
+ const int len = 1;
280+
+
281+
+ if (ichspi_lock) {
282+
+ msg_perr("%s: SPI configuration is locked; cannot write STATUS register\n",
283+
+ __func__);
284+
+ return -1;
285+
+ }
286+
+
287+
+ if (reg != STATUS1) {
288+
+ msg_pdbg("%s: only STATUS1 is supported\n", __func__);
289+
+ return SPI_INVALID_OPCODE;
290+
+ }
291+
+
292+
+ /* clear FDONE, FCERR, AEL by writing 1 to them (if set) */
293+
+ REGWRITE16(ICH9_REG_HSFS, REGREAD16(ICH9_REG_HSFS));
294+
+
295+
+ ich_fill_data(&value, len, ICH9_REG_FDATA0);
296+
+
297+
+ hsfc = REGREAD16(ICH9_REG_HSFC);
298+
+ hsfc &= ~hwseq_data.hsfc_fcycle; /* clear cycle type */
299+
+ hsfc |= (0x7 << HSFC_FCYCLE_OFF); /* WR_STATUS cycle */
300+
+ hsfc &= ~HSFC_FDBC; /* clear byte count */
301+
+ hsfc |= (((len - 1) << HSFC_FDBC_OFF) & HSFC_FDBC);
302+
+ hsfc |= HSFC_FGO; /* start */
303+
+ REGWRITE16(ICH9_REG_HSFC, hsfc);
304+
+
305+
+ if (ich_hwseq_wait_for_cycle_complete(len))
306+
+ return -1;
307+
+
308+
+ return 0;
309+
+}
310+
+
311+
static const struct opaque_master opaque_master_ich_hwseq = {
312+
- .max_data_read = 64,
313+
- .max_data_write = 64,
314+
- .probe = ich_hwseq_probe,
315+
- .read = ich_hwseq_read,
316+
- .write = ich_hwseq_write,
317+
- .erase = ich_hwseq_block_erase,
318+
+ .max_data_read = 64,
319+
+ .max_data_write = 64,
320+
+ .probe = ich_hwseq_probe,
321+
+ .read = ich_hwseq_read,
322+
+ .write = ich_hwseq_write,
323+
+ .erase = ich_hwseq_block_erase,
324+
+ .read_register = ich_hwseq_read_status,
325+
+ .write_register = ich_hwseq_write_status,
326+
+ .wp_read_cfg = ich_hwseq_wp_read_cfg,
327+
+ .wp_write_cfg = ich_hwseq_wp_write_cfg,
328+
+ .wp_get_ranges = ich_hwseq_wp_get_ranges,
329+
};
330+
331+
int ich9_init_spi(void *spibar, enum ich_chipset ich_gen)
332+
@@ -1948,6 +2239,8 @@
333+
return ERROR_FATAL;
334+
}
335+
hwseq_data.size_comp1 = tmpi;
336+
+ hwseq_data.reg_pr0 = reg_pr0;
337+
+ hwseq_data.num_pr = num_pr;
338+
339+
register_opaque_master(&opaque_master_ich_hwseq, NULL);
340+
} else {

0 commit comments

Comments
 (0)