|
| 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