summaryrefslogtreecommitdiff
path: root/drivers/acpi/apei/bert.c
blob: dc2b79f5dc15bde8b43f15469cfac3dad73734a1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
/*
 * APEI Boot Error Record Table (BERT) support
 *
 * Copyright 2011 Intel Corp.
 *   Author: Huang Ying <ying.huang@intel.com>
 *
 * Under normal circumstances, when a hardware error occurs, kernel
 * will be notified via NMI, MCE or some other method, then kernel
 * will process the error condition, report it, and recover it if
 * possible. But sometime, the situation is so bad, so that firmware
 * may choose to reset directly without notifying Linux kernel.
 *
 * Linux kernel can use the Boot Error Record Table (BERT) to get the
 * un-notified hardware errors that occurred in a previous boot.
 *
 * For more information about BERT, please refer to ACPI Specification
 * version 4.0, section 17.3.1
 *
 * This file is licensed under GPLv2.
 *
 */

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/acpi.h>
#include <linux/io.h>

#include "apei-internal.h"

#define BERT_PFX "BERT: "

static int bert_disable;

static void __init bert_print_all(struct acpi_hest_generic_status *region,
				  unsigned int region_len)
{
	struct acpi_hest_generic_status *estatus = region;
	int remain = region_len;
	u32 estatus_len;
	int first = 1;

	while (remain > sizeof(struct acpi_hest_generic_status)) {
		/* No more error record */
		if (!estatus->block_status)
			break;

		estatus_len = cper_estatus_len(estatus);
		if (estatus_len < sizeof(struct acpi_hest_generic_status) ||
		    remain < estatus_len) {
			pr_err(FW_BUG BERT_PFX
			       "Invalid error status block with length %u\n",
			       estatus_len);
			return;
		}

		if (cper_estatus_check(estatus)) {
			pr_err(FW_BUG BERT_PFX "Invalid error status block\n");
			goto next;
		}

		if (first) {
			pr_info(HW_ERR "Error record from previous boot:\n");
			first = 0;
		}

		cper_estatus_print(KERN_INFO HW_ERR, estatus);

		/* Clear error status */
		estatus->block_status = 0;
next:
		estatus = (void *)estatus + estatus_len;
		remain -= estatus_len;
	}
}

static int __init setup_bert_disable(char *str)
{
	bert_disable = 1;

	return 0;
}
__setup("bert_disable", setup_bert_disable);

static int __init bert_check_table(struct acpi_table_bert *bert_tab)
{
	if (bert_tab->header.length < sizeof(struct acpi_table_bert))
		return -EINVAL;
	if (bert_tab->region_length &&
	    bert_tab->region_length < sizeof(struct acpi_bert_region))
		return -EINVAL;

	return 0;
}

static int __init bert_init(void)
{
	struct acpi_hest_generic_status *bert_region;
	struct acpi_table_bert *bert_tab;
	unsigned int region_len;
	acpi_status status;
	struct resource *r;
	int rc = -EINVAL;

	if (acpi_disabled)
		goto out;

	if (bert_disable) {
		pr_info(BERT_PFX "Boot Error Record Table (BERT) support is disabled.\n");
		goto out;
	}

	status = acpi_get_table(ACPI_SIG_BERT, 0,
				(struct acpi_table_header **)&bert_tab);
	if (status == AE_NOT_FOUND) {
		pr_err(BERT_PFX "Table is not found!\n");
		goto out;
	} else if (ACPI_FAILURE(status)) {
		const char *msg = acpi_format_exception(status);

		pr_err(BERT_PFX "Failed to get table, %s\n", msg);
		goto out;
	}

	rc = bert_check_table(bert_tab);
	if (rc) {
		pr_err(FW_BUG BERT_PFX "BERT table is invalid\n");
		goto out;
	}

	region_len = bert_tab->region_length;
	if (!region_len) {
		rc = 0;
		goto out;
	}

	r = request_mem_region(bert_tab->address, region_len, "APEI BERT");
	if (!r) {
		pr_err(BERT_PFX "Can't request iomem region <%016llx-%016llx>\n",
		       (unsigned long long)bert_tab->address,
		       (unsigned long long)bert_tab->address + region_len - 1);
		rc = -EIO;
		goto out;
	}

	bert_region = ioremap_cache(bert_tab->address, region_len);
	if (!bert_region) {
		rc = -ENOMEM;
		goto out_release;
	}

	bert_print_all(bert_region, region_len);

	iounmap(bert_region);

out_release:
	release_mem_region(bert_tab->address, region_len);
out:
	if (rc)
		bert_disable = 1;

	return rc;
}

late_initcall(bert_init);