1 | // SPDX-License-Identifier: GPL-2.0-or-later |
---|---|
2 | /* |
3 | * FRU (Field-Replaceable Unit) Memory Poison Manager |
4 | * |
5 | * Copyright (c) 2024, Advanced Micro Devices, Inc. |
6 | * All Rights Reserved. |
7 | * |
8 | * Authors: |
9 | * Naveen Krishna Chatradhi <naveenkrishna.chatradhi@amd.com> |
10 | * Muralidhara M K <muralidhara.mk@amd.com> |
11 | * Yazen Ghannam <Yazen.Ghannam@amd.com> |
12 | * |
13 | * Implementation notes, assumptions, and limitations: |
14 | * |
15 | * - FRU memory poison section and memory poison descriptor definitions are not yet |
16 | * included in the UEFI specification. So they are defined here. Afterwards, they |
17 | * may be moved to linux/cper.h, if appropriate. |
18 | * |
19 | * - Platforms based on AMD MI300 systems will be the first to use these structures. |
20 | * There are a number of assumptions made here that will need to be generalized |
21 | * to support other platforms. |
22 | * |
23 | * AMD MI300-based platform(s) assumptions: |
24 | * - Memory errors are reported through x86 MCA. |
25 | * - The entire DRAM row containing a memory error should be retired. |
26 | * - There will be (1) FRU memory poison section per CPER. |
27 | * - The FRU will be the CPU package (processor socket). |
28 | * - The default number of memory poison descriptor entries should be (8). |
29 | * - The platform will use ACPI ERST for persistent storage. |
30 | * - All FRU records should be saved to persistent storage. Module init will |
31 | * fail if any FRU record is not successfully written. |
32 | * |
33 | * - Boot time memory retirement may occur later than ideal due to dependencies |
34 | * on other libraries and drivers. This leaves a gap where bad memory may be |
35 | * accessed during early boot stages. |
36 | * |
37 | * - Enough memory should be pre-allocated for each FRU record to be able to hold |
38 | * the expected number of descriptor entries. This, mostly empty, record is |
39 | * written to storage during init time. Subsequent writes to the same record |
40 | * should allow the Platform to update the stored record in-place. Otherwise, |
41 | * if the record is extended, then the Platform may need to perform costly memory |
42 | * management operations on the storage. For example, the Platform may spend time |
43 | * in Firmware copying and invalidating memory on a relatively slow SPI ROM. |
44 | */ |
45 | |
46 | #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt |
47 | |
48 | #include <linux/cper.h> |
49 | #include <linux/ras.h> |
50 | #include <linux/cpu.h> |
51 | |
52 | #include <acpi/apei.h> |
53 | |
54 | #include <asm/cpu_device_id.h> |
55 | #include <asm/mce.h> |
56 | |
57 | #include "../debugfs.h" |
58 | |
59 | #define INVALID_CPU UINT_MAX |
60 | |
61 | /* Validation Bits */ |
62 | #define FMP_VALID_ARCH_TYPE BIT_ULL(0) |
63 | #define FMP_VALID_ARCH BIT_ULL(1) |
64 | #define FMP_VALID_ID_TYPE BIT_ULL(2) |
65 | #define FMP_VALID_ID BIT_ULL(3) |
66 | #define FMP_VALID_LIST_ENTRIES BIT_ULL(4) |
67 | #define FMP_VALID_LIST BIT_ULL(5) |
68 | |
69 | /* FRU Architecture Types */ |
70 | #define FMP_ARCH_TYPE_X86_CPUID_1_EAX 0 |
71 | |
72 | /* FRU ID Types */ |
73 | #define FMP_ID_TYPE_X86_PPIN 0 |
74 | |
75 | /* FRU Memory Poison Section */ |
76 | struct cper_sec_fru_mem_poison { |
77 | u32 checksum; |
78 | u64 validation_bits; |
79 | u32 fru_arch_type; |
80 | u64 fru_arch; |
81 | u32 fru_id_type; |
82 | u64 fru_id; |
83 | u32 nr_entries; |
84 | } __packed; |
85 | |
86 | /* FRU Descriptor ID Types */ |
87 | #define FPD_HW_ID_TYPE_MCA_IPID 0 |
88 | |
89 | /* FRU Descriptor Address Types */ |
90 | #define FPD_ADDR_TYPE_MCA_ADDR 0 |
91 | |
92 | /* Memory Poison Descriptor */ |
93 | struct cper_fru_poison_desc { |
94 | u64 timestamp; |
95 | u32 hw_id_type; |
96 | u64 hw_id; |
97 | u32 addr_type; |
98 | u64 addr; |
99 | } __packed; |
100 | |
101 | /* Collection of headers and sections for easy pointer use. */ |
102 | struct fru_rec { |
103 | struct cper_record_header hdr; |
104 | struct cper_section_descriptor sec_desc; |
105 | struct cper_sec_fru_mem_poison fmp; |
106 | struct cper_fru_poison_desc entries[]; |
107 | } __packed; |
108 | |
109 | /* |
110 | * Pointers to the complete CPER record of each FRU. |
111 | * |
112 | * Memory allocation will include padded space for descriptor entries. |
113 | */ |
114 | static struct fru_rec **fru_records; |
115 | |
116 | /* system physical addresses array */ |
117 | static u64 *spa_entries; |
118 | |
119 | #define INVALID_SPA ~0ULL |
120 | |
121 | static struct dentry *fmpm_dfs_dir; |
122 | static struct dentry *fmpm_dfs_entries; |
123 | |
124 | #define CPER_CREATOR_FMP \ |
125 | GUID_INIT(0xcd5c2993, 0xf4b2, 0x41b2, 0xb5, 0xd4, 0xf9, 0xc3, \ |
126 | 0xa0, 0x33, 0x08, 0x75) |
127 | |
128 | #define CPER_SECTION_TYPE_FMP \ |
129 | GUID_INIT(0x5e4706c1, 0x5356, 0x48c6, 0x93, 0x0b, 0x52, 0xf2, \ |
130 | 0x12, 0x0a, 0x44, 0x58) |
131 | |
132 | /** |
133 | * DOC: max_nr_entries (byte) |
134 | * Maximum number of descriptor entries possible for each FRU. |
135 | * |
136 | * Values between '1' and '255' are valid. |
137 | * No input or '0' will default to FMPM_DEFAULT_MAX_NR_ENTRIES. |
138 | */ |
139 | static u8 max_nr_entries; |
140 | module_param(max_nr_entries, byte, 0644); |
141 | MODULE_PARM_DESC(max_nr_entries, |
142 | "Maximum number of memory poison descriptor entries per FRU"); |
143 | |
144 | #define FMPM_DEFAULT_MAX_NR_ENTRIES 8 |
145 | |
146 | /* Maximum number of FRUs in the system. */ |
147 | #define FMPM_MAX_NR_FRU 256 |
148 | static unsigned int max_nr_fru; |
149 | |
150 | /* Total length of record including headers and list of descriptor entries. */ |
151 | static size_t max_rec_len; |
152 | |
153 | #define FMPM_MAX_REC_LEN (sizeof(struct fru_rec) + (sizeof(struct cper_fru_poison_desc) * 255)) |
154 | |
155 | /* Total number of SPA entries across all FRUs. */ |
156 | static unsigned int spa_nr_entries; |
157 | |
158 | /* |
159 | * Protect the local records cache in fru_records and prevent concurrent |
160 | * writes to storage. This is only needed after init once notifier block |
161 | * registration is done. |
162 | * |
163 | * The majority of a record is fixed at module init and will not change |
164 | * during run time. The entries within a record will be updated as new |
165 | * errors are reported. The mutex should be held whenever the entries are |
166 | * accessed during run time. |
167 | */ |
168 | static DEFINE_MUTEX(fmpm_update_mutex); |
169 | |
170 | #define for_each_fru(i, rec) \ |
171 | for (i = 0; rec = fru_records[i], i < max_nr_fru; i++) |
172 | |
173 | static inline u32 get_fmp_len(struct fru_rec *rec) |
174 | { |
175 | return rec->sec_desc.section_length - sizeof(struct cper_section_descriptor); |
176 | } |
177 | |
178 | static struct fru_rec *get_fru_record(u64 fru_id) |
179 | { |
180 | struct fru_rec *rec; |
181 | unsigned int i; |
182 | |
183 | for_each_fru(i, rec) { |
184 | if (rec->fmp.fru_id == fru_id) |
185 | return rec; |
186 | } |
187 | |
188 | pr_debug("Record not found for FRU 0x%016llx\n", fru_id); |
189 | |
190 | return NULL; |
191 | } |
192 | |
193 | /* |
194 | * Sum up all bytes within the FRU Memory Poison Section including the Memory |
195 | * Poison Descriptor entries. |
196 | * |
197 | * Don't include the old checksum here. It's a u32 value, so summing each of its |
198 | * bytes will give the wrong total. |
199 | */ |
200 | static u32 do_fmp_checksum(struct cper_sec_fru_mem_poison *fmp, u32 len) |
201 | { |
202 | u32 checksum = 0; |
203 | u8 *buf, *end; |
204 | |
205 | /* Skip old checksum. */ |
206 | buf = (u8 *)fmp + sizeof(u32); |
207 | end = buf + len; |
208 | |
209 | while (buf < end) |
210 | checksum += (u8)(*(buf++)); |
211 | |
212 | return checksum; |
213 | } |
214 | |
215 | static int update_record_on_storage(struct fru_rec *rec) |
216 | { |
217 | u32 len, checksum; |
218 | int ret; |
219 | |
220 | /* Calculate a new checksum. */ |
221 | len = get_fmp_len(rec); |
222 | |
223 | /* Get the current total. */ |
224 | checksum = do_fmp_checksum(fmp: &rec->fmp, len); |
225 | |
226 | /* Use the complement value. */ |
227 | rec->fmp.checksum = -checksum; |
228 | |
229 | pr_debug("Writing to storage\n"); |
230 | |
231 | ret = erst_write(record: &rec->hdr); |
232 | if (ret) { |
233 | pr_warn("Storage update failed for FRU 0x%016llx\n", rec->fmp.fru_id); |
234 | |
235 | if (ret == -ENOSPC) |
236 | pr_warn("Not enough space on storage\n"); |
237 | } |
238 | |
239 | return ret; |
240 | } |
241 | |
242 | static bool rec_has_valid_entries(struct fru_rec *rec) |
243 | { |
244 | if (!(rec->fmp.validation_bits & FMP_VALID_LIST_ENTRIES)) |
245 | return false; |
246 | |
247 | if (!(rec->fmp.validation_bits & FMP_VALID_LIST)) |
248 | return false; |
249 | |
250 | return true; |
251 | } |
252 | |
253 | static bool fpds_equal(struct cper_fru_poison_desc *old, struct cper_fru_poison_desc *new) |
254 | { |
255 | /* |
256 | * Ignore timestamp field. |
257 | * The same physical error may be reported multiple times due to stuck bits, etc. |
258 | * |
259 | * Also, order the checks from most->least likely to fail to shortcut the code. |
260 | */ |
261 | if (old->addr != new->addr) |
262 | return false; |
263 | |
264 | if (old->hw_id != new->hw_id) |
265 | return false; |
266 | |
267 | if (old->addr_type != new->addr_type) |
268 | return false; |
269 | |
270 | if (old->hw_id_type != new->hw_id_type) |
271 | return false; |
272 | |
273 | return true; |
274 | } |
275 | |
276 | static bool rec_has_fpd(struct fru_rec *rec, struct cper_fru_poison_desc *fpd) |
277 | { |
278 | unsigned int i; |
279 | |
280 | for (i = 0; i < rec->fmp.nr_entries; i++) { |
281 | struct cper_fru_poison_desc *fpd_i = &rec->entries[i]; |
282 | |
283 | if (fpds_equal(old: fpd_i, new: fpd)) { |
284 | pr_debug("Found duplicate record\n"); |
285 | return true; |
286 | } |
287 | } |
288 | |
289 | return false; |
290 | } |
291 | |
292 | static void save_spa(struct fru_rec *rec, unsigned int entry, |
293 | u64 addr, u64 id, unsigned int cpu) |
294 | { |
295 | unsigned int i, fru_idx, spa_entry; |
296 | struct atl_err a_err; |
297 | unsigned long spa; |
298 | |
299 | if (entry >= max_nr_entries) { |
300 | pr_warn_once("FRU descriptor entry %d out-of-bounds (max: %d)\n", |
301 | entry, max_nr_entries); |
302 | return; |
303 | } |
304 | |
305 | /* spa_nr_entries is always multiple of max_nr_entries */ |
306 | for (i = 0; i < spa_nr_entries; i += max_nr_entries) { |
307 | fru_idx = i / max_nr_entries; |
308 | if (fru_records[fru_idx] == rec) |
309 | break; |
310 | } |
311 | |
312 | if (i >= spa_nr_entries) { |
313 | pr_warn_once("FRU record %d not found\n", i); |
314 | return; |
315 | } |
316 | |
317 | spa_entry = i + entry; |
318 | if (spa_entry >= spa_nr_entries) { |
319 | pr_warn_once("spa_entries[] index out-of-bounds\n"); |
320 | return; |
321 | } |
322 | |
323 | memset(&a_err, 0, sizeof(struct atl_err)); |
324 | |
325 | a_err.addr = addr; |
326 | a_err.ipid = id; |
327 | a_err.cpu = cpu; |
328 | |
329 | spa = amd_convert_umc_mca_addr_to_sys_addr(err: &a_err); |
330 | if (IS_ERR_VALUE(spa)) { |
331 | pr_debug("Failed to get system address\n"); |
332 | return; |
333 | } |
334 | |
335 | spa_entries[spa_entry] = spa; |
336 | pr_debug("fru_idx: %u, entry: %u, spa_entry: %u, spa: 0x%016llx\n", |
337 | fru_idx, entry, spa_entry, spa_entries[spa_entry]); |
338 | } |
339 | |
340 | static void update_fru_record(struct fru_rec *rec, struct mce *m) |
341 | { |
342 | struct cper_sec_fru_mem_poison *fmp = &rec->fmp; |
343 | struct cper_fru_poison_desc fpd, *fpd_dest; |
344 | u32 entry = 0; |
345 | |
346 | mutex_lock(&fmpm_update_mutex); |
347 | |
348 | memset(&fpd, 0, sizeof(struct cper_fru_poison_desc)); |
349 | |
350 | fpd.timestamp = m->time; |
351 | fpd.hw_id_type = FPD_HW_ID_TYPE_MCA_IPID; |
352 | fpd.hw_id = m->ipid; |
353 | fpd.addr_type = FPD_ADDR_TYPE_MCA_ADDR; |
354 | fpd.addr = m->addr; |
355 | |
356 | /* This is the first entry, so just save it. */ |
357 | if (!rec_has_valid_entries(rec)) |
358 | goto save_fpd; |
359 | |
360 | /* Ignore already recorded errors. */ |
361 | if (rec_has_fpd(rec, fpd: &fpd)) |
362 | goto out_unlock; |
363 | |
364 | if (rec->fmp.nr_entries >= max_nr_entries) { |
365 | pr_warn("Exceeded number of entries for FRU 0x%016llx\n", rec->fmp.fru_id); |
366 | goto out_unlock; |
367 | } |
368 | |
369 | entry = fmp->nr_entries; |
370 | |
371 | save_fpd: |
372 | save_spa(rec, entry, addr: m->addr, id: m->ipid, cpu: m->extcpu); |
373 | fpd_dest = &rec->entries[entry]; |
374 | memcpy(fpd_dest, &fpd, sizeof(struct cper_fru_poison_desc)); |
375 | |
376 | fmp->nr_entries = entry + 1; |
377 | fmp->validation_bits |= FMP_VALID_LIST_ENTRIES; |
378 | fmp->validation_bits |= FMP_VALID_LIST; |
379 | |
380 | pr_debug("Updated FRU 0x%016llx entry #%u\n", fmp->fru_id, entry); |
381 | |
382 | update_record_on_storage(rec); |
383 | |
384 | out_unlock: |
385 | mutex_unlock(lock: &fmpm_update_mutex); |
386 | } |
387 | |
388 | static void retire_dram_row(u64 addr, u64 id, u32 cpu) |
389 | { |
390 | struct atl_err a_err; |
391 | |
392 | memset(&a_err, 0, sizeof(struct atl_err)); |
393 | |
394 | a_err.addr = addr; |
395 | a_err.ipid = id; |
396 | a_err.cpu = cpu; |
397 | |
398 | amd_retire_dram_row(err: &a_err); |
399 | } |
400 | |
401 | static int fru_handle_mem_poison(struct notifier_block *nb, unsigned long val, void *data) |
402 | { |
403 | struct mce *m = (struct mce *)data; |
404 | struct fru_rec *rec; |
405 | |
406 | if (!mce_is_memory_error(m)) |
407 | return NOTIFY_DONE; |
408 | |
409 | retire_dram_row(addr: m->addr, id: m->ipid, cpu: m->extcpu); |
410 | |
411 | /* |
412 | * An invalid FRU ID should not happen on real errors. But it |
413 | * could happen from software error injection, etc. |
414 | */ |
415 | rec = get_fru_record(fru_id: m->ppin); |
416 | if (!rec) |
417 | return NOTIFY_DONE; |
418 | |
419 | update_fru_record(rec, m); |
420 | |
421 | return NOTIFY_OK; |
422 | } |
423 | |
424 | static struct notifier_block fru_mem_poison_nb = { |
425 | .notifier_call = fru_handle_mem_poison, |
426 | .priority = MCE_PRIO_LOWEST, |
427 | }; |
428 | |
429 | static void retire_mem_fmp(struct fru_rec *rec) |
430 | { |
431 | struct cper_sec_fru_mem_poison *fmp = &rec->fmp; |
432 | unsigned int i, cpu; |
433 | |
434 | for (i = 0; i < fmp->nr_entries; i++) { |
435 | struct cper_fru_poison_desc *fpd = &rec->entries[i]; |
436 | unsigned int err_cpu = INVALID_CPU; |
437 | |
438 | if (fpd->hw_id_type != FPD_HW_ID_TYPE_MCA_IPID) |
439 | continue; |
440 | |
441 | if (fpd->addr_type != FPD_ADDR_TYPE_MCA_ADDR) |
442 | continue; |
443 | |
444 | cpus_read_lock(); |
445 | for_each_online_cpu(cpu) { |
446 | if (topology_ppin(cpu) == fmp->fru_id) { |
447 | err_cpu = cpu; |
448 | break; |
449 | } |
450 | } |
451 | cpus_read_unlock(); |
452 | |
453 | if (err_cpu == INVALID_CPU) |
454 | continue; |
455 | |
456 | retire_dram_row(addr: fpd->addr, id: fpd->hw_id, cpu: err_cpu); |
457 | save_spa(rec, entry: i, addr: fpd->addr, id: fpd->hw_id, cpu: err_cpu); |
458 | } |
459 | } |
460 | |
461 | static void retire_mem_records(void) |
462 | { |
463 | struct fru_rec *rec; |
464 | unsigned int i; |
465 | |
466 | for_each_fru(i, rec) { |
467 | if (!rec_has_valid_entries(rec)) |
468 | continue; |
469 | |
470 | retire_mem_fmp(rec); |
471 | } |
472 | } |
473 | |
474 | /* Set the CPER Record Header and CPER Section Descriptor fields. */ |
475 | static void set_rec_fields(struct fru_rec *rec) |
476 | { |
477 | struct cper_section_descriptor *sec_desc = &rec->sec_desc; |
478 | struct cper_record_header *hdr = &rec->hdr; |
479 | |
480 | /* |
481 | * This is a saved record created with fewer max_nr_entries. |
482 | * Update the record lengths and keep everything else as-is. |
483 | */ |
484 | if (hdr->record_length && hdr->record_length < max_rec_len) { |
485 | pr_debug("Growing record 0x%016llx from %u to %zu bytes\n", |
486 | hdr->record_id, hdr->record_length, max_rec_len); |
487 | goto update_lengths; |
488 | } |
489 | |
490 | memcpy(hdr->signature, CPER_SIG_RECORD, CPER_SIG_SIZE); |
491 | hdr->revision = CPER_RECORD_REV; |
492 | hdr->signature_end = CPER_SIG_END; |
493 | |
494 | /* |
495 | * Currently, it is assumed that there is one FRU Memory Poison |
496 | * section per CPER. But this may change for other implementations. |
497 | */ |
498 | hdr->section_count = 1; |
499 | |
500 | /* The logged errors are recoverable. Otherwise, they'd never make it here. */ |
501 | hdr->error_severity = CPER_SEV_RECOVERABLE; |
502 | |
503 | hdr->validation_bits = 0; |
504 | hdr->creator_id = CPER_CREATOR_FMP; |
505 | hdr->notification_type = CPER_NOTIFY_MCE; |
506 | hdr->record_id = cper_next_record_id(); |
507 | hdr->flags = CPER_HW_ERROR_FLAGS_PREVERR; |
508 | |
509 | sec_desc->section_offset = sizeof(struct cper_record_header); |
510 | sec_desc->revision = CPER_SEC_REV; |
511 | sec_desc->validation_bits = 0; |
512 | sec_desc->flags = CPER_SEC_PRIMARY; |
513 | sec_desc->section_type = CPER_SECTION_TYPE_FMP; |
514 | sec_desc->section_severity = CPER_SEV_RECOVERABLE; |
515 | |
516 | update_lengths: |
517 | hdr->record_length = max_rec_len; |
518 | sec_desc->section_length = max_rec_len - sizeof(struct cper_record_header); |
519 | } |
520 | |
521 | static int save_new_records(void) |
522 | { |
523 | DECLARE_BITMAP(new_records, FMPM_MAX_NR_FRU); |
524 | struct fru_rec *rec; |
525 | unsigned int i; |
526 | int ret = 0; |
527 | |
528 | for_each_fru(i, rec) { |
529 | /* No need to update saved records that match the current record size. */ |
530 | if (rec->hdr.record_length == max_rec_len) |
531 | continue; |
532 | |
533 | if (!rec->hdr.record_length) |
534 | set_bit(nr: i, addr: new_records); |
535 | |
536 | set_rec_fields(rec); |
537 | |
538 | ret = update_record_on_storage(rec); |
539 | if (ret) |
540 | goto out_clear; |
541 | } |
542 | |
543 | return ret; |
544 | |
545 | out_clear: |
546 | for_each_fru(i, rec) { |
547 | if (!test_bit(i, new_records)) |
548 | continue; |
549 | |
550 | erst_clear(record_id: rec->hdr.record_id); |
551 | } |
552 | |
553 | return ret; |
554 | } |
555 | |
556 | /* Check that the record matches expected types for the current system.*/ |
557 | static bool fmp_is_usable(struct fru_rec *rec) |
558 | { |
559 | struct cper_sec_fru_mem_poison *fmp = &rec->fmp; |
560 | u64 cpuid; |
561 | |
562 | pr_debug("Validation bits: 0x%016llx\n", fmp->validation_bits); |
563 | |
564 | if (!(fmp->validation_bits & FMP_VALID_ARCH_TYPE)) { |
565 | pr_debug("Arch type unknown\n"); |
566 | return false; |
567 | } |
568 | |
569 | if (fmp->fru_arch_type != FMP_ARCH_TYPE_X86_CPUID_1_EAX) { |
570 | pr_debug("Arch type not 'x86 Family/Model/Stepping'\n"); |
571 | return false; |
572 | } |
573 | |
574 | if (!(fmp->validation_bits & FMP_VALID_ARCH)) { |
575 | pr_debug("Arch value unknown\n"); |
576 | return false; |
577 | } |
578 | |
579 | cpuid = cpuid_eax(op: 1); |
580 | if (fmp->fru_arch != cpuid) { |
581 | pr_debug("Arch value mismatch: record = 0x%016llx, system = 0x%016llx\n", |
582 | fmp->fru_arch, cpuid); |
583 | return false; |
584 | } |
585 | |
586 | if (!(fmp->validation_bits & FMP_VALID_ID_TYPE)) { |
587 | pr_debug("FRU ID type unknown\n"); |
588 | return false; |
589 | } |
590 | |
591 | if (fmp->fru_id_type != FMP_ID_TYPE_X86_PPIN) { |
592 | pr_debug("FRU ID type is not 'x86 PPIN'\n"); |
593 | return false; |
594 | } |
595 | |
596 | if (!(fmp->validation_bits & FMP_VALID_ID)) { |
597 | pr_debug("FRU ID value unknown\n"); |
598 | return false; |
599 | } |
600 | |
601 | return true; |
602 | } |
603 | |
604 | static bool fmp_is_valid(struct fru_rec *rec) |
605 | { |
606 | struct cper_sec_fru_mem_poison *fmp = &rec->fmp; |
607 | u32 checksum, len; |
608 | |
609 | len = get_fmp_len(rec); |
610 | if (len < sizeof(struct cper_sec_fru_mem_poison)) { |
611 | pr_debug("fmp length is too small\n"); |
612 | return false; |
613 | } |
614 | |
615 | /* Checksum must sum to zero for the entire section. */ |
616 | checksum = do_fmp_checksum(fmp, len) + fmp->checksum; |
617 | if (checksum) { |
618 | pr_debug("fmp checksum failed: sum = 0x%x\n", checksum); |
619 | print_hex_dump_debug("fmp record: ", DUMP_PREFIX_NONE, 16, 1, fmp, len, false); |
620 | return false; |
621 | } |
622 | |
623 | if (!fmp_is_usable(rec)) |
624 | return false; |
625 | |
626 | return true; |
627 | } |
628 | |
629 | static struct fru_rec *get_valid_record(struct fru_rec *old) |
630 | { |
631 | struct fru_rec *new; |
632 | |
633 | if (!fmp_is_valid(rec: old)) { |
634 | pr_debug("Ignoring invalid record\n"); |
635 | return NULL; |
636 | } |
637 | |
638 | new = get_fru_record(fru_id: old->fmp.fru_id); |
639 | if (!new) |
640 | pr_debug("Ignoring record for absent FRU\n"); |
641 | |
642 | return new; |
643 | } |
644 | |
645 | /* |
646 | * Fetch saved records from persistent storage. |
647 | * |
648 | * For each found record: |
649 | * - If it was not created by this module, then ignore it. |
650 | * - If it is valid, then copy its data to the local cache. |
651 | * - If it is not valid, then erase it. |
652 | */ |
653 | static int get_saved_records(void) |
654 | { |
655 | struct fru_rec *old, *new; |
656 | u64 record_id; |
657 | int ret, pos; |
658 | ssize_t len; |
659 | |
660 | old = kmalloc(FMPM_MAX_REC_LEN, GFP_KERNEL); |
661 | if (!old) { |
662 | ret = -ENOMEM; |
663 | goto out; |
664 | } |
665 | |
666 | ret = erst_get_record_id_begin(pos: &pos); |
667 | if (ret < 0) |
668 | goto out_end; |
669 | |
670 | while (!erst_get_record_id_next(pos: &pos, record_id: &record_id)) { |
671 | if (record_id == APEI_ERST_INVALID_RECORD_ID) |
672 | goto out_end; |
673 | /* |
674 | * Make sure to clear temporary buffer between reads to avoid |
675 | * leftover data from records of various sizes. |
676 | */ |
677 | memset(old, 0, FMPM_MAX_REC_LEN); |
678 | |
679 | len = erst_read_record(record_id, record: &old->hdr, FMPM_MAX_REC_LEN, |
680 | recordlen: sizeof(struct fru_rec), creatorid: &CPER_CREATOR_FMP); |
681 | if (len < 0) |
682 | continue; |
683 | |
684 | new = get_valid_record(old); |
685 | if (!new) { |
686 | erst_clear(record_id); |
687 | continue; |
688 | } |
689 | |
690 | if (len > max_rec_len) { |
691 | unsigned int saved_nr_entries; |
692 | |
693 | saved_nr_entries = len - sizeof(struct fru_rec); |
694 | saved_nr_entries /= sizeof(struct cper_fru_poison_desc); |
695 | |
696 | pr_warn("Saved record found with %u entries.\n", saved_nr_entries); |
697 | pr_warn("Please increase max_nr_entries to %u.\n", saved_nr_entries); |
698 | |
699 | ret = -EINVAL; |
700 | goto out_end; |
701 | } |
702 | |
703 | /* Restore the record */ |
704 | memcpy(new, old, len); |
705 | } |
706 | |
707 | out_end: |
708 | erst_get_record_id_end(); |
709 | kfree(objp: old); |
710 | out: |
711 | return ret; |
712 | } |
713 | |
714 | static void set_fmp_fields(struct fru_rec *rec, unsigned int cpu) |
715 | { |
716 | struct cper_sec_fru_mem_poison *fmp = &rec->fmp; |
717 | |
718 | fmp->fru_arch_type = FMP_ARCH_TYPE_X86_CPUID_1_EAX; |
719 | fmp->validation_bits |= FMP_VALID_ARCH_TYPE; |
720 | |
721 | /* Assume all CPUs in the system have the same value for now. */ |
722 | fmp->fru_arch = cpuid_eax(op: 1); |
723 | fmp->validation_bits |= FMP_VALID_ARCH; |
724 | |
725 | fmp->fru_id_type = FMP_ID_TYPE_X86_PPIN; |
726 | fmp->validation_bits |= FMP_VALID_ID_TYPE; |
727 | |
728 | fmp->fru_id = topology_ppin(cpu); |
729 | fmp->validation_bits |= FMP_VALID_ID; |
730 | } |
731 | |
732 | static int init_fmps(void) |
733 | { |
734 | struct fru_rec *rec; |
735 | unsigned int i, cpu; |
736 | int ret = 0; |
737 | |
738 | for_each_fru(i, rec) { |
739 | unsigned int fru_cpu = INVALID_CPU; |
740 | |
741 | cpus_read_lock(); |
742 | for_each_online_cpu(cpu) { |
743 | if (topology_physical_package_id(cpu) == i) { |
744 | fru_cpu = cpu; |
745 | break; |
746 | } |
747 | } |
748 | cpus_read_unlock(); |
749 | |
750 | if (fru_cpu == INVALID_CPU) { |
751 | pr_debug("Failed to find matching CPU for FRU #%u\n", i); |
752 | ret = -ENODEV; |
753 | break; |
754 | } |
755 | |
756 | set_fmp_fields(rec, cpu: fru_cpu); |
757 | } |
758 | |
759 | return ret; |
760 | } |
761 | |
762 | static int get_system_info(void) |
763 | { |
764 | /* Only load on MI300A systems for now. */ |
765 | if (!(boot_cpu_data.x86_model >= 0x90 && |
766 | boot_cpu_data.x86_model <= 0x9f)) |
767 | return -ENODEV; |
768 | |
769 | if (!cpu_feature_enabled(X86_FEATURE_AMD_PPIN)) { |
770 | pr_debug("PPIN feature not available\n"); |
771 | return -ENODEV; |
772 | } |
773 | |
774 | /* Use CPU socket as FRU for MI300 systems. */ |
775 | max_nr_fru = topology_max_packages(); |
776 | if (!max_nr_fru) |
777 | return -ENODEV; |
778 | |
779 | if (max_nr_fru > FMPM_MAX_NR_FRU) { |
780 | pr_warn("Too many FRUs to manage: found: %u, max: %u\n", |
781 | max_nr_fru, FMPM_MAX_NR_FRU); |
782 | return -ENODEV; |
783 | } |
784 | |
785 | if (!max_nr_entries) |
786 | max_nr_entries = FMPM_DEFAULT_MAX_NR_ENTRIES; |
787 | |
788 | spa_nr_entries = max_nr_fru * max_nr_entries; |
789 | |
790 | max_rec_len = sizeof(struct fru_rec); |
791 | max_rec_len += sizeof(struct cper_fru_poison_desc) * max_nr_entries; |
792 | |
793 | pr_info("max FRUs: %u, max entries: %u, max record length: %lu\n", |
794 | max_nr_fru, max_nr_entries, max_rec_len); |
795 | |
796 | return 0; |
797 | } |
798 | |
799 | static void free_records(void) |
800 | { |
801 | struct fru_rec *rec; |
802 | int i; |
803 | |
804 | for_each_fru(i, rec) |
805 | kfree(objp: rec); |
806 | |
807 | kfree(objp: fru_records); |
808 | kfree(objp: spa_entries); |
809 | } |
810 | |
811 | static int allocate_records(void) |
812 | { |
813 | int i, ret = 0; |
814 | |
815 | fru_records = kcalloc(n: max_nr_fru, size: sizeof(struct fru_rec *), GFP_KERNEL); |
816 | if (!fru_records) { |
817 | ret = -ENOMEM; |
818 | goto out; |
819 | } |
820 | |
821 | for (i = 0; i < max_nr_fru; i++) { |
822 | fru_records[i] = kzalloc(size: max_rec_len, GFP_KERNEL); |
823 | if (!fru_records[i]) { |
824 | ret = -ENOMEM; |
825 | goto out_free; |
826 | } |
827 | } |
828 | |
829 | spa_entries = kcalloc(n: spa_nr_entries, size: sizeof(u64), GFP_KERNEL); |
830 | if (!spa_entries) { |
831 | ret = -ENOMEM; |
832 | goto out_free; |
833 | } |
834 | |
835 | for (i = 0; i < spa_nr_entries; i++) |
836 | spa_entries[i] = INVALID_SPA; |
837 | |
838 | return ret; |
839 | |
840 | out_free: |
841 | while (--i >= 0) |
842 | kfree(objp: fru_records[i]); |
843 | |
844 | kfree(objp: fru_records); |
845 | out: |
846 | return ret; |
847 | } |
848 | |
849 | static void *fmpm_start(struct seq_file *f, loff_t *pos) |
850 | { |
851 | if (*pos >= (spa_nr_entries + 1)) |
852 | return NULL; |
853 | return pos; |
854 | } |
855 | |
856 | static void *fmpm_next(struct seq_file *f, void *data, loff_t *pos) |
857 | { |
858 | if (++(*pos) >= (spa_nr_entries + 1)) |
859 | return NULL; |
860 | return pos; |
861 | } |
862 | |
863 | static void fmpm_stop(struct seq_file *f, void *data) |
864 | { |
865 | } |
866 | |
867 | #define SHORT_WIDTH 8 |
868 | #define U64_WIDTH 18 |
869 | #define TIMESTAMP_WIDTH 19 |
870 | #define LONG_WIDTH 24 |
871 | #define U64_PAD (LONG_WIDTH - U64_WIDTH) |
872 | #define TS_PAD (LONG_WIDTH - TIMESTAMP_WIDTH) |
873 | static int fmpm_show(struct seq_file *f, void *data) |
874 | { |
875 | unsigned int fru_idx, entry, spa_entry, line; |
876 | struct cper_fru_poison_desc *fpd; |
877 | struct fru_rec *rec; |
878 | |
879 | line = *(loff_t *)data; |
880 | if (line == 0) { |
881 | seq_printf(m: f, fmt: "%-*s", SHORT_WIDTH, "fru_idx"); |
882 | seq_printf(m: f, fmt: "%-*s", LONG_WIDTH, "fru_id"); |
883 | seq_printf(m: f, fmt: "%-*s", SHORT_WIDTH, "entry"); |
884 | seq_printf(m: f, fmt: "%-*s", LONG_WIDTH, "timestamp"); |
885 | seq_printf(m: f, fmt: "%-*s", LONG_WIDTH, "hw_id"); |
886 | seq_printf(m: f, fmt: "%-*s", LONG_WIDTH, "addr"); |
887 | seq_printf(m: f, fmt: "%-*s", LONG_WIDTH, "spa"); |
888 | goto out_newline; |
889 | } |
890 | |
891 | spa_entry = line - 1; |
892 | fru_idx = spa_entry / max_nr_entries; |
893 | entry = spa_entry % max_nr_entries; |
894 | |
895 | rec = fru_records[fru_idx]; |
896 | if (!rec) |
897 | goto out; |
898 | |
899 | seq_printf(m: f, fmt: "%-*u", SHORT_WIDTH, fru_idx); |
900 | seq_printf(m: f, fmt: "0x%016llx%-*s", rec->fmp.fru_id, U64_PAD, ""); |
901 | seq_printf(m: f, fmt: "%-*u", SHORT_WIDTH, entry); |
902 | |
903 | mutex_lock(&fmpm_update_mutex); |
904 | |
905 | if (entry >= rec->fmp.nr_entries) { |
906 | seq_printf(m: f, fmt: "%-*s", LONG_WIDTH, "*"); |
907 | seq_printf(m: f, fmt: "%-*s", LONG_WIDTH, "*"); |
908 | seq_printf(m: f, fmt: "%-*s", LONG_WIDTH, "*"); |
909 | seq_printf(m: f, fmt: "%-*s", LONG_WIDTH, "*"); |
910 | goto out_unlock; |
911 | } |
912 | |
913 | fpd = &rec->entries[entry]; |
914 | |
915 | seq_printf(m: f, fmt: "%ptT%-*s", &fpd->timestamp, TS_PAD, ""); |
916 | seq_printf(m: f, fmt: "0x%016llx%-*s", fpd->hw_id, U64_PAD, ""); |
917 | seq_printf(m: f, fmt: "0x%016llx%-*s", fpd->addr, U64_PAD, ""); |
918 | |
919 | if (spa_entries[spa_entry] == INVALID_SPA) |
920 | seq_printf(m: f, fmt: "%-*s", LONG_WIDTH, "*"); |
921 | else |
922 | seq_printf(m: f, fmt: "0x%016llx%-*s", spa_entries[spa_entry], U64_PAD, ""); |
923 | |
924 | out_unlock: |
925 | mutex_unlock(lock: &fmpm_update_mutex); |
926 | out_newline: |
927 | seq_putc(m: f, c: '\n'); |
928 | out: |
929 | return 0; |
930 | } |
931 | |
932 | static const struct seq_operations fmpm_seq_ops = { |
933 | .start = fmpm_start, |
934 | .next = fmpm_next, |
935 | .stop = fmpm_stop, |
936 | .show = fmpm_show, |
937 | }; |
938 | |
939 | static int fmpm_open(struct inode *inode, struct file *file) |
940 | { |
941 | return seq_open(file, &fmpm_seq_ops); |
942 | } |
943 | |
944 | static const struct file_operations fmpm_fops = { |
945 | .open = fmpm_open, |
946 | .release = seq_release, |
947 | .read = seq_read, |
948 | .llseek = seq_lseek, |
949 | }; |
950 | |
951 | static void setup_debugfs(void) |
952 | { |
953 | struct dentry *dfs = ras_get_debugfs_root(); |
954 | |
955 | if (!dfs) |
956 | return; |
957 | |
958 | fmpm_dfs_dir = debugfs_create_dir(name: "fmpm", parent: dfs); |
959 | if (!fmpm_dfs_dir) |
960 | return; |
961 | |
962 | fmpm_dfs_entries = debugfs_create_file(name: "entries", mode: 0400, parent: fmpm_dfs_dir, NULL, fops: &fmpm_fops); |
963 | if (!fmpm_dfs_entries) |
964 | debugfs_remove(dentry: fmpm_dfs_dir); |
965 | } |
966 | |
967 | static const struct x86_cpu_id fmpm_cpuids[] = { |
968 | X86_MATCH_VENDOR_FAM(AMD, 0x19, NULL), |
969 | { } |
970 | }; |
971 | MODULE_DEVICE_TABLE(x86cpu, fmpm_cpuids); |
972 | |
973 | static int __init fru_mem_poison_init(void) |
974 | { |
975 | int ret; |
976 | |
977 | if (!x86_match_cpu(match: fmpm_cpuids)) { |
978 | ret = -ENODEV; |
979 | goto out; |
980 | } |
981 | |
982 | if (erst_disable) { |
983 | pr_debug("ERST not available\n"); |
984 | ret = -ENODEV; |
985 | goto out; |
986 | } |
987 | |
988 | ret = get_system_info(); |
989 | if (ret) |
990 | goto out; |
991 | |
992 | ret = allocate_records(); |
993 | if (ret) |
994 | goto out; |
995 | |
996 | ret = init_fmps(); |
997 | if (ret) |
998 | goto out_free; |
999 | |
1000 | ret = get_saved_records(); |
1001 | if (ret) |
1002 | goto out_free; |
1003 | |
1004 | ret = save_new_records(); |
1005 | if (ret) |
1006 | goto out_free; |
1007 | |
1008 | setup_debugfs(); |
1009 | |
1010 | retire_mem_records(); |
1011 | |
1012 | mce_register_decode_chain(nb: &fru_mem_poison_nb); |
1013 | |
1014 | pr_info("FRU Memory Poison Manager initialized\n"); |
1015 | return 0; |
1016 | |
1017 | out_free: |
1018 | free_records(); |
1019 | out: |
1020 | return ret; |
1021 | } |
1022 | |
1023 | static void __exit fru_mem_poison_exit(void) |
1024 | { |
1025 | mce_unregister_decode_chain(nb: &fru_mem_poison_nb); |
1026 | debugfs_remove(dentry: fmpm_dfs_dir); |
1027 | free_records(); |
1028 | } |
1029 | |
1030 | module_init(fru_mem_poison_init); |
1031 | module_exit(fru_mem_poison_exit); |
1032 | |
1033 | MODULE_LICENSE("GPL"); |
1034 | MODULE_DESCRIPTION("FRU Memory Poison Manager"); |
1035 |
Definitions
- cper_sec_fru_mem_poison
- cper_fru_poison_desc
- fru_rec
- fru_records
- spa_entries
- fmpm_dfs_dir
- fmpm_dfs_entries
- max_nr_entries
- max_nr_fru
- max_rec_len
- spa_nr_entries
- fmpm_update_mutex
- get_fmp_len
- get_fru_record
- do_fmp_checksum
- update_record_on_storage
- rec_has_valid_entries
- fpds_equal
- rec_has_fpd
- save_spa
- update_fru_record
- retire_dram_row
- fru_handle_mem_poison
- fru_mem_poison_nb
- retire_mem_fmp
- retire_mem_records
- set_rec_fields
- save_new_records
- fmp_is_usable
- fmp_is_valid
- get_valid_record
- get_saved_records
- set_fmp_fields
- init_fmps
- get_system_info
- free_records
- allocate_records
- fmpm_start
- fmpm_next
- fmpm_stop
- fmpm_show
- fmpm_seq_ops
- fmpm_open
- fmpm_fops
- setup_debugfs
- fmpm_cpuids
- fru_mem_poison_init
Improve your Profiling and Debugging skills
Find out more