Linux/ECC: Difference between revisions
| (One intermediate revision by the same user not shown) | |||
| Line 113: | Line 113: | ||
=== check_ecc.py === | === check_ecc.py === | ||
My Python3 modified version that also supports AMD: | My Python3 modified version that also supports AMD and more Intel chipsets: | ||
check_ecc.py :: | check_ecc.py :: | ||
| Line 129: | Line 129: | ||
http://pastebin.com/URExZi29 | http://pastebin.com/URExZi29 | ||
AMD register definitions follow the Linux kernel's amd64_edac | AMD and Xeon E5/E7 register definitions follow the Linux kernel's amd64_edac | ||
(drivers/edac/ | and sb_edac drivers (drivers/edac/) and arch/x86/kernel/amd_node.c. | ||
""" | """ | ||
| Line 141: | Line 141: | ||
MEM_START = 0xFED10000 | MEM_START = 0xFED10000 | ||
FILESIZE = 32*1024 | FILESIZE = 32*1024 | ||
# 00:00.0 MCHBAR register on desktop/Xeon E3 chipsets: bit 0 enable, bits 38:15 base | |||
MCHBAR = 0x48 | |||
MCHBAR_ENABLE = 1 | |||
MCHBAR_BASE_MASK = 0x7FFFFF8000 | |||
# ECC mode byte (5007h/500Bh) of MAD_DIMM_CH0/CH1, per Intel's datasheet | # ECC mode byte (5007h/500Bh) of MAD_DIMM_CH0/CH1, per Intel's datasheet | ||
| Line 151: | Line 156: | ||
PCI_DEVICES = "/sys/bus/pci/devices" | PCI_DEVICES = "/sys/bus/pci/devices" | ||
INTEL_VENDOR_ID = 0x8086 | |||
AMD_VENDOR_ID = 0x1022 | AMD_VENDOR_ID = 0x1022 | ||
# Xeon E5 v1-v4 / E7 v2-v4 (sb_edac): memory controller TA device ID -> home agent | |||
XEON_TA_HOME_AGENT = {0x3CA8: 0, 0x0EA8: 0, 0x0E68: 1, 0x2FA8: 0, 0x2F68: 1, 0x6FA8: 0, 0x6F68: 1} | |||
MCMTR = 0x7C | |||
MCMTR_ECC_ENABLED = 1 << 2 | |||
# Channel (TAD) device MTR0-2 registers, bit 14 = DIMM present | |||
MTR_REGS = (0x80, 0x84, 0x88) | |||
MTR_DIMM_PRESENT = 1 << 14 | |||
# Zen: UMC (memory channel) registers sit behind the root complex's SMN index/data pair | # Zen: UMC (memory channel) registers sit behind the root complex's SMN index/data pair | ||
| Line 176: | Line 190: | ||
return "ECC enabled" if cap_hi & UMC_ECC_ENABLED else "ECC disabled" | return "ECC enabled" if cap_hi & UMC_ECC_ENABLED else "ECC disabled" | ||
def | def decode_ecc_bit(value, mask): | ||
if | if value == 0xFFFFFFFF: | ||
return "Unknown (register read returned all ones)" | return "Unknown (register read returned all ones)" | ||
return "ECC enabled" if | return "ECC enabled" if value & mask else "ECC disabled" | ||
def read_cpu(): | def read_cpu(): | ||
| Line 199: | Line 213: | ||
def pci_config(bdf): | def pci_config(bdf): | ||
return os.path.join(PCI_DEVICES, "0000:" + bdf, "config") | return os.path.join(PCI_DEVICES, "0000:" + bdf, "config") | ||
def pci_attr(dev, attr): | |||
with open(os.path.join(PCI_DEVICES, dev, attr)) as f: | |||
return int(f.read(), 16) | |||
def xeon_memory_controllers(): | |||
return [dev for dev in sorted(os.listdir(PCI_DEVICES)) | |||
if pci_attr(dev, "vendor") == INTEL_VENDOR_ID and pci_attr(dev, "device") in XEON_TA_HOME_AGENT] | |||
def xeon_dimms_present(dev, ta_id): | |||
# The home agent's channel devices sit on the same bus with device IDs TA+2..TA+5 | |||
bus = dev.rsplit(":", 1)[0] | |||
for chan in os.listdir(PCI_DEVICES): | |||
if chan.rsplit(":", 1)[0] != bus or pci_attr(chan, "vendor") != INTEL_VENDOR_ID: | |||
continue | |||
if not ta_id + 2 <= pci_attr(chan, "device") <= ta_id + 5: | |||
continue | |||
with open(os.path.join(PCI_DEVICES, chan, "config"), "rb") as f: | |||
if any(read_dword(f.fileno(), mtr) & MTR_DIMM_PRESENT for mtr in MTR_REGS): | |||
return True | |||
return False | |||
def read_mchbar(): | |||
path = pci_config("00:00.0") | |||
if not os.path.exists(path): | |||
return 0 | |||
with open(path, "rb") as f: | |||
return struct.unpack("<Q", os.pread(f.fileno(), 8, MCHBAR))[0] | |||
def is_amd_host_bridge(fd): | def is_amd_host_bridge(fd): | ||
| Line 250: | Line 292: | ||
return value | return value | ||
raise RuntimeError("SMN index %#x kept changing under concurrent access" % address) | raise RuntimeError("SMN index %#x kept changing under concurrent access" % address) | ||
def check_intel_xeon(controllers): | |||
for dev in controllers: | |||
ta_id = pci_attr(dev, "device") | |||
with open(os.path.join(PCI_DEVICES, dev, "config"), "rb") as f: | |||
mcmtr = read_dword(f.fileno(), MCMTR) | |||
if xeon_dimms_present(dev, ta_id): | |||
status = decode_ecc_bit(mcmtr, MCMTR_ECC_ENABLED) | |||
else: | |||
status = "Not in use (no DIMMs)" | |||
print("%s: MCMTR=%08x" % (dev, mcmtr)) | |||
print(" Home agent %d: %s" % (XEON_TA_HOME_AGENT[ta_id], status)) | |||
def check_intel(): | def check_intel(): | ||
mchbar = read_mchbar() | |||
if mchbar & (MCHBAR_BASE_MASK | MCHBAR_ENABLE) != MEM_START | MCHBAR_ENABLE: | |||
print("Unsupported platform: MCHBAR is not enabled at %#x (00:00.0 offset 48h = %x)" % (MEM_START, mchbar)) | |||
print("and no Xeon E5 v1-v4 / E7 v2-v4 memory controllers were found.") | |||
return | |||
f = open("/dev/mem", "r+b") | f = open("/dev/mem", "r+b") | ||
mem = mmap.mmap(f.fileno(), FILESIZE, prot=mmap.PROT_READ, offset=MEM_START) | mem = mmap.mmap(f.fileno(), FILESIZE, prot=mmap.PROT_READ, offset=MEM_START) | ||
| Line 295: | Line 355: | ||
nb_cfg = read_dword(f.fileno(), NB_CFG) | nb_cfg = read_dword(f.fileno(), NB_CFG) | ||
print("D%XF3x44: %08x" % (0x18 + node, nb_cfg)) | print("D%XF3x44: %08x" % (0x18 + node, nb_cfg)) | ||
print(" Node %d: %s" % (node, | print(" Node %d: %s" % (node, decode_ecc_bit(nb_cfg, NB_CFG_ECC_ENABLE))) | ||
def main(): | def main(): | ||
| Line 310: | Line 370: | ||
vendor, family, model = read_cpu() | vendor, family, model = read_cpu() | ||
if vendor == "GenuineIntel": | if vendor == "GenuineIntel": | ||
check_intel() | controllers = xeon_memory_controllers() | ||
if controllers: | |||
check_intel_xeon(controllers) | |||
else: | |||
check_intel() | |||
elif vendor == "AuthenticAMD" and family in (0x17, 0x19, 0x1A): | elif vendor == "AuthenticAMD" and family in (0x17, 0x19, 0x1A): | ||
check_amd_zen(family, model, args.allow_taint) | check_amd_zen(family, model, args.allow_taint) | ||
Latest revision as of 17:19, 30 September 2026
Error-Correcting Code Random Access Memory
ECC RAM (Error-Correcting Code Random Access Memory) is a specialized type of computer memory that can detect and automatically correct single-bit data corruption in real time.
ECC needs to BOTH be Supported and Enabled
You can have ECC RAM installed, but not enabled. So you need to both check to see if the system supports it, and if it is enabled in both BIOS and OS.
Check if ECC is supported: [1]
# rough checks for ecc sudo dmidecode | grep -i ecc sudo lshw | grep -i ecc sudo dmesg | grep -i ecc sudo dmesg | grep -i edac
# specific memory checks: sudo dmidecode -t memory | grep -i ecc # Error Correction Type: Multi-bit ECC # Error Correction Type: Multi-bit ECC sudo lshw -C memory | grep -i ecc # capabilities: ecc # configuration: errordetection=multi-bit-ecc sudo dmesg | grep -i EDAC # [ 21.860740] EDAC MC0: Giving out device to module sb_edac controller Haswell SrcID#1_Ha#0: DEV 0000:ff:12.0 (INTERRUPT) # [ 21.860828] EDAC MC1: Giving out device to module sb_edac controller Haswell SrcID#0_Ha#0: DEV 0000:7f:12.0 (INTERRUPT)
Check if ECC is enabled in the OS: [2] [3]
For dmidecode, you must look for the “Error Correction Type” field in the output. For example, If ECC RAM is installed, you should see '“Error Correction Type: ECC” or “Error Correction Type: Single-bit ECC” or “Error Correction Type: Multi-bit ECC” for the corresponding memory module(s) installed in the system.
for dmesg edac, look for "ecc RAM"
---
Also check sysfs for memory controllers:
ls /sys/devices/system/edac/mc/ ls -d /sys/devices/system/edac/mc/mc* mc0 mc1
edac-utils
(notice the "s" on apt package, and without when using the tool)
Install:
# edac-utils : report kernel-detected PCI and ECC RAM errors sudo apt install edac-utils
Usage (show memory controllers):
sudo edac-util -v
No memory controller reported:
$ sudo edac-util -v edac-util: Error: No memory controller data found.
Memory controller reported:
mc0: 0 Uncorrected Errors with no DIMM info mc0: 0 Corrected Errors with no DIMM info mc1: 0 Uncorrected Errors with no DIMM info mc1: 0 Corrected Errors with no DIMM info edac-util: No errors to report.
Check drivers are loaded:
# sb_edac : MC Driver for Intel Sandy Bridge and Ivy Bridge memory controllers # modinfo sb_edac $ lsmod | grep -i edac sb_edac 53248 0
$ sudo edac-ctl --status edac-ctl: drivers are loaded.
$ sudo edac-util --status edac-util: EDAC drivers are loaded. 2 MCs detected
Also check sysfs for memory controllers:
ls /sys/devices/system/edac/mc/ ls -d /sys/devices/system/edac/mc/mc* mc0 mc1
Python Check Script
Well, I was going to wait until tomorrow to do this, but when I get an itch, I have to scratch it. :-D I successfully ported the ecc_check C program to python, and it works perfectly. Here's what it gives me on my FreeNAS system:
5004-5007h: 20 0 62 3 5008-500Bh: 20 0 62 3The last byte can be decoded as such (according to this thread)
According to Intel's datasheet: 0: ECC disabled 1: ECC is active in I/O; ECC logic is not active In this case. 2: ECC is disabled in I/O, but ECC logic is enabled. 3: ECC active in both I/O and ECC logicSo it seems I am running with ECC.
You can grab the python code here: http://pastebin.com/sxd7QcYj
If somebody doesn't beat me to it, I'll make another thread dedicated to the python version of this code tomorrow, so it's not buried here.
Now off to bed for me.
Thanks, Steve
check_ecc.py
My Python3 modified version that also supports AMD and more Intel chipsets:
check_ecc.py ::
$ cat check_ecc.py
#!/usr/bin/env python3
"""
This code is based on a discussion here:
http://hardforum.com/showthread.php?t=1693051
and code pasted here:
http://pastebin.com/URExZi29
AMD and Xeon E5/E7 register definitions follow the Linux kernel's amd64_edac
and sb_edac drivers (drivers/edac/) and arch/x86/kernel/amd_node.c.
"""
import argparse
import mmap
import os
import re
import struct
MEM_START = 0xFED10000
FILESIZE = 32*1024
# 00:00.0 MCHBAR register on desktop/Xeon E3 chipsets: bit 0 enable, bits 38:15 base
MCHBAR = 0x48
MCHBAR_ENABLE = 1
MCHBAR_BASE_MASK = 0x7FFFFF8000
# ECC mode byte (5007h/500Bh) of MAD_DIMM_CH0/CH1, per Intel's datasheet
ECC_MODES = {
0: "ECC disabled",
1: "ECC is active in I/O; ECC logic is not active",
2: "ECC is disabled in I/O, but ECC logic is enabled",
3: "ECC active in both I/O and ECC logic",
}
PCI_DEVICES = "/sys/bus/pci/devices"
INTEL_VENDOR_ID = 0x8086
AMD_VENDOR_ID = 0x1022
# Xeon E5 v1-v4 / E7 v2-v4 (sb_edac): memory controller TA device ID -> home agent
XEON_TA_HOME_AGENT = {0x3CA8: 0, 0x0EA8: 0, 0x0E68: 1, 0x2FA8: 0, 0x2F68: 1, 0x6FA8: 0, 0x6F68: 1}
MCMTR = 0x7C
MCMTR_ECC_ENABLED = 1 << 2
# Channel (TAD) device MTR0-2 registers, bit 14 = DIMM present
MTR_REGS = (0x80, 0x84, 0x88)
MTR_DIMM_PRESENT = 1 << 14
# Zen: UMC (memory channel) registers sit behind the root complex's SMN index/data pair
SMN_INDEX = 0x60
SMN_DATA = 0x64
UMC_BASE = 0x50000
UMC_SDP_CTRL = 0x104
UMC_CAP_HI = 0xDF4
UMC_SDP_INIT = 1 << 31
UMC_ECC_ENABLED = 1 << 30
# Pre-Zen: D18F3x44 NB Configuration, bit 22 DramEccEn
NB_CFG = 0x44
NB_CFG_ECC_ENABLE = 1 << 22
def decode_ecc(value):
return ECC_MODES.get(value, "Unknown ECC mode %#x (expected 0-3)" % value)
def decode_umc(sdp_ctrl, cap_hi):
if 0xFFFFFFFF in (sdp_ctrl, cap_hi):
return "Unknown (register read returned all ones)"
if not sdp_ctrl & UMC_SDP_INIT:
return "Not in use (no DIMMs)"
return "ECC enabled" if cap_hi & UMC_ECC_ENABLED else "ECC disabled"
def decode_ecc_bit(value, mask):
if value == 0xFFFFFFFF:
return "Unknown (register read returned all ones)"
return "ECC enabled" if value & mask else "ECC disabled"
def read_cpu():
info = {}
with open("/proc/cpuinfo") as f:
for line in f:
if not line.strip():
break
key, _, value = line.partition(":")
info[key.strip()] = value.strip()
return info["vendor_id"], int(info["cpu family"]), int(info["model"])
def kernel_version():
return tuple(int(n) for n in re.findall(r"\d+", os.uname().release)[:2])
def read_dword(fd, offset):
return struct.unpack("<I", os.pread(fd, 4, offset))[0]
def pci_config(bdf):
return os.path.join(PCI_DEVICES, "0000:" + bdf, "config")
def pci_attr(dev, attr):
with open(os.path.join(PCI_DEVICES, dev, attr)) as f:
return int(f.read(), 16)
def xeon_memory_controllers():
return [dev for dev in sorted(os.listdir(PCI_DEVICES))
if pci_attr(dev, "vendor") == INTEL_VENDOR_ID and pci_attr(dev, "device") in XEON_TA_HOME_AGENT]
def xeon_dimms_present(dev, ta_id):
# The home agent's channel devices sit on the same bus with device IDs TA+2..TA+5
bus = dev.rsplit(":", 1)[0]
for chan in os.listdir(PCI_DEVICES):
if chan.rsplit(":", 1)[0] != bus or pci_attr(chan, "vendor") != INTEL_VENDOR_ID:
continue
if not ta_id + 2 <= pci_attr(chan, "device") <= ta_id + 5:
continue
with open(os.path.join(PCI_DEVICES, chan, "config"), "rb") as f:
if any(read_dword(f.fileno(), mtr) & MTR_DIMM_PRESENT for mtr in MTR_REGS):
return True
return False
def read_mchbar():
path = pci_config("00:00.0")
if not os.path.exists(path):
return 0
with open(path, "rb") as f:
return struct.unpack("<Q", os.pread(f.fileno(), 8, MCHBAR))[0]
def is_amd_host_bridge(fd):
return read_dword(fd, 0x00) & 0xFFFF == AMD_VENDOR_ID and read_dword(fd, 0x08) >> 16 == 0x0600
def amd_nodes(function):
nodes = []
for slot in range(0x18, 0x20):
path = pci_config("00:%02x.%d" % (slot, function))
if os.path.exists(path):
with open(path, "rb") as f:
if is_amd_host_bridge(f.fileno()):
nodes.append((slot - 0x18, path))
if not nodes:
print("No AMD node devices at PCI 00:18-00:1f (virtual machine?)")
return nodes
def umc_count(family, model):
# UMCs per node, from amd64_edac's per_family_init()
if family == 0x17 and 0x30 <= model <= 0x3F:
return 8
if family == 0x19:
if model <= 0x0F or 0x30 <= model <= 0x3F:
return 8
if 0x10 <= model <= 0x1F or 0xA0 <= model <= 0xAF:
return 12
if 0x40 <= model <= 0x4F or 0x70 <= model <= 0x7F:
return 4
if family == 0x1A:
if model <= 0x1F:
return 12
if 0x50 <= model <= 0x57 or 0xC0 <= model <= 0xC7:
return 16
if 0x90 <= model <= 0xAF:
return 8
return 2
def smn_root(df_f0):
with open(df_f0, "rb") as f:
# DF::CfgAddressCntl holds the root bus; it moved to 0xC04 in Data Fabric v4
offset = 0xC04 if read_dword(f.fileno(), 0x40) & 0x0F000000 else 0x84
bus = read_dword(f.fileno(), offset) & 0xFF
return pci_config("%02x:00.0" % bus)
def smn_read(fd, address):
for _ in range(10):
os.pwrite(fd, struct.pack("<I", address), SMN_INDEX)
value = read_dword(fd, SMN_DATA)
# Kernel SMN users don't lock against user space; retry if one moved the index
if read_dword(fd, SMN_INDEX) == address:
return value
raise RuntimeError("SMN index %#x kept changing under concurrent access" % address)
def check_intel_xeon(controllers):
for dev in controllers:
ta_id = pci_attr(dev, "device")
with open(os.path.join(PCI_DEVICES, dev, "config"), "rb") as f:
mcmtr = read_dword(f.fileno(), MCMTR)
if xeon_dimms_present(dev, ta_id):
status = decode_ecc_bit(mcmtr, MCMTR_ECC_ENABLED)
else:
status = "Not in use (no DIMMs)"
print("%s: MCMTR=%08x" % (dev, mcmtr))
print(" Home agent %d: %s" % (XEON_TA_HOME_AGENT[ta_id], status))
def check_intel():
mchbar = read_mchbar()
if mchbar & (MCHBAR_BASE_MASK | MCHBAR_ENABLE) != MEM_START | MCHBAR_ENABLE:
print("Unsupported platform: MCHBAR is not enabled at %#x (00:00.0 offset 48h = %x)" % (MEM_START, mchbar))
print("and no Xeon E5 v1-v4 / E7 v2-v4 memory controllers were found.")
return
f = open("/dev/mem", "r+b")
mem = mmap.mmap(f.fileno(), FILESIZE, prot=mmap.PROT_READ, offset=MEM_START)
print("5004-5007h:", end=" ")
for i in range(0x5004, 0x5004+4):
print("%x" % mem[i], end=" ")
print()
print(" Channel 0: %s" % decode_ecc(mem[0x5007]))
print("5008-500Bh:", end=" ")
for i in range(0x5008, 0x5008+4):
print("%x" % mem[i], end=" ")
print()
print(" Channel 1: %s" % decode_ecc(mem[0x500B]))
mem.close()
def check_amd_zen(family, model, allow_taint):
if kernel_version() >= (6, 15) and not allow_taint:
print("Linux %s reserves AMD SMN access; reading it from user space taints the kernel." % os.uname().release)
print("Re-run with --allow-taint to read the memory controller anyway.")
return
for node, df_f0 in amd_nodes(0):
root = smn_root(df_f0)
with open(root, "r+b", buffering=0) as f:
if not is_amd_host_bridge(f.fileno()):
print("Node %d: %s is not an AMD root complex; skipped" % (node, root))
continue
for umc in range(umc_count(family, model)):
base = UMC_BASE + (umc << 20)
sdp_ctrl = smn_read(f.fileno(), base + UMC_SDP_CTRL)
cap_hi = smn_read(f.fileno(), base + UMC_CAP_HI)
print("Node %d UMC%d: SdpCtrl=%08x UmcCapHi=%08x" % (node, umc, sdp_ctrl, cap_hi))
print(" Channel %d: %s" % (umc, decode_umc(sdp_ctrl, cap_hi)))
def check_amd_legacy():
for node, f3 in amd_nodes(3):
with open(f3, "rb") as f:
nb_cfg = read_dword(f.fileno(), NB_CFG)
print("D%XF3x44: %08x" % (0x18 + node, nb_cfg))
print(" Node %d: %s" % (node, decode_ecc_bit(nb_cfg, NB_CFG_ECC_ENABLE)))
def main():
parser = argparse.ArgumentParser(description="Report whether DRAM ECC is enabled.")
parser.add_argument("--allow-taint", action="store_true",
help="AMD Zen on Linux 6.15+: read the memory controller even though it taints the kernel")
args = parser.parse_args()
# Script must be run as root
if os.geteuid() != 0:
print("You must be root or root-like to run this script.")
return
vendor, family, model = read_cpu()
if vendor == "GenuineIntel":
controllers = xeon_memory_controllers()
if controllers:
check_intel_xeon(controllers)
else:
check_intel()
elif vendor == "AuthenticAMD" and family in (0x17, 0x19, 0x1A):
check_amd_zen(family, model, args.allow_taint)
elif vendor == "AuthenticAMD" and family in (0x0F, 0x10, 0x15, 0x16):
check_amd_legacy()
else:
print("Unsupported CPU: %s family %#x model %#x" % (vendor, family, model))
if __name__ == "__main__":
main()