mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v3 0/6] x86/hygon: Add Family 0x18 DF node enumeration and SMN access
@ 2026-07-09  7:55 Lin Wang
  2026-07-09  7:55 ` [PATCH v3 1/6] pci_ids: Add Hygon Family 0x18 DF F3/F4 device IDs Lin Wang
                   ` (5 more replies)
  0 siblings, 6 replies; 8+ messages in thread
From: Lin Wang @ 2026-07-09  7:55 UTC (permalink / raw)
  To: yazen.ghannam, mario.limonciello, Borislav Petkov
  Cc: tglx, mingo, x86, linux-kernel

Hygon Family 0x18 systems expose Data Fabric (DF) devices in a layout
that cannot be handled by the AMD fixed-slot node model.

AMD node enumeration relies on DF nodes being visible at fixed PCI
slots 00:18.x through 00:1f.x.  Hygon Family 0x18 instead enumerates
DF instances by PCI device ID at platform-assigned slots.  The node
identity is provided by DF registers, not by the PCI slot number:

  - F1x200 provides socket identity and SocketPresent.
  - F5x180 provides the real DFID on models which need it.
  - CPUID 0x8000001E reports sparse phys_node_id values across sockets.
  - SMN root devices are shared per socket rather than per DF node.

This series adds a Hygon-specific node implementation in
arch/x86/kernel/hygon_node.c.  The Hygon code owns DF enumeration,
node identity, CPU-to-DF-node translation, DF F3/F4 lookup, SMN access,
and the optional SMN debugfs interface.

The AMD-side changes are limited to vendor gating and access-path
hardening:

  - amd_nb.c no longer runs AMD NB framework initialization for Hygon.
  - amd_node.c no longer reserves Hygon SMN root devices from the AMD
    SMN init path.
  - __amd_smn_rw() checks smn_exclusive before indexing amd_roots[],
    so callers that reach the AMD SMN path before initialization get
    -ENODEV rather than dereferencing a NULL root array.

amd_get_mmconfig_range() is intentionally unchanged because it reads
the shared MSR_FAM10H_MMIO_CONF_BASE path used by x86 PCI/PnP code.

No intended behavioral change for AMD systems: the vendor-gating changes
only remove Hygon from AMD-only init paths, and the AMD CPU paths they
now exclusively gate are otherwise unchanged.  The patch 3 SMN access
hardening does not affect AMD SMN access after successful initialization.

The Hygon node cache is built once from fs_initcall:

  1. Enumerate DF misc (F3) devices by Hygon PCI ID.
  2. Read node identity from DF registers.
  3. Validate the enumerated socket set against SocketPresent.
  4. Sort nodes into a dense software order:
       CDD nodes first, sorted by socket and DFID;
       IOD nodes afterwards, sorted by socket and DFID.
  5. Enable CPU-to-DF translation when CPUID NodesPerProcessor matches
     the enumerated CDD count per socket.
  6. Set up Hygon SMN roots and optional debugfs.

The public API is provided by <asm/hygon/node.h>.  It exposes validated
topology and lookup helpers, not model-range helpers.  Model matching is
kept private to the Hygon node implementation through the internal DF
identity table.  Consumers that need their own per-model behavior should
use standard x86_cpu_id matching with driver_data.

When CONFIG_HYGON_NODE=n, the header provides static-inline stubs
returning 0, NULL, or -ENODEV, so consumers do not need local #ifdefs.

SMN access uses the same index/data PCI config-space transaction model
as amd_smn_read/write(), serialized by a Hygon-local mutex.  The
Hygon-specific part is the binding from dense DF node id to the correct
socket-local SMN root.

Patch layout:

  [1/6] pci_ids:      Add Hygon Family 0x18 DF F3/F4 device IDs.
  [2/6] x86/amd_nb:   Restrict AMD NB framework init to AMD vendor.
  [3/6] x86/amd_node: Restrict AMD SMN root enumeration to AMD vendor
                      and harden AMD SMN access before root indexing.
  [4/6] x86/hygon:    Add Hygon DF node enumeration, node cache, lookup
                      API, and CPU-to-DF translation.
  [5/6] x86/hygon:    Add Hygon SMN read/write support.
  [6/6] x86/hygon:    Add optional Hygon SMN debugfs support.

Patch 5 depends on patch 3 at runtime: without the AMD SMN vendor gate,
amd_smn_init() can reserve Hygon root config space before the Hygon SMN
setup runs.  Patch 6 is optional debugfs support and can be reverted
independently.

Tested on:

  - Hygon Family 0x18 Model 0x04, 4 sockets, 16 CDD + 4 IOD
  - Hygon Family 0x18 Model 0x04, 2 sockets, 8 CDD + 2 IOD
  - Hygon Family 0x18 Model 0x07, 2 sockets, 12 CDD + 4 IOD

Verified:

  - Boot and initcall ordering.
  - DF identity register reads.
  - SocketPresent validation against enumerated sockets.
  - CPU-to-DF-node translation.
  - CPU-to-DF-node translation with maxcpus=4 on Model 0x04.
  - SMN read/write on representative addresses.
  - AMD-only and Hygon-enabled build configurations.
  - x86_64 defconfig and allyesconfig sanity builds.

Changes since RFC v2:

  - Drop the vendor-neutral registration interfaces from amd_nb.c and
    amd_node.c.
  - Move Hygon DF enumeration, node identity, CPU-to-DF translation,
    SMN access, and debugfs into Hygon-specific code.
  - Restrict AMD NB and AMD SMN initialization paths to AMD vendor.
  - Harden the AMD SMN access path before indexing amd_roots[].
  - Replace the online-CPU-derived nid map with CPUID NodeId based
    CPU-to-DF translation.
  - Keep supported-model matching private to the Hygon node module.
  - Limit this series to Hygon Family 0x18 models 0x04-0x08.
  - Validate DF enumeration completeness against SocketPresent.
  - Add Hygon SMN debugfs topology output.

Sashiko review follow-up:

  RFC v2 also received a Sashiko automated review.  The changes listed
  above cover its main design findings; patch 3 additionally reorders
  __amd_smn_rw() to check smn_exclusive before indexing amd_roots[].
  The remaining low-level SMN index/data and selector-style debugfs
  behavior stays aligned with the existing AMD SMN model, while
  Hygon-specific discovery and topology binding live in hygon_node.c.

  Patch-level nits are handled in the individual commit messages; patch
  1 keeps the DF F3/F4 IDs as Hygon-prefixed pci_ids.h entries because
  future Hygon DF consumers may share them.

Links:

  RFC v1:
    https://lore.kernel.org/all/20260402111515.1155505-1-wanglin@open-hieco.net/

  RFC v2:
    https://lore.kernel.org/all/20260423060420.1785357-1-wanglin@open-hieco.net/

  Sashiko review of RFC v2:
    https://sashiko.dev/#/patchset/20260423060420.1785357-1-wanglin@open-hieco.net/

Lin Wang (6):
  pci_ids: Add Hygon Family 0x18 DF F3/F4 device IDs
  x86/amd_nb: Restrict NB framework init to AMD vendor
  x86/amd_node: Restrict SMN root enumeration to AMD vendor
  x86/hygon: Add Family 0x18 DF node enumeration
  x86/hygon: Add Family 0x18 SMN access
  x86/hygon: Add SMN debugfs interface

 .../admin-guide/kernel-parameters.txt         |    8 +
 MAINTAINERS                                   |    3 +
 arch/x86/Kconfig                              |    4 +
 arch/x86/include/asm/hygon/node.h             |  205 +++
 arch/x86/kernel/Makefile                      |    1 +
 arch/x86/kernel/amd_nb.c                      |    6 +-
 arch/x86/kernel/amd_node.c                    |   23 +-
 arch/x86/kernel/hygon_node.c                  | 1340 +++++++++++++++++
 include/linux/pci_ids.h                       |    6 +
 9 files changed, 1584 insertions(+), 12 deletions(-)
 create mode 100644 arch/x86/include/asm/hygon/node.h
 create mode 100644 arch/x86/kernel/hygon_node.c

base-commit: 0e35b9b6ec0ffcc5e23cbdec09f5c622ad532b53
--
2.43.0

^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH v3 1/6] pci_ids: Add Hygon Family 0x18 DF F3/F4 device IDs
  2026-07-09  7:55 [PATCH v3 0/6] x86/hygon: Add Family 0x18 DF node enumeration and SMN access Lin Wang
@ 2026-07-09  7:55 ` Lin Wang
  2026-07-09  7:55 ` [PATCH v3 2/6] x86/amd_nb: Restrict NB framework init to AMD vendor Lin Wang
                   ` (4 subsequent siblings)
  5 siblings, 0 replies; 8+ messages in thread
From: Lin Wang @ 2026-07-09  7:55 UTC (permalink / raw)
  To: yazen.ghannam, mario.limonciello, Borislav Petkov
  Cc: tglx, mingo, x86, linux-kernel

Add the PCI device IDs for the Data Fabric (DF) misc (function 3) and
link (function 4) devices on Hygon Family 0x18 processors.  These IDs
are used by Hygon DF node enumeration to discover and
identify DF instances on the system.

Coverage spans the supported Hygon Family 0x18 models (0x04-0x08):

  Model 0x04 (incl. mixed-silicon variant): F3=0x1463 F4=0x1464,
                                            F3=0x1493 F4=0x1494
  Model 0x05 through 0x08:                  F3=0x14b3 F4=0x14b4

Some values numerically match AMD DF IDs, but the IDs are matched as
vendor/device pairs with PCI_VENDOR_ID_HYGON; the explicit
Hygon-prefixed names keep enumeration independent of AMD device-ID
naming.  Like the AMD DF IDs they sit next to, they may be referenced
by further Hygon DF consumers over time.

Signed-off-by: Lin Wang <wanglin@open-hieco.net>
---
 include/linux/pci_ids.h | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/include/linux/pci_ids.h b/include/linux/pci_ids.h
index 1c9d40e09107..513856c63266 100644
--- a/include/linux/pci_ids.h
+++ b/include/linux/pci_ids.h
@@ -2615,6 +2615,12 @@
 #define PCI_VENDOR_ID_ROCKCHIP		0x1d87
 
 #define PCI_VENDOR_ID_HYGON		0x1d94
+#define PCI_DEVICE_ID_HYGON_18H_M04H_DF_F3	0x1463
+#define PCI_DEVICE_ID_HYGON_18H_M04H_DF_F4	0x1464
+#define PCI_DEVICE_ID_HYGON_18H_M04H_DF_F3B	0x1493
+#define PCI_DEVICE_ID_HYGON_18H_M04H_DF_F4B	0x1494
+#define PCI_DEVICE_ID_HYGON_18H_M05H_DF_F3	0x14b3
+#define PCI_DEVICE_ID_HYGON_18H_M05H_DF_F4	0x14b4
 
 #define PCI_VENDOR_ID_META		0x1d9b
 
-- 
2.43.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH v3 2/6] x86/amd_nb: Restrict NB framework init to AMD vendor
  2026-07-09  7:55 [PATCH v3 0/6] x86/hygon: Add Family 0x18 DF node enumeration and SMN access Lin Wang
  2026-07-09  7:55 ` [PATCH v3 1/6] pci_ids: Add Hygon Family 0x18 DF F3/F4 device IDs Lin Wang
@ 2026-07-09  7:55 ` Lin Wang
  2026-07-14 13:10   ` Yazen Ghannam
  2026-07-09  7:55 ` [PATCH v3 3/6] x86/amd_node: Restrict SMN root enumeration " Lin Wang
                   ` (3 subsequent siblings)
  5 siblings, 1 reply; 8+ messages in thread
From: Lin Wang @ 2026-07-09  7:55 UTC (permalink / raw)
  To: yazen.ghannam, mario.limonciello, Borislav Petkov
  Cc: tglx, mingo, x86, linux-kernel

Drop the X86_VENDOR_HYGON branch from init_amd_nbs() and
early_is_amd_nb().  Both functions are entry points to the AMD NB
framework, which uses the legacy fixed-slot PCI enumeration model
(slot 0x18+i for node i) and the AMD-only amd_nb_misc_ids[] device-ID
table.

On Hygon Family 0x18, these paths do not find usable AMD NB entries:
amd_cache_northbridges() probes the AMD fixed slots, while
early_is_amd_nb() matches only AMD IDs from amd_nb_misc_ids[].

Restrict these AMD framework entry points to AMD vendor.  Hygon DF
enumeration is added separately in arch/x86/kernel/hygon_node.c.

No user-visible functional regression on Hygon Fam18h.

amd_get_mmconfig_range() is intentionally left unchanged: it reads
the MSR_FAM10H_MMIO_CONF_BASE shared CPU MSR and serves
arch/x86/pci/amd_bus.c and drivers/pnp/quirks.c on Hygon platforms.

Signed-off-by: Lin Wang <wanglin@open-hieco.net>
---
 arch/x86/kernel/amd_nb.c | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/arch/x86/kernel/amd_nb.c b/arch/x86/kernel/amd_nb.c
index 5d364540673d..848ed2ca17c3 100644
--- a/arch/x86/kernel/amd_nb.c
+++ b/arch/x86/kernel/amd_nb.c
@@ -125,8 +125,7 @@ bool __init early_is_amd_nb(u32 device)
 	const struct pci_device_id *id;
 	u32 vendor = device & 0xffff;
 
-	if (boot_cpu_data.x86_vendor != X86_VENDOR_AMD &&
-	    boot_cpu_data.x86_vendor != X86_VENDOR_HYGON)
+	if (boot_cpu_data.x86_vendor != X86_VENDOR_AMD)
 		return false;
 
 	if (cpu_feature_enabled(X86_FEATURE_ZEN))
@@ -315,8 +314,7 @@ static __init void fix_erratum_688(void)
 
 static __init int init_amd_nbs(void)
 {
-	if (boot_cpu_data.x86_vendor != X86_VENDOR_AMD &&
-	    boot_cpu_data.x86_vendor != X86_VENDOR_HYGON)
+	if (boot_cpu_data.x86_vendor != X86_VENDOR_AMD)
 		return 0;
 
 	amd_cache_northbridges();
-- 
2.43.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH v3 3/6] x86/amd_node: Restrict SMN root enumeration to AMD vendor
  2026-07-09  7:55 [PATCH v3 0/6] x86/hygon: Add Family 0x18 DF node enumeration and SMN access Lin Wang
  2026-07-09  7:55 ` [PATCH v3 1/6] pci_ids: Add Hygon Family 0x18 DF F3/F4 device IDs Lin Wang
  2026-07-09  7:55 ` [PATCH v3 2/6] x86/amd_nb: Restrict NB framework init to AMD vendor Lin Wang
@ 2026-07-09  7:55 ` Lin Wang
  2026-07-09  7:55 ` [PATCH v3 4/6] x86/hygon: Add Family 0x18 DF node enumeration Lin Wang
                   ` (2 subsequent siblings)
  5 siblings, 0 replies; 8+ messages in thread
From: Lin Wang @ 2026-07-09  7:55 UTC (permalink / raw)
  To: yazen.ghannam, mario.limonciello, Borislav Petkov
  Cc: tglx, mingo, x86, linux-kernel

Drop X86_VENDOR_HYGON / PCI_VENDOR_ID_HYGON support from the AMD SMN
init path:

  - get_next_root(): only match PCI_VENDOR_ID_AMD host bridges.
  - amd_smn_init(): add boot_cpu_data.x86_vendor != X86_VENDOR_AMD to
    the early-return condition, alongside the existing X86_FEATURE_ZEN
    check (X86_FEATURE_ZEN is set on Hygon Dhyana too, so the feature
    check alone is not sufficient to gate AMD-only init).

This is functionally required by the Hygon DF node and SMN access
implementation added in subsequent patches.  Before this cleanup, on
a Hygon Family 0x18 platform:

  amd_smn_init (fs_initcall)
    -> get_next_root() matched Hygon host bridges
    -> pci_request_config_region_exclusive() reserved Hygon root
       PCI config space
    -> smn_exclusive = true

That left Hygon's own SMN setup unable to reserve the same config
space, failing with -EEXIST.  Restricting both sites to AMD vendor
leaves Hygon root devices untouched by the AMD path so the
Hygon-specific SMN implementation in arch/x86/kernel/hygon_node.c
can reserve them itself.

Together, these changes keep the AMD SMN path AMD-only: root
discovery matches AMD host bridges only, and amd_smn_init() returns
early on non-AMD CPUs.

Gating amd_smn_init() on vendor also changes a precondition the SMN
access path relied on.  On Hygon, amd_smn_init() now returns early, so
amd_roots stays NULL and smn_exclusive stays false, while
amd_num_nodes() (derived from CPU topology) remains non-zero.  Harden
__amd_smn_rw() to test smn_exclusive before indexing amd_roots[], so a
caller that has not yet been converted to the Hygon SMN path receives
-ENODEV instead of dereferencing a NULL amd_roots[].  Additionally,
amd_smn_read() now returns the underlying error directly instead of
reading back a possibly-uninitialised *value, and zeroes *value on any
failure.  Neither change affects AMD SMN access after successful
initialization.

Signed-off-by: Lin Wang <wanglin@open-hieco.net>
---
 arch/x86/kernel/amd_node.c | 23 +++++++++++++++--------
 1 file changed, 15 insertions(+), 8 deletions(-)

diff --git a/arch/x86/kernel/amd_node.c b/arch/x86/kernel/amd_node.c
index 0be01725a2a4..62086b8b5804 100644
--- a/arch/x86/kernel/amd_node.c
+++ b/arch/x86/kernel/amd_node.c
@@ -88,16 +88,17 @@ static int __amd_smn_rw(u8 i_off, u8 d_off, u16 node, u32 address, u32 *value, b
 	struct pci_dev *root;
 	int err = -ENODEV;
 
-	if (node >= amd_num_nodes())
+	/*
+	 * smn_exclusive is set only after amd_roots is allocated and the
+	 * root config space reserved; test it before indexing amd_roots[].
+	 */
+	if (!smn_exclusive || node >= amd_num_nodes())
 		return err;
 
 	root = amd_roots[node];
 	if (!root)
 		return err;
 
-	if (!smn_exclusive)
-		return err;
-
 	guard(mutex)(&smn_mutex);
 
 	err = pci_write_config_dword(root, i_off, address);
@@ -116,9 +117,15 @@ int __must_check amd_smn_read(u16 node, u32 address, u32 *value)
 {
 	int err = __amd_smn_rw(SMN_INDEX_OFFSET, SMN_DATA_OFFSET, node, address, value, false);
 
+	/* On error *value may be untouched; do not read it back. */
+	if (err) {
+		*value = 0;
+		return err;
+	}
+
 	if (PCI_POSSIBLE_ERROR(*value)) {
-		err = -ENODEV;
 		*value = 0;
+		return -ENODEV;
 	}
 
 	return err;
@@ -225,8 +232,7 @@ static struct pci_dev *get_next_root(struct pci_dev *root)
 		if (root->devfn)
 			continue;
 
-		if (root->vendor != PCI_VENDOR_ID_AMD &&
-		    root->vendor != PCI_VENDOR_ID_HYGON)
+		if (root->vendor != PCI_VENDOR_ID_AMD)
 			continue;
 
 		break;
@@ -249,7 +255,8 @@ static int __init amd_smn_init(void)
 	u16 count, num_roots, roots_per_node, node, num_nodes;
 	struct pci_dev *root;
 
-	if (!cpu_feature_enabled(X86_FEATURE_ZEN))
+	if (!cpu_feature_enabled(X86_FEATURE_ZEN) ||
+	    boot_cpu_data.x86_vendor != X86_VENDOR_AMD)
 		return 0;
 
 	guard(mutex)(&smn_mutex);
-- 
2.43.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH v3 4/6] x86/hygon: Add Family 0x18 DF node enumeration
  2026-07-09  7:55 [PATCH v3 0/6] x86/hygon: Add Family 0x18 DF node enumeration and SMN access Lin Wang
                   ` (2 preceding siblings ...)
  2026-07-09  7:55 ` [PATCH v3 3/6] x86/amd_node: Restrict SMN root enumeration " Lin Wang
@ 2026-07-09  7:55 ` Lin Wang
  2026-07-09  7:55 ` [PATCH v3 5/6] x86/hygon: Add Family 0x18 SMN access Lin Wang
  2026-07-09  7:55 ` [PATCH v3 6/6] x86/hygon: Add SMN debugfs interface Lin Wang
  5 siblings, 0 replies; 8+ messages in thread
From: Lin Wang @ 2026-07-09  7:55 UTC (permalink / raw)
  To: yazen.ghannam, mario.limonciello, Borislav Petkov
  Cc: tglx, mingo, x86, linux-kernel

Add a Hygon-specific Data Fabric (DF) node enumeration module, gated
by the new CONFIG_HYGON_NODE.

AMD systems can enumerate DF nodes from fixed PCI slots 00:18..1f on
bus 0. Hygon Family 0x18 systems expose DF instances at
platform-assigned PCI slots, so PCI slot number cannot be used as node
identity and the AMD node enumeration path cannot be reused.

Enumerate Hygon DF nodes by walking DF misc (F3) devices by PCI ID and
reading hardware identity from DF registers. F1x200 provides socket
identity and the system-wide SocketPresent mask, which is checked
against the enumerated sockets to catch a missing one; supported models
that encode sparse DFIDs use F5x180 for the real Fabric ID. The
resulting node cache is sorted into a dense software order: CDD nodes
first, ordered by socket and DFID, followed by IOD nodes.

Provide a Hygon node API in <asm/hygon/node.h> for consumers that need
node counts, socket/DFID/type identity, CPU-to-DF translation, F3/F4
PCI function lookup, and PCI-device-to-DF-node lookup.

CPU topology reports a sparse phys_node_id value, while Hygon DF
consumers need the dense CDD index used by the DF node cache. On the
supported Fam18h models, CPUID 0x8000001E ECX[7:0] encodes the socket
in the high nibble and a socket-local CDD ordinal in the low nibble.
The translator uses that encoding to select the matching CDD from the
DF cache, whose CDD region is sorted by hardware (socket_id, dfid).
This keeps CPU-to-DF translation independent of the online CPU set.

After the node cache is built, init enables the CPU-to-DF helper only
when CPUID NodesPerProcessor matches the enumerated CDD count per
socket; otherwise the rest of the node stack remains usable and only
hygon_cpu_to_df_node() is disabled.

Supported-model matching is private to the Hygon node module and is
driven by its internal DF identity table. The public API exposes
validated topology rather than model-range helpers.  Model-specific
behavior for consumers is left to their own x86_cpu_id tables and
driver_data.

The cache is built once from fs_initcall and is read-only afterwards.
SMN access support is added in the following patch.

Signed-off-by: Lin Wang <wanglin@open-hieco.net>
---
 MAINTAINERS                       |   3 +
 arch/x86/Kconfig                  |   4 +
 arch/x86/include/asm/hygon/node.h | 171 ++++++
 arch/x86/kernel/Makefile          |   1 +
 arch/x86/kernel/hygon_node.c      | 849 ++++++++++++++++++++++++++++++
 5 files changed, 1028 insertions(+)
 create mode 100644 arch/x86/include/asm/hygon/node.h
 create mode 100644 arch/x86/kernel/hygon_node.c

diff --git a/MAINTAINERS b/MAINTAINERS
index f37a81950e25..a533fa480f95 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -12052,9 +12052,12 @@ F:	drivers/input/touchscreen/hycon-hy46xx.c
 
 HYGON PROCESSOR SUPPORT
 M:	Pu Wen <puwen@hygon.cn>
+M:	Lin Wang <wanglin@open-hieco.net>
 L:	linux-kernel@vger.kernel.org
 S:	Maintained
+F:	arch/x86/include/asm/hygon/
 F:	arch/x86/kernel/cpu/hygon.c
+F:	arch/x86/kernel/hygon_node.c
 
 HYNIX HI556 SENSOR DRIVER
 M:	Sakari Ailus <sakari.ailus@linux.intel.com>
diff --git a/arch/x86/Kconfig b/arch/x86/Kconfig
index bdad90f210e4..2e9396520203 100644
--- a/arch/x86/Kconfig
+++ b/arch/x86/Kconfig
@@ -3073,6 +3073,10 @@ config AMD_NODE
 	def_bool y
 	depends on CPU_SUP_AMD && PCI
 
+config HYGON_NODE
+	def_bool y
+	depends on CPU_SUP_HYGON && PCI
+
 endmenu
 
 menu "Binary Emulations"
diff --git a/arch/x86/include/asm/hygon/node.h b/arch/x86/include/asm/hygon/node.h
new file mode 100644
index 000000000000..bb8168beb288
--- /dev/null
+++ b/arch/x86/include/asm/hygon/node.h
@@ -0,0 +1,171 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Hygon Family 0x18 Data Fabric node enumeration API
+ *
+ * This header exposes the Hygon Fam18h DF node enumeration and DF
+ * function access primitives consumers (EDAC, MCE decode, ATL, etc.)
+ * need.
+ */
+#ifndef _ASM_X86_HYGON_NODE_H
+#define _ASM_X86_HYGON_NODE_H
+
+#include <linux/errno.h>
+#include <linux/processor.h>
+#include <linux/types.h>
+
+struct pci_dev;
+
+/* DF function indices supported by hygon_node_get_func(). */
+#define HYGON_DF_F3	3	/* misc */
+#define HYGON_DF_F4	4	/* link */
+
+/* Hygon compute dies (CDD) start at DFID 4; IO dies occupy DFIDs 0-3. */
+#define HYGON_CDD_DFID_BASE	4
+
+/**
+ * enum hygon_node_type - DF node type, derived from DFID
+ * @HYGON_NODE_IOD: I/O die (DFID < HYGON_CDD_DFID_BASE); no UMC.
+ * @HYGON_NODE_CDD: compute die (DFID >= HYGON_CDD_DFID_BASE); hosts
+ *                  CPU cores and UMC controllers.
+ */
+enum hygon_node_type {
+	HYGON_NODE_IOD = 0,
+	HYGON_NODE_CDD = 1,
+};
+
+/**
+ * struct hygon_node_info - identity snapshot for a DF node
+ * @socket_id: physical socket ID, F1x200[30:28]
+ * @dfid:      Data Fabric ID, model-dependent source
+ * @type:      HYGON_NODE_CDD or HYGON_NODE_IOD
+ */
+struct hygon_node_info {
+	u8			socket_id;
+	u8			dfid;
+	enum hygon_node_type	type;
+};
+
+#ifdef CONFIG_HYGON_NODE
+
+/**
+ * hygon_node_num() - total number of DF nodes (CDD + IOD)
+ *
+ * Return: total node count, or 0 if the cache is not ready or this is
+ * not a Hygon Fam18h platform.  Use this as the upper bound when
+ * iterating over DF nodes or indexing SMN access.
+ */
+u16 hygon_node_num(void);
+
+/**
+ * hygon_cdd_num() - number of compute dies (CDD)
+ *
+ * DFID >= 4 marks a compute die (CDD).  DFID < 4 marks an I/O die
+ * (IOD) which has no UMC.  Use this count to size EDAC memory-controller
+ * instances and to iterate over UMC-bearing nodes.
+ *
+ * Return: CDD count, or 0 if the cache is not ready.
+ */
+u16 hygon_cdd_num(void);
+
+/**
+ * hygon_node_get_info() - read identity snapshot for a DF node
+ * @node: DF node index in [0, hygon_node_num())
+ * @info: output structure (socket_id, dfid, type)
+ *
+ * Copies all immutable identity fields in one call so callers that
+ * need to make decisions on multiple fields (e.g. "if CDD then read
+ * DFID and compute UMC base") do not need multiple cache lookups.
+ *
+ * Return: 0 on success, -EINVAL if @node is out of range or @info is
+ * NULL, -ENODEV if the cache is not ready.
+ */
+int hygon_node_get_info(u16 node, struct hygon_node_info *info);
+
+/**
+ * hygon_cpu_to_df_node() - map CPU to dense DF CDD index
+ * @cpu: CPU index
+ *
+ * Hygon Fam18h exposes sparse physical node IDs via CPUID 8000001E[7:0].
+ * This function translates the per-CPU physical node ID into a dense
+ * DF CDD index in [0, hygon_cdd_num()).
+ *
+ * Return: DF CDD index on success, -EINVAL if @cpu is out of range,
+ * -ENODEV if CPU-to-DF mapping is unsupported on this model or the
+ * physical node ID does not map to a known DF node.
+ */
+int hygon_cpu_to_df_node(unsigned int cpu);
+
+/**
+ * hygon_node_get_func() - get DF function PCI device for a node
+ * @node: DF node index in [0, hygon_node_num())
+ * @func: HYGON_DF_F3 or HYGON_DF_F4
+ *
+ * Return: referenced pci_dev on success.  NULL if @node is out of
+ * range, @func is unsupported, or the cache is not ready.  The
+ * caller must release the reference with pci_dev_put().
+ */
+struct pci_dev *hygon_node_get_func(u16 node, u8 func);
+
+/**
+ * hygon_pci_dev_to_df_node() - find DF node owning the given PCI device
+ * @pdev: PCI device on the same domain, bus and slot as one of the DF
+ *        nodes (typically a sibling function of the DF misc device,
+ *        e.g. a UMC channel)
+ *
+ * Looks up the DF node whose misc (F3) device shares the same PCI
+ * domain, bus, and slot as @pdev.  Useful for drivers that hold a
+ * per-PCI-function device and need the containing DF node index for
+ * SMN access or other node-indexed operations.
+ *
+ * Return: DF node index in [0, hygon_node_num()) on success, -EINVAL
+ * if @pdev is NULL, -ENODEV if no matching node is found or the cache
+ * is not ready.
+ */
+int hygon_pci_dev_to_df_node(struct pci_dev *pdev);
+
+#else /* !CONFIG_HYGON_NODE */
+
+static inline u16 hygon_node_num(void)
+{
+	return 0;
+}
+
+static inline u16 hygon_cdd_num(void)
+{
+	return 0;
+}
+
+static inline int hygon_node_get_info(u16 node, struct hygon_node_info *info)
+{
+	return -ENODEV;
+}
+
+static inline int hygon_cpu_to_df_node(unsigned int cpu)
+{
+	return -ENODEV;
+}
+
+static inline struct pci_dev *hygon_node_get_func(u16 node, u8 func)
+{
+	return NULL;
+}
+
+static inline int hygon_pci_dev_to_df_node(struct pci_dev *pdev)
+{
+	return -ENODEV;
+}
+
+#endif /* CONFIG_HYGON_NODE */
+
+/* Inline helpers, available regardless of CONFIG_HYGON_NODE. */
+
+/**
+ * is_hygon_f18h() - true on Hygon Family 0x18 CPUs
+ */
+static inline bool is_hygon_f18h(void)
+{
+	return boot_cpu_data.x86_vendor == X86_VENDOR_HYGON &&
+	       boot_cpu_data.x86 == 0x18;
+}
+
+#endif /* _ASM_X86_HYGON_NODE_H */
diff --git a/arch/x86/kernel/Makefile b/arch/x86/kernel/Makefile
index 31f46fd00527..b8430e5e53fb 100644
--- a/arch/x86/kernel/Makefile
+++ b/arch/x86/kernel/Makefile
@@ -136,6 +136,7 @@ obj-$(CONFIG_HPET_TIMER) 	+= hpet.o
 
 obj-$(CONFIG_AMD_NB)		+= amd_nb.o
 obj-$(CONFIG_AMD_NODE)		+= amd_node.o
+obj-$(CONFIG_HYGON_NODE)	+= hygon_node.o
 obj-$(CONFIG_DEBUG_NMI_SELFTEST) += nmi_selftest.o
 
 obj-$(CONFIG_KVM_GUEST)		+= kvm.o kvmclock.o
diff --git a/arch/x86/kernel/hygon_node.c b/arch/x86/kernel/hygon_node.c
new file mode 100644
index 000000000000..d0c861f61d1f
--- /dev/null
+++ b/arch/x86/kernel/hygon_node.c
@@ -0,0 +1,849 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Hygon Family 0x18 Data Fabric node enumeration
+ *
+ * AMD systems enumerate DF nodes at fixed PCI slots 00:18..1f on bus 0.
+ * Hygon Fam18h places DF instances at platform-assigned slots with no
+ * fixed relationship to node identity, so the AMD enumeration path in
+ * amd_nb.c / amd_node.c cannot be used.
+ *
+ * This file provides:
+ *
+ *  - DF node enumeration: walk DF misc (F3) devices by PCI ID, read
+ *    hardware identity from per-instance registers (F1x200 SystemCfg,
+ *    F5x180 FabricId), and build a sorted node cache that classifies
+ *    each node as CDD (compute, DFID >= 4) or IOD (I/O, DFID < 4).
+ *
+ *  - CPU-to-node mapping: map per-CPU phys_node_id (CPUID
+ *    8000001E[7:0]) to a dense DF CDD index for lookup by EDAC,
+ *    MCE decode, and ATL.
+ *
+ *  - DF function access: hygon_node_get_func() returns the misc (F3)
+ *    or link (F4) pci_dev for a node, with a referenced return.
+ */
+
+#define pr_fmt(fmt) "hygon_node: " fmt
+
+#include <linux/bitops.h>
+#include <linux/cpu.h>
+#include <linux/export.h>
+#include <linux/init.h>
+#include <linux/pci.h>
+#include <linux/pci_ids.h>
+#include <linux/processor.h>
+#include <linux/slab.h>
+#include <linux/sort.h>
+#include <linux/topology.h>
+
+#include <asm/cpuid/api.h>
+#include <asm/hygon/node.h>
+
+/* Maximum socket count this implementation supports. */
+#define HYGON_MAX_SOCKETS	8
+
+/*
+ * DF register offsets used for node identity discovery.
+ *
+ * F1x200 (SystemCfg) -- present on all models:
+ *   [30:28]  MySocketId    - hardware socket ID
+ *   [23:20]  MyDieId       - die ID (equals DFID on some models, see below)
+ *
+ * F5x180 (FabricBlockInstanceInformation3_CS) -- Model 06h-08h only:
+ *   [19:16]  DFID          - real Data Fabric ID for UMC/SMN addressing
+ *
+ * DFID source by model:
+ *   Model 04h/05h:  F1x200[23:20] (MyDieId == DFID)
+ *   Model 06h-08h:  F5x180[19:16] (MyDieId != DFID, different numbering)
+ */
+#define DF_F1_SYSTEM_CFG	0x200
+#define DF_F5_FABRIC_ID		0x180
+
+/* DF function numbers for sibling device access (internal use). */
+#define HYGON_DF_F1	1	/* SystemCfg: socket and die identity */
+#define HYGON_DF_F5	5	/* FabricId: real DFID on Model 06h-08h */
+
+/* DF sibling device IDs used only within this file for identity reads. */
+#define PCI_DEVICE_ID_HYGON_18H_M04H_DF_F1	0x1491
+#define PCI_DEVICE_ID_HYGON_18H_M05H_DF_F1	0x14b1
+#define PCI_DEVICE_ID_HYGON_18H_M06H_DF_F5	0x14b5
+
+/*
+ * Cached identity for one DF instance.  After sorting, CDDs occupy
+ * nodes[0..num_cdd-1].
+ *
+ * socket_id + dfid together uniquely identify a DF node in hardware.
+ * Unlike AMD (fixed PCI slot == node ID), Hygon DF nodes sit at
+ * platform-assigned slots, so consumers can use socket_id + dfid as
+ * the hardware location of a DF node.
+ */
+struct hygon_node {
+	struct pci_dev	*misc;		/* DF function 3 */
+	struct pci_dev	*link;		/* DF function 4 */
+	u8		socket_id;	/* F1x200[30:28] */
+	u8		dfid;		/* model-dependent DFID */
+	bool		is_cdd;		/* DFID >= 4 */
+};
+
+struct hygon_node_cache {
+	struct hygon_node	*nodes;		/* sorted: CDD first, then IOD */
+	u16			num_nodes;	/* CDD + IOD = total */
+	u16			num_cdd;	/* CDD only */
+	u16			num_sockets;
+	u16			cdd_per_socket;	/* num_cdd / num_sockets */
+
+	/*
+	 * Set once the DF node cache, the identity / lookup APIs and DF
+	 * function (F3/F4) PCI access are usable.  SMN read/write have a
+	 * separate gate (hygon_smn_exclusive).
+	 */
+	bool			ready;
+
+	/*
+	 * Set when hygon_cpu_to_df_node() is supported on the running
+	 * model.  Independent of @ready: the phys_node_id encoding is
+	 * model-specific (supported on Hygon Fam18h Model 0x04-0x08), so
+	 * other models keep the rest of the node stack working while
+	 * hygon_cpu_to_df_node() returns -ENODEV.
+	 */
+	bool			cpu_map_supported;
+};
+
+struct hygon_df_id {
+	u8	socket_id;
+	u8	dfid;
+};
+
+/* Model-specific DF sibling device IDs for reading node identity. */
+struct hygon_df_func_ids {
+	u8	model_start;
+	u8	model_end;
+	u16	f1_id;
+	u16	f5_id;			/* 0 = not available */
+};
+
+/* DF misc (F3) device IDs for all supported Hygon Family 0x18 models. */
+static const struct pci_device_id hygon_nb_misc_ids[] = {
+	{ PCI_DEVICE(PCI_VENDOR_ID_HYGON, PCI_DEVICE_ID_HYGON_18H_M04H_DF_F3) },
+	{ PCI_DEVICE(PCI_VENDOR_ID_HYGON, PCI_DEVICE_ID_HYGON_18H_M04H_DF_F3B) },
+	{ PCI_DEVICE(PCI_VENDOR_ID_HYGON, PCI_DEVICE_ID_HYGON_18H_M05H_DF_F3) },
+	{}
+};
+
+/* DF link (F4) device IDs, parallel to hygon_nb_misc_ids[]. */
+static const struct pci_device_id hygon_nb_link_ids[] = {
+	{ PCI_DEVICE(PCI_VENDOR_ID_HYGON, PCI_DEVICE_ID_HYGON_18H_M04H_DF_F4) },
+	{ PCI_DEVICE(PCI_VENDOR_ID_HYGON, PCI_DEVICE_ID_HYGON_18H_M04H_DF_F4B) },
+	{ PCI_DEVICE(PCI_VENDOR_ID_HYGON, PCI_DEVICE_ID_HYGON_18H_M05H_DF_F4) },
+	{}
+};
+
+/*
+ * Map Hygon Fam18h model ranges onto the DF sibling functions used to
+ * read node identity.  Model 5 may still expose the Model 4 F1 device
+ * id on mixed silicon, which is handled separately in
+ * hygon_read_df_reg().
+ */
+static const struct hygon_df_func_ids hygon_df_table[] = {
+	{ 0x04, 0x04, PCI_DEVICE_ID_HYGON_18H_M04H_DF_F1, 0 },
+	{ 0x05, 0x05, PCI_DEVICE_ID_HYGON_18H_M05H_DF_F1, 0 },
+	{ 0x06, 0x08, PCI_DEVICE_ID_HYGON_18H_M05H_DF_F1,
+		      PCI_DEVICE_ID_HYGON_18H_M06H_DF_F5 },
+	{}
+};
+
+/*
+ * Global cache.  BSS-initialised: both ready flags are false until
+ * hygon_build_cache() sets them.  Runtime APIs short-circuit on their
+ * respective flag, so any caller arriving before fs_initcall sees a
+ * documented failure value (0 / NULL / -ENODEV).
+ */
+static struct hygon_node_cache hygon_cache;
+
+static void __init hygon_dump_nodes(const struct hygon_node_cache *cache,
+				    const char *phase)
+{
+	u16 i;
+
+	pr_debug("%s: %u nodes\n", phase, cache->num_nodes);
+
+	for (i = 0; i < cache->num_nodes; i++) {
+		const struct hygon_node *node = &cache->nodes[i];
+
+		pr_debug("%s: node[%u] %04x:%02x:%02x.%u socket=%u dfid=%u type=%s\n",
+			 phase, i, pci_domain_nr(node->misc->bus),
+			 node->misc->bus->number,
+			 PCI_SLOT(node->misc->devfn),
+			 PCI_FUNC(node->misc->devfn),
+			 node->socket_id, node->dfid,
+			 node->is_cdd ? "CDD" : "IOD");
+	}
+}
+
+/*
+ * Iterate Hygon PCI devices, returning the next one that matches @ids.
+ * Follows the pci_get_device() convention: @from is consumed (its
+ * reference is dropped) and the returned device has an elevated
+ * reference count.
+ */
+static struct pci_dev * __init next_hygon_dev(struct pci_dev *from,
+					      const struct pci_device_id *ids)
+{
+	while ((from = pci_get_device(PCI_VENDOR_ID_HYGON, PCI_ANY_ID, from))) {
+		if (pci_match_id(ids, from))
+			return from;
+	}
+
+	return NULL;
+}
+
+/*
+ * Find the DF link (function 4) sibling of a DF misc (function 3)
+ * device.  Both functions share the same PCI bus and slot.
+ */
+static struct pci_dev * __init hygon_get_link(struct pci_dev *misc)
+{
+	struct pci_dev *link;
+
+	link = pci_get_domain_bus_and_slot(pci_domain_nr(misc->bus),
+					   misc->bus->number,
+					   PCI_DEVFN(PCI_SLOT(misc->devfn),
+						     HYGON_DF_F4));
+	if (!link)
+		return NULL;
+
+	if (!pci_match_id(hygon_nb_link_ids, link)) {
+		pci_dev_put(link);
+		return NULL;
+	}
+
+	return link;
+}
+
+/*
+ * Look up the DF sibling device IDs for the current boot CPU model.
+ *
+ * This is the only place that decides node-layer model support: a
+ * NULL return means the running CPU is not a Hygon Fam18h model the
+ * node layer supports.  Range matching is kept private here; the node
+ * layer does not expose a model-range helper.
+ */
+static const struct hygon_df_func_ids * __init hygon_get_df_func_ids(void)
+{
+	const struct hygon_df_func_ids *entry;
+	u8 model;
+
+	if (!is_hygon_f18h())
+		return NULL;
+
+	model = boot_cpu_data.x86_model;
+
+	for (entry = hygon_df_table; entry->f1_id; entry++) {
+		if (model >= entry->model_start && model <= entry->model_end)
+			return entry;
+	}
+
+	return NULL;
+}
+
+/*
+ * Read a config register from a DF sibling function on the same PCI
+ * slot as @misc.  Only functions 1 (F1, SystemCfg) and 5 (F5,
+ * FabricId) are supported.
+ */
+static int __init hygon_read_df_reg(struct pci_dev *misc, u8 func,
+				    int offset, u32 *value)
+{
+	const struct hygon_df_func_ids *ids;
+	struct pci_dev *sibling;
+	u16 expected_device;
+	int err;
+
+	ids = hygon_get_df_func_ids();
+	if (!ids)
+		return -ENODEV;
+
+	if (func == HYGON_DF_F1) {
+		expected_device = ids->f1_id;
+
+		/*
+		 * Model 5 can expose an older mixed-silicon variant where
+		 * the F1 sibling still uses the M04H device ID.
+		 */
+		if (boot_cpu_data.x86_model == 0x5 &&
+		    misc->device != PCI_DEVICE_ID_HYGON_18H_M05H_DF_F3)
+			expected_device = PCI_DEVICE_ID_HYGON_18H_M04H_DF_F1;
+	} else if (func == HYGON_DF_F5) {
+		expected_device = ids->f5_id;
+	} else {
+		return -EINVAL;
+	}
+
+	if (!expected_device)
+		return -ENODEV;
+
+	sibling = pci_get_domain_bus_and_slot(pci_domain_nr(misc->bus),
+					      misc->bus->number,
+					      PCI_DEVFN(PCI_SLOT(misc->devfn),
+							func));
+	if (!sibling)
+		return -ENODEV;
+
+	if (sibling->vendor != PCI_VENDOR_ID_HYGON ||
+	    sibling->device != expected_device) {
+		pci_dev_put(sibling);
+		return -ENODEV;
+	}
+
+	err = pci_read_config_dword(sibling, offset, value);
+	pci_dev_put(sibling);
+
+	if (err) {
+		pr_warn("error reading %04x:%02x:%02x.%u offset 0x%x\n",
+			pci_domain_nr(misc->bus), misc->bus->number,
+			PCI_SLOT(misc->devfn), func, offset);
+		return pcibios_err_to_errno(err);
+	}
+
+	/*
+	 * A successful config read can still return the all-ones PCI error
+	 * response; reject it so 0xffffffff is never parsed as
+	 * socket_id/dfid/SocketPresent.
+	 */
+	if (PCI_POSSIBLE_ERROR(*value)) {
+		pr_warn("error response reading %04x:%02x:%02x.%u offset 0x%x\n",
+			pci_domain_nr(misc->bus), misc->bus->number,
+			PCI_SLOT(misc->devfn), func, offset);
+		return -ENODEV;
+	}
+
+	return 0;
+}
+
+/*
+ * Read the hardware identity for one DF misc device from its sibling
+ * functions.
+ *
+ * All models expose F1x200 (SystemCfg): socket_id from [30:28] and a
+ * die identifier (MyDieId) from [23:20].
+ * On Model 06h-08h MyDieId differs from the DFID used by UMC and SMN
+ * addressing, so an additional F5x180 (FabricId) read is required to
+ * obtain the real DFID from [19:16].
+ *
+ * All DF instances on a Hygon system are the same model, so
+ * boot_cpu_data.x86_model is representative for all devices.
+ */
+static int __init hygon_read_df_id(struct pci_dev *misc,
+				   struct hygon_df_id *id)
+{
+	const struct hygon_df_func_ids *ids = hygon_get_df_func_ids();
+	u32 reg;
+	int ret;
+
+	if (!ids)
+		return -ENODEV;
+
+	/*
+	 * F1x200; hygon_read_df_reg() resolves the F1 sibling device ID
+	 * (including the Model 5 mixed-silicon variant) from the DF table.
+	 */
+	ret = hygon_read_df_reg(misc, HYGON_DF_F1, DF_F1_SYSTEM_CFG, &reg);
+	if (ret)
+		return ret;
+
+	id->socket_id = (reg >> 28) & 0x7;
+	id->dfid      = (reg >> 20) & 0xf;
+
+	/*
+	 * Models that carry the real DFID in F5x180 (M06-08) advertise an
+	 * F5 sibling device ID in the DF table.  Use that presence rather
+	 * than a raw model-number range to decide the DFID source.
+	 */
+	if (ids->f5_id) {
+		ret = hygon_read_df_reg(misc, HYGON_DF_F5, DF_F5_FABRIC_ID,
+					&reg);
+		if (ret)
+			return ret;
+		id->dfid = (reg >> 16) & 0xf;
+	}
+
+	return 0;
+}
+
+/*
+ * Read the system-wide SocketPresent mask from F1x200[7:0].
+ *
+ * SocketPresent reports which sockets are populated; it is a platform
+ * property identical for every DF instance, so one read from the first
+ * DF misc device is sufficient.  It is kept separate from the
+ * per-node identity in hygon_read_df_id() to avoid mixing system-wide
+ * topology with per-node fields.
+ *
+ * A successful PCI config read yields the whole register, so on the
+ * supported models -- which all provide SocketPresent -- a zero mask
+ * is a hard error rather than something to tolerate.
+ */
+static int __init hygon_read_socket_present(struct pci_dev *misc,
+					    u8 *socket_present)
+{
+	u32 reg;
+	int ret;
+
+	ret = hygon_read_df_reg(misc, HYGON_DF_F1, DF_F1_SYSTEM_CFG, &reg);
+	if (ret)
+		return ret;
+
+	*socket_present = reg & 0xff;
+	if (!*socket_present) {
+		pr_warn("SocketPresent is zero\n");
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static void __init hygon_release_nodes(struct hygon_node *nodes, u16 count)
+{
+	u16 i;
+
+	for (i = 0; i < count; i++) {
+		pci_dev_put(nodes[i].misc);
+		pci_dev_put(nodes[i].link);
+	}
+
+	kfree(nodes);
+}
+
+/*
+ * Walk all DF misc (F3) devices and read per-node identity (socket_id,
+ * dfid) from each, collecting them into a flat array.  The system-wide
+ * SocketPresent mask is sampled once, and the enumerated socket set is
+ * validated against it; socket IDs must also be dense (0..N-1).
+ */
+static int __init hygon_collect_nodes(struct hygon_node_cache *cache)
+{
+	struct hygon_node *nodes;
+	struct pci_dev *misc;
+	u16 capacity = 0, count = 0;
+	u8 observed_socket_mask = 0;
+	u8 socket_present_mask = 0;
+	int ret;
+
+	misc = NULL;
+	while ((misc = next_hygon_dev(misc, hygon_nb_misc_ids)))
+		capacity++;
+
+	if (!capacity)
+		return -ENODEV;
+
+	nodes = kcalloc(capacity, sizeof(*nodes), GFP_KERNEL);
+	if (!nodes)
+		return -ENOMEM;
+
+	misc = NULL;
+	while ((misc = next_hygon_dev(misc, hygon_nb_misc_ids))) {
+		struct hygon_df_id id;
+		struct pci_dev *link;
+
+		link = hygon_get_link(misc);
+		if (!link) {
+			pci_dev_put(misc);
+			ret = -ENODEV;
+			goto fail;
+		}
+
+		ret = hygon_read_df_id(misc, &id);
+		if (ret) {
+			pci_dev_put(link);
+			pci_dev_put(misc);
+			goto fail;
+		}
+
+		/*
+		 * SocketPresent is system-wide, so sample it exactly once,
+		 * from the first DF misc whose identity read succeeded.
+		 */
+		if (!count) {
+			ret = hygon_read_socket_present(misc,
+							&socket_present_mask);
+			if (ret) {
+				pci_dev_put(link);
+				pci_dev_put(misc);
+				goto fail;
+			}
+		}
+
+		if (count >= capacity) {
+			pci_dev_put(link);
+			pci_dev_put(misc);
+			ret = -ENODEV;
+			goto fail;
+		}
+
+		pr_debug("collect: %04x:%02x:%02x.%u socket=%u dfid=%u\n",
+			 pci_domain_nr(misc->bus), misc->bus->number,
+			 PCI_SLOT(misc->devfn), PCI_FUNC(misc->devfn),
+			 id.socket_id, id.dfid);
+
+		nodes[count].misc      = pci_dev_get(misc);
+		nodes[count].link      = link;
+		nodes[count].socket_id = id.socket_id;
+		nodes[count].dfid      = id.dfid;
+		nodes[count].is_cdd    = (id.dfid >= HYGON_CDD_DFID_BASE);
+		count++;
+
+		observed_socket_mask |= BIT(id.socket_id);
+	}
+
+	if (!count) {
+		ret = -ENODEV;
+		goto fail;
+	}
+
+	/*
+	 * Validate enumeration completeness against the hardware-declared
+	 * socket set: a mismatch means an entire socket's DF misc devices
+	 * were missed.  socket_present_mask is already guaranteed non-zero
+	 * by hygon_read_socket_present().
+	 */
+	if (observed_socket_mask != socket_present_mask) {
+		pr_warn("SocketPresent mismatch: observed=0x%x present=0x%x\n",
+			observed_socket_mask, socket_present_mask);
+		ret = -EINVAL;
+		goto fail;
+	}
+
+	cache->nodes       = nodes;
+	cache->num_nodes   = count;
+	cache->num_sockets = hweight8(socket_present_mask);
+
+	if (socket_present_mask != GENMASK(cache->num_sockets - 1, 0)) {
+		pr_warn("sparse socket IDs not supported (SocketPresent=0x%x)\n",
+			socket_present_mask);
+		ret = -EINVAL;
+		goto fail;
+	}
+
+	return 0;
+
+fail:
+	hygon_release_nodes(nodes, count);
+	cache->nodes = NULL;
+	cache->num_nodes = 0;
+	return ret;
+}
+
+/*
+ * Sort CDD nodes before IOD nodes, then order by hardware
+ * (socket_id, dfid).
+ */
+static int __init hygon_node_cmp(const void *a, const void *b)
+{
+	const struct hygon_node *left = a;
+	const struct hygon_node *right = b;
+
+	if (left->is_cdd != right->is_cdd)
+		return right->is_cdd - left->is_cdd;
+
+	if (left->socket_id != right->socket_id)
+		return (int)left->socket_id - right->socket_id;
+
+	return (int)left->dfid - (int)right->dfid;
+}
+
+/*
+ * Classify the sorted node array and validate the CDD layout.
+ *
+ * The initial dense CDD model expects each socket to contribute the
+ * same number of compute dies, so reject topologies that do not
+ * satisfy that invariant.
+ */
+static int __init hygon_sort_and_classify(struct hygon_node_cache *cache)
+{
+	u16 cdd_per_socket;
+	u16 i;
+	u8 per_sock_count[HYGON_MAX_SOCKETS] = { 0 };
+
+	hygon_dump_nodes(cache, "before-sort");
+
+	sort(cache->nodes, cache->num_nodes, sizeof(*cache->nodes),
+	     hygon_node_cmp, NULL);
+
+	for (i = 0; i < cache->num_nodes; i++) {
+		if (!cache->nodes[i].is_cdd)
+			break;
+	}
+
+	cache->num_cdd = i;
+
+	if (!cache->num_cdd)
+		return -ENODEV;
+
+	if (cache->num_cdd % cache->num_sockets) {
+		pr_warn("CDD count %u not divisible by %u sockets\n",
+			cache->num_cdd, cache->num_sockets);
+		return -EINVAL;
+	}
+
+	cdd_per_socket = cache->num_cdd / cache->num_sockets;
+	if (!cdd_per_socket)
+		return -EINVAL;
+
+	for (i = 0; i < cache->num_cdd; i++) {
+		u8 socket_id = cache->nodes[i].socket_id;
+
+		if (socket_id >= cache->num_sockets)
+			return -EINVAL;
+		per_sock_count[socket_id]++;
+	}
+
+	for (i = 0; i < cache->num_sockets; i++) {
+		if (per_sock_count[i] != cdd_per_socket) {
+			pr_warn("socket %u: %u CDDs, expected %u\n",
+				i, per_sock_count[i], cdd_per_socket);
+			return -EINVAL;
+		}
+	}
+
+	cache->cdd_per_socket = cdd_per_socket;
+
+	hygon_dump_nodes(cache, "after-sort");
+
+	return 0;
+}
+
+/*
+ * Validate that the running CPU implements the Hygon Fam18h phys_node_id
+ * encoding used by hygon_phys_nid_to_df_node():
+ *
+ *   bits [7:4] = socket_id
+ *   bits [3:0] = local CDD index within socket, in ascending DFID order
+ *
+ * Supported on the same models the node layer enumerates (the DF
+ * table decides this); other models stay unsupported until their
+ * encoding is confirmed, so hygon_cpu_to_df_node() returns -ENODEV
+ * there while the rest of the node stack keeps working.
+ *
+ * Also requires CPUID 0x8000001E ECX[10:8] + 1 (NodesPerProcessor) to
+ * match cache->cdd_per_socket, detecting a node-count mismatch between
+ * CPUID and DF enumeration before any caller acts on a translated index.
+ */
+static bool __init hygon_cpu_map_validate(const struct hygon_node_cache *cache)
+{
+	unsigned int nodes_per_socket;
+
+	if (!hygon_get_df_func_ids())
+		return false;
+
+	nodes_per_socket = ((cpuid_ecx(0x8000001e) >> 8) & 0x7) + 1;
+	if (nodes_per_socket != cache->cdd_per_socket) {
+		pr_warn("CPU-to-DF map disabled: CPUID nodes_per_socket=%u != cdd_per_socket=%u\n",
+			nodes_per_socket, cache->cdd_per_socket);
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Translate a Hygon Fam18h phys_node_id (CPUID 8000001E ECX[7:0]) to a
+ * dense DF CDD index.  On supported models, the node layer uses
+ * this encoding:
+ *
+ *   phys_node_id = (socket_id << 4) | local_cdd_index_in_dfid_order
+ *
+ * The cache->nodes[] CDD region is sorted by (socket_id ASC, dfid ASC),
+ * so walk it and return the actual cache index of the CDD whose
+ * socket_id matches and whose socket-local ordinal is @local.
+ *
+ * A socket or local index with no matching CDD maps to -ENODEV, which
+ * also covers a model not in the supported range (cpu_map_supported ==
+ * false).
+ */
+static int hygon_phys_nid_to_df_node(unsigned int phys_nid)
+{
+	unsigned int socket = phys_nid >> 4;
+	unsigned int local = phys_nid & 0xf;
+	unsigned int ordinal = 0;
+	u16 i;
+
+	if (!hygon_cache.cpu_map_supported)
+		return -ENODEV;
+
+	for (i = 0; i < hygon_cache.num_cdd; i++) {
+		if (hygon_cache.nodes[i].socket_id != socket)
+			continue;
+
+		if (ordinal++ == local)
+			return i;
+	}
+
+	return -ENODEV;
+}
+
+/*
+ * Build the global DF node cache.
+ *
+ * Called exactly once from hygon_node_init() at fs_initcall, so no
+ * locking is required.  Two independent flags:
+ *
+ *   cache.ready              set after collect + sort succeed.  DF
+ *                            identity / lookup APIs and DF function
+ *                            PCI access work from here.  SMN
+ *                            read/write are gated separately on
+ *                            hygon_smn_exclusive.
+ *   cache.cpu_map_supported  set when the running model exposes the
+ *                            phys_node_id encoding consumed by
+ *                            hygon_cpu_to_df_node().  Unset leaves the
+ *                            rest of the node stack working and only
+ *                            disables that single API.
+ *
+ * Runtime APIs short-circuit on their respective flag, so pre-init
+ * callers see a documented failure value (0 / NULL / -ENODEV).
+ */
+static int __init hygon_build_cache(void)
+{
+	int err;
+
+	err = hygon_collect_nodes(&hygon_cache);
+	if (err)
+		return err;
+
+	err = hygon_sort_and_classify(&hygon_cache);
+	if (err)
+		goto fail;
+
+	hygon_cache.ready = true;
+	hygon_cache.cpu_map_supported = hygon_cpu_map_validate(&hygon_cache);
+
+	return 0;
+
+fail:
+	hygon_release_nodes(hygon_cache.nodes, hygon_cache.num_nodes);
+	hygon_cache.nodes = NULL;
+	hygon_cache.num_nodes = 0;
+	hygon_cache.num_cdd = 0;
+	hygon_cache.num_sockets = 0;
+	hygon_cache.cdd_per_socket = 0;
+	return err;
+}
+
+u16 hygon_node_num(void)
+{
+	return hygon_cache.ready ? hygon_cache.num_nodes : 0;
+}
+EXPORT_SYMBOL_GPL(hygon_node_num);
+
+u16 hygon_cdd_num(void)
+{
+	return hygon_cache.ready ? hygon_cache.num_cdd : 0;
+}
+EXPORT_SYMBOL_GPL(hygon_cdd_num);
+
+int hygon_node_get_info(u16 node, struct hygon_node_info *info)
+{
+	const struct hygon_node *n;
+
+	if (!info)
+		return -EINVAL;
+
+	if (!hygon_cache.ready)
+		return -ENODEV;
+
+	if (node >= hygon_cache.num_nodes)
+		return -EINVAL;
+
+	n = &hygon_cache.nodes[node];
+	info->socket_id = n->socket_id;
+	info->dfid      = n->dfid;
+	info->type      = n->is_cdd ? HYGON_NODE_CDD : HYGON_NODE_IOD;
+	return 0;
+}
+EXPORT_SYMBOL_GPL(hygon_node_get_info);
+
+int hygon_cpu_to_df_node(unsigned int cpu)
+{
+	if (cpu >= nr_cpu_ids)
+		return -EINVAL;
+
+	return hygon_phys_nid_to_df_node(topology_amd_node_id(cpu));
+}
+EXPORT_SYMBOL_GPL(hygon_cpu_to_df_node);
+
+struct pci_dev *hygon_node_get_func(u16 node, u8 func)
+{
+	if (!hygon_cache.ready)
+		return NULL;
+
+	if (node >= hygon_cache.num_nodes)
+		return NULL;
+
+	switch (func) {
+	case HYGON_DF_F3:
+		return pci_dev_get(hygon_cache.nodes[node].misc);
+	case HYGON_DF_F4:
+		return pci_dev_get(hygon_cache.nodes[node].link);
+	default:
+		return NULL;
+	}
+}
+EXPORT_SYMBOL_GPL(hygon_node_get_func);
+
+int hygon_pci_dev_to_df_node(struct pci_dev *pdev)
+{
+	u16 i;
+
+	if (!pdev)
+		return -EINVAL;
+
+	if (!hygon_cache.ready)
+		return -ENODEV;
+
+	for (i = 0; i < hygon_cache.num_nodes; i++) {
+		struct pci_dev *misc = hygon_cache.nodes[i].misc;
+
+		if (pci_domain_nr(misc->bus) == pci_domain_nr(pdev->bus) &&
+		    misc->bus->number       == pdev->bus->number      &&
+		    PCI_SLOT(misc->devfn)   == PCI_SLOT(pdev->devfn))
+			return i;
+	}
+
+	return -ENODEV;
+}
+EXPORT_SYMBOL_GPL(hygon_pci_dev_to_df_node);
+
+/*
+ * Single Hygon fs_initcall: build the DF node cache, then run any
+ * Hygon-specific subsequent setup.
+ *
+ * A failed cache build short-circuits the rest of the initcall.
+ * Failures in optional subsequent steps are logged but do not fail the
+ * initcall, because the DF identity and DF function access APIs
+ * remain useful even when later setup fails.
+ */
+static int __init hygon_node_init(void)
+{
+	int ret;
+
+	/*
+	 * Gate on node-layer model support, not just family.  Unsupported
+	 * Hygon Fam18h models (and non-Hygon CPUs) return without building
+	 * the cache, instead of entering the build and failing later with
+	 * a misleading "build failed".  Log unsupported Hygon models so
+	 * the missing node layer is visible in dmesg.
+	 */
+	if (!hygon_get_df_func_ids()) {
+		if (is_hygon_f18h())
+			pr_info("Hygon Fam18h model 0x%x is not supported by the node layer\n",
+				boot_cpu_data.x86_model);
+		return 0;
+	}
+
+	ret = hygon_build_cache();
+	if (ret) {
+		pr_warn("DF node cache build failed: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+}
+fs_initcall(hygon_node_init);
-- 
2.43.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH v3 5/6] x86/hygon: Add Family 0x18 SMN access
  2026-07-09  7:55 [PATCH v3 0/6] x86/hygon: Add Family 0x18 DF node enumeration and SMN access Lin Wang
                   ` (3 preceding siblings ...)
  2026-07-09  7:55 ` [PATCH v3 4/6] x86/hygon: Add Family 0x18 DF node enumeration Lin Wang
@ 2026-07-09  7:55 ` Lin Wang
  2026-07-09  7:55 ` [PATCH v3 6/6] x86/hygon: Add SMN debugfs interface Lin Wang
  5 siblings, 0 replies; 8+ messages in thread
From: Lin Wang @ 2026-07-09  7:55 UTC (permalink / raw)
  To: yazen.ghannam, mario.limonciello, Borislav Petkov
  Cc: tglx, mingo, x86, linux-kernel

Add Hygon-specific System Management Network (SMN) read/write
primitives on top of the DF node cache.  SMN access on Hygon Fam18h
goes through host-bridge PCI config space index/data register pairs
shared per-socket.

Public API (declared in asm/hygon/node.h):

  int __must_check hygon_smn_read(u16 node, u32 address, u32 *value);
  int __must_check hygon_smn_write(u16 node, u32 address, u32 value);

Internals added to arch/x86/kernel/hygon_node.c:

  - hygon_smn_roots[], hygon_smn_num_nodes, hygon_smn_exclusive,
    hygon_smn_mutex: per-node SMN state, populated at fs_initcall and
    held for the lifetime of the system.

  - __hygon_smn_rw(): index/data register transaction, serialized by
    hygon_smn_mutex.

  - hygon_smn_setup(): walks Hygon host-bridge devices, reserves
    their PCI config space exclusively, keeps one root per socket
    (grouped by enumeration order, relying on contiguous per-socket
    root enumeration), and expands that selection into a
    per-node array.  The translation node -> socket -> root uses
    hygon_node_socket(), an internal helper that reads the socket_id
    field from the DF node cache.

  - hygon_get_next_root(), hygon_release_root_regions(),
    hygon_node_socket(), hygon_socket_num(): SMN setup helpers,
    marked __init where appropriate.

hygon_node_init() now calls hygon_build_cache() and then
hygon_smn_setup().  A failed cache build short-circuits SMN; a
failed SMN setup is logged but does not fail the initcall, because
the DF identity and DF function access APIs remain useful even
without SMN.

The SMN index/data transaction follows the established AMD SMN access
pattern in arch/x86/kernel/amd_node.c: serialize the index/data PCI
config-space pair with a mutex.  Root discovery, socket binding and
dense DF-node mapping are Hygon-specific and stay in hygon_node.c.

This patch depends on the AMD-side cleanup in
"x86/amd_node: Restrict SMN root enumeration to AMD vendor".
Without that cleanup, amd_smn_init() would reserve Hygon root PCI
config space first and hygon_smn_setup() would fail with -EEXIST.

Signed-off-by: Lin Wang <wanglin@open-hieco.net>
---
 arch/x86/include/asm/hygon/node.h |  34 ++++
 arch/x86/kernel/hygon_node.c      | 308 ++++++++++++++++++++++++++++++
 2 files changed, 342 insertions(+)

diff --git a/arch/x86/include/asm/hygon/node.h b/arch/x86/include/asm/hygon/node.h
index bb8168beb288..984e59a32472 100644
--- a/arch/x86/include/asm/hygon/node.h
+++ b/arch/x86/include/asm/hygon/node.h
@@ -123,6 +123,30 @@ struct pci_dev *hygon_node_get_func(u16 node, u8 func);
  */
 int hygon_pci_dev_to_df_node(struct pci_dev *pdev);
 
+/**
+ * hygon_smn_read() - read a 32-bit value from a Hygon SMN address
+ * @node: DF node index in [0, hygon_node_num())
+ * @address: SMN address
+ * @value: output value
+ *
+ * Return: 0 on success, -EINVAL if @value is NULL, -ENODEV if Hygon
+ * SMN is not initialised or @node is out of range, or a negative
+ * errno from the underlying PCI config access.
+ */
+int __must_check hygon_smn_read(u16 node, u32 address, u32 *value);
+
+/**
+ * hygon_smn_write() - write a 32-bit value to a Hygon SMN address
+ * @node: DF node index in [0, hygon_node_num())
+ * @address: SMN address
+ * @value: value to write
+ *
+ * Return: 0 on success, -ENODEV if Hygon SMN is not initialised or
+ * @node is out of range, or a negative errno from the underlying
+ * PCI config access.
+ */
+int __must_check hygon_smn_write(u16 node, u32 address, u32 value);
+
 #else /* !CONFIG_HYGON_NODE */
 
 static inline u16 hygon_node_num(void)
@@ -155,6 +179,16 @@ static inline int hygon_pci_dev_to_df_node(struct pci_dev *pdev)
 	return -ENODEV;
 }
 
+static inline int hygon_smn_read(u16 node, u32 address, u32 *value)
+{
+	return -ENODEV;
+}
+
+static inline int hygon_smn_write(u16 node, u32 address, u32 value)
+{
+	return -ENODEV;
+}
+
 #endif /* CONFIG_HYGON_NODE */
 
 /* Inline helpers, available regardless of CONFIG_HYGON_NODE. */
diff --git a/arch/x86/kernel/hygon_node.c b/arch/x86/kernel/hygon_node.c
index d0c861f61d1f..09c08aa0c2c7 100644
--- a/arch/x86/kernel/hygon_node.c
+++ b/arch/x86/kernel/hygon_node.c
@@ -25,9 +25,11 @@
 #define pr_fmt(fmt) "hygon_node: " fmt
 
 #include <linux/bitops.h>
+#include <linux/cleanup.h>
 #include <linux/cpu.h>
 #include <linux/export.h>
 #include <linux/init.h>
+#include <linux/mutex.h>
 #include <linux/pci.h>
 #include <linux/pci_ids.h>
 #include <linux/processor.h>
@@ -811,6 +813,308 @@ int hygon_pci_dev_to_df_node(struct pci_dev *pdev)
 }
 EXPORT_SYMBOL_GPL(hygon_pci_dev_to_df_node);
 
+/*
+ * SMN index/data register pair offsets in the host-bridge PCI config
+ * space.  Reads and writes to a (node, address) pair are issued as a
+ * two-step transaction: write the SMN address to the index register,
+ * then read or write the value at the data register.
+ */
+#define HYGON_SMN_INDEX_OFFSET	0x60
+#define HYGON_SMN_DATA_OFFSET	0x64
+
+/*
+ * Runtime SMN state.  hygon_smn_exclusive is false in BSS until
+ * hygon_smn_setup() succeeds; runtime API short-circuits on !exclusive.
+ */
+static struct pci_dev	**hygon_smn_roots;
+static u16		hygon_smn_num_nodes;
+static bool		hygon_smn_exclusive;
+static DEFINE_MUTEX(hygon_smn_mutex);
+
+/* Internal cache accessors used by SMN setup. */
+static u8 __init hygon_node_socket(u16 node)
+{
+	if (!hygon_cache.ready || node >= hygon_cache.num_nodes)
+		return U8_MAX;
+	return hygon_cache.nodes[node].socket_id;
+}
+
+static u16 __init hygon_socket_num(void)
+{
+	return hygon_cache.ready ? hygon_cache.num_sockets : 0;
+}
+
+/*
+ * Walk PCI host-bridge devices matching the Hygon vendor.  The SMN
+ * index/data registers live in function 0 of each root complex (class
+ * PCI_CLASS_BRIDGE_HOST, devfn 0).
+ */
+static struct pci_dev * __init hygon_get_next_root(struct pci_dev *root)
+{
+	while ((root = pci_get_class(PCI_CLASS_BRIDGE_HOST << 8, root))) {
+		if (root->devfn)
+			continue;
+		if (root->vendor != PCI_VENDOR_ID_HYGON)
+			continue;
+		break;
+	}
+	return root;
+}
+
+static void __init hygon_release_root_regions(struct pci_dev **roots,
+					      u16 count)
+{
+	u16 i;
+
+	for (i = 0; i < count; i++)
+		pci_release_config_region(roots[i], 0, PCI_CFG_SPACE_SIZE);
+}
+
+/*
+ * Hygon SMN setup.
+ *
+ * On Hygon Fam18h, SMN root devices are shared per-socket: all nodes
+ * (CDD and IOD) on the same socket use the same host-bridge root for
+ * SMN access.  These root devices are transport endpoints, not
+ * topology identifiers: socket_id / DFID come from DF registers,
+ * while the selected root only determines which socket-local SMN
+ * index/data pair is used by hygon_smn_read() / hygon_smn_write().
+ * Any root within a socket-equivalent group is sufficient, but
+ * binding a node to a root from another socket would route the
+ * transaction through the wrong SMN ingress.
+ *
+ * We therefore keep one root per socket -- grouping roots by
+ * enumeration order, which relies on the platform enumerating each
+ * socket's roots contiguously -- and expand that per-socket selection
+ * into a per-node root array.  The root's own socket id is not read
+ * back.
+ *
+ * On success, hygon_smn_exclusive is set true and the reserved PCI
+ * config regions are kept for the lifetime of the system.  On
+ * failure, partial reservations are released and all allocations
+ * freed; hygon_smn_exclusive stays false so the read/write primitive
+ * returns -ENODEV for all subsequent calls.
+ */
+static int __init hygon_smn_setup(void)
+{
+	struct pci_dev *socket_roots[HYGON_MAX_SOCKETS] = { };
+	struct pci_dev **reserved_roots, **roots, *root;
+	u16 count, num_roots, roots_per_socket, node, num_nodes;
+	u16 num_sockets, reserved, socket;
+	u8 socket_id;
+	int ret;
+
+	num_roots = 0;
+	root = NULL;
+	while ((root = hygon_get_next_root(root)))
+		num_roots++;
+
+	pr_debug("Found %u Hygon SMN root devices\n", num_roots);
+
+	if (!num_roots)
+		return -ENODEV;
+
+	num_nodes = hygon_node_num();
+	if (!num_nodes)
+		return -ENODEV;
+
+	num_sockets = hygon_socket_num();
+	if (!num_sockets)
+		return -ENODEV;
+
+	if (num_sockets > ARRAY_SIZE(socket_roots)) {
+		pr_err("Socket count %u exceeds maximum %zu\n",
+		       num_sockets, ARRAY_SIZE(socket_roots));
+		return -EINVAL;
+	}
+
+	if (num_roots % num_sockets) {
+		pr_err("Root count %u not divisible by socket count %u\n",
+		       num_roots, num_sockets);
+		return -ENODEV;
+	}
+
+	roots = kcalloc(num_nodes, sizeof(*roots), GFP_KERNEL);
+	if (!roots)
+		return -ENOMEM;
+
+	reserved_roots = kcalloc(num_roots, sizeof(*reserved_roots),
+				 GFP_KERNEL);
+	if (!reserved_roots) {
+		kfree(roots);
+		return -ENOMEM;
+	}
+
+	/*
+	 * Keep the first of every roots_per_socket consecutive roots and
+	 * skip the rest.  This groups roots by enumeration order, relying on
+	 * the platform enumerating each socket's roots contiguously.  Roots
+	 * within the same socket are redundant SMN ingress points.
+	 */
+	roots_per_socket = num_roots / num_sockets;
+	socket = 0;
+	reserved = 0;
+	count = 0;
+	root = NULL;
+	while ((root = hygon_get_next_root(root))) {
+		/* Bail out if the walk yields more roots than first counted. */
+		if (reserved >= num_roots) {
+			ret = -ENODEV;
+			pci_dev_put(root);
+			goto err_release;
+		}
+
+		pci_dbg(root, "Reserving PCI config space\n");
+
+		/*
+		 * Reserve the entire PCI config space so user space cannot
+		 * race with SMN index/data register access.
+		 */
+		if (!pci_request_config_region_exclusive(root, 0,
+							 PCI_CFG_SPACE_SIZE,
+							 NULL)) {
+			pci_err(root, "Failed to reserve config space\n");
+			ret = -EEXIST;
+			/*
+			 * hygon_get_next_root() returns a referenced device
+			 * (and drops the previous one on the next iteration).
+			 * Breaking out of the loop here skips that drop, so
+			 * release the current root explicitly.  It is not yet
+			 * in reserved_roots[], so err_release does not cover
+			 * it.
+			 */
+			pci_dev_put(root);
+			goto err_release;
+		}
+
+		reserved_roots[reserved++] = root;
+
+		if (count++ % roots_per_socket)
+			continue;
+
+		if (socket >= num_sockets) {
+			ret = -ENODEV;
+			/*
+			 * Same as above: drop the current root's reference
+			 * before leaving the iteration.  err_release frees its
+			 * reserved config region but not the PCI reference.
+			 */
+			pci_dev_put(root);
+			goto err_release;
+		}
+
+		pci_dbg(root, "is root for Hygon socket %u\n", socket);
+		socket_roots[socket++] = root;
+	}
+
+	if (socket != num_sockets) {
+		ret = -ENODEV;
+		goto err_release;
+	}
+
+	/* Expand socket roots to a per-node array. */
+	for (node = 0; node < num_nodes; node++) {
+		socket_id = hygon_node_socket(node);
+
+		if (socket_id >= num_sockets) {
+			ret = -ENODEV;
+			goto err_release;
+		}
+
+		pci_dbg(socket_roots[socket_id],
+			"is root for Hygon node %u (socket %u)\n",
+			node, socket_id);
+		roots[node] = socket_roots[socket_id];
+	}
+
+	kfree(reserved_roots);
+
+	hygon_smn_roots = roots;
+	hygon_smn_num_nodes = num_nodes;
+	hygon_smn_exclusive = true;
+	return 0;
+
+err_release:
+	hygon_release_root_regions(reserved_roots, reserved);
+	kfree(reserved_roots);
+	kfree(roots);
+	return ret;
+}
+
+/*
+ * SMN index/data register transaction.  The PCI config write+read
+ * pair is serialized by hygon_smn_mutex against userspace and other
+ * in-kernel SMN users.
+ *
+ * Same low-level index/data transaction model as amd_smn_read/write();
+ * the Hygon-specific node-to-root binding is established during SMN
+ * setup.
+ */
+static int __hygon_smn_rw(u16 node, u32 address, u32 *value, bool write)
+{
+	struct pci_dev *root;
+	int err;
+
+	if (!hygon_smn_exclusive || node >= hygon_smn_num_nodes)
+		return -ENODEV;
+
+	root = hygon_smn_roots[node];
+	if (!root)
+		return -ENODEV;
+
+	guard(mutex)(&hygon_smn_mutex);
+
+	err = pci_write_config_dword(root, HYGON_SMN_INDEX_OFFSET, address);
+	if (err) {
+		pr_warn("SMN index write failed (addr 0x%x)\n", address);
+		return pcibios_err_to_errno(err);
+	}
+
+	err = write ? pci_write_config_dword(root, HYGON_SMN_DATA_OFFSET, *value)
+		    : pci_read_config_dword(root, HYGON_SMN_DATA_OFFSET, value);
+
+	return pcibios_err_to_errno(err);
+}
+
+int hygon_smn_read(u16 node, u32 address, u32 *value)
+{
+	int err;
+
+	if (!value)
+		return -EINVAL;
+
+	err = __hygon_smn_rw(node, address, value, false);
+
+	/*
+	 * On any failure from __hygon_smn_rw(), @value may be unchanged or
+	 * uninitialised.  Zero it so callers do not consume a stale value,
+	 * and return the real error so the caller can act on it.
+	 */
+	if (err) {
+		*value = 0;
+		return err;
+	}
+
+	/*
+	 * pci_read_config_dword() succeeded but returned the
+	 * all-ones pattern that PCI uses for "device gone".  Translate to
+	 * -ENODEV and zero @value to avoid handing back the sentinel.
+	 */
+	if (PCI_POSSIBLE_ERROR(*value)) {
+		*value = 0;
+		return -ENODEV;
+	}
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(hygon_smn_read);
+
+int hygon_smn_write(u16 node, u32 address, u32 value)
+{
+	return __hygon_smn_rw(node, address, &value, true);
+}
+EXPORT_SYMBOL_GPL(hygon_smn_write);
+
 /*
  * Single Hygon fs_initcall: build the DF node cache, then run any
  * Hygon-specific subsequent setup.
@@ -844,6 +1148,10 @@ static int __init hygon_node_init(void)
 		return ret;
 	}
 
+	ret = hygon_smn_setup();
+	if (ret)
+		pr_warn("SMN setup failed: %d\n", ret);
+
 	return 0;
 }
 fs_initcall(hygon_node_init);
-- 
2.43.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH v3 6/6] x86/hygon: Add SMN debugfs interface
  2026-07-09  7:55 [PATCH v3 0/6] x86/hygon: Add Family 0x18 DF node enumeration and SMN access Lin Wang
                   ` (4 preceding siblings ...)
  2026-07-09  7:55 ` [PATCH v3 5/6] x86/hygon: Add Family 0x18 SMN access Lin Wang
@ 2026-07-09  7:55 ` Lin Wang
  5 siblings, 0 replies; 8+ messages in thread
From: Lin Wang @ 2026-07-09  7:55 UTC (permalink / raw)
  To: yazen.ghannam, mario.limonciello, Borislav Petkov
  Cc: tglx, mingo, x86, linux-kernel

Add an optional debugfs interface for issuing raw Hygon SMN reads and
writes.  The interface is intended as a kernel-developer debug aid and
is disabled by default.

Enable on the kernel command line:

  hygon_smn_debugfs_enable

When enabled and SMN setup succeeds, four files appear under
/sys/kernel/debug/x86/hygon_smn/:

  topology - read-only.  Prints a node count summary (num_nodes,
             num_cdd, num_sockets, cdd_per_socket) and the dense DF
             node id -> (socket, dfid, type, PCI BDF) mapping.  Unlike
             AMD, Hygon DF nodes are not at fixed PCI slots and the
             node id space is a dense range partitioned into CDD
             nodes first, then IOD nodes.  This file exposes the valid
             node range and the hardware identity of each dense node
             id.
  node     - DF node index that subsequent value reads/writes target;
             writes are validated against hygon_node_num().
  address  - SMN address to read or write.
  value    - read returns the 32-bit value at (node, address);
             write performs an SMN write and taints the kernel with
             TAINT_CPU_OUT_OF_SPEC, because arbitrary SMN writes can
             leave the CPU in an undefined state.

The interface is created from hygon_smn_setup() after
hygon_smn_exclusive is set, so the underlying hygon_smn_read /
hygon_smn_write APIs are available when a debugfs op is
issued.  For the same reason cache.ready should already be true when
"topology" is readable; the show handler still guards on it and
returns -ENODEV if it is not, which would be an internal ordering
error.

The node/address/value selector-style debugfs interface follows the
existing amd_smn debugfs model.  It is kept as an opt-in developer
debug aid instead of introducing a different Hygon-only debug ABI.

No new in-kernel API.  No effect on systems that do not opt in via the
boot parameter.

Signed-off-by: Lin Wang <wanglin@open-hieco.net>
---
 .../admin-guide/kernel-parameters.txt         |   8 +
 arch/x86/kernel/hygon_node.c                  | 183 ++++++++++++++++++
 2 files changed, 191 insertions(+)

diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt
index b5493a7f8f22..06d85930af01 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -2135,6 +2135,14 @@ Kernel parameters
 			Hardware protection action taken on critical events like
 			overtemperature or imminent voltage loss.
 
+	hygon_smn_debugfs_enable
+			[X86] Enable the Hygon Family 0x18 SMN debugfs
+			interface at /sys/kernel/debug/x86/hygon_smn/, which
+			allows raw reads and writes of System Management
+			Network (SMN) registers.  Requires CONFIG_HYGON_NODE
+			and CONFIG_DEBUG_FS.  Off by default; intended for
+			development and debugging only.
+
 	i2c_bus=	[HW]	Override the default board specific I2C bus speed
 				or register an additional I2C bus that is not
 				registered from board initialization code.
diff --git a/arch/x86/kernel/hygon_node.c b/arch/x86/kernel/hygon_node.c
index 09c08aa0c2c7..9f597e3f377a 100644
--- a/arch/x86/kernel/hygon_node.c
+++ b/arch/x86/kernel/hygon_node.c
@@ -27,6 +27,7 @@
 #include <linux/bitops.h>
 #include <linux/cleanup.h>
 #include <linux/cpu.h>
+#include <linux/debugfs.h>
 #include <linux/export.h>
 #include <linux/init.h>
 #include <linux/mutex.h>
@@ -870,6 +871,185 @@ static void __init hygon_release_root_regions(struct pci_dev **roots,
 		pci_release_config_region(roots[i], 0, PCI_CFG_SPACE_SIZE);
 }
 
+/*
+ * Optional Hygon SMN debugfs interface.
+ *
+ * Disabled by default; enable with the "hygon_smn_debugfs_enable" kernel
+ * command-line option.  When enabled, this exposes four files under
+ * /sys/kernel/debug/x86/hygon_smn/:
+ *
+ *   topology  (ro) -- node count summary and the dense DF node id ->
+ *                     (socket, dfid, type, PCI BDF) mapping, so a
+ *                     developer can discover the valid range for "node"
+ *                     and what each dense id refers to in hardware.
+ *   node      (rw) -- select the target dense DF node id.
+ *   address   (rw) -- select the target SMN address.
+ *   value     (rw) -- read/write the SMN value at (node, address).
+ *
+ * A write to "value" performs an SMN write and taints the kernel with
+ * TAINT_CPU_OUT_OF_SPEC, because arbitrary SMN writes can put the CPU
+ * into an undefined state.
+ */
+static bool hygon_smn_enable_dfs __initdata;
+
+static int __init hygon_smn_enable_debugfs(char *str)
+{
+	hygon_smn_enable_dfs = true;
+	return 1;
+}
+__setup("hygon_smn_debugfs_enable", hygon_smn_enable_debugfs);
+
+static struct dentry *hygon_smn_debugfs_dir;
+
+/*
+ * The node/address/value selector layout follows the existing amd_smn
+ * debugfs model: global selector state for an opt-in, single-user
+ * developer debug interface.
+ */
+static u16 hygon_smn_debug_node;
+static u32 hygon_smn_debug_address;
+
+/*
+ * "topology" -- read-only summary plus the dense DF node id ->
+ * (socket, dfid, type, PCI BDF) mapping.  This is the discovery file
+ * for the SMN interface: it tells the user the valid range for "node"
+ * (0 .. num_nodes - 1), which dense ids are CDD vs IOD, and which PCI
+ * misc (F3) / link (F4) functions back each node.
+ *
+ * The file only exists once hygon_smn_debugfs_init() has run, which is
+ * after hygon_build_cache() set cache.ready, so a !ready state here is
+ * an internal ordering error rather than a normal transient.  Report it
+ * as -ENODEV instead of printing misleading values.
+ */
+static int hygon_smn_topology_show(struct seq_file *m, void *v)
+{
+	u16 i;
+
+	if (!hygon_cache.ready)
+		return -ENODEV;
+
+	seq_printf(m, "num_nodes:       %u\n", hygon_cache.num_nodes);
+	seq_printf(m, "num_cdd:         %u\n", hygon_cache.num_cdd);
+	seq_printf(m, "num_sockets:     %u\n", hygon_cache.num_sockets);
+	seq_printf(m, "cdd_per_socket:  %u\n", hygon_cache.cdd_per_socket);
+	seq_putc(m, '\n');
+
+	seq_printf(m, "%-4s %-6s %-4s %-4s %-12s %-12s\n",
+		   "node", "socket", "dfid", "type", "misc", "link");
+
+	for (i = 0; i < hygon_cache.num_nodes; i++) {
+		const struct hygon_node *node = &hygon_cache.nodes[i];
+
+		seq_printf(m, "%-4u %-6u %-4u %-4s %-12s %-12s\n",
+			   i, node->socket_id, node->dfid,
+			   node->is_cdd ? "CDD" : "IOD",
+			   pci_name(node->misc), pci_name(node->link));
+	}
+
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(hygon_smn_topology);
+
+static ssize_t hygon_smn_node_write(struct file *file,
+				    const char __user *userbuf,
+				    size_t count, loff_t *ppos)
+{
+	u16 node;
+	int ret;
+
+	ret = kstrtou16_from_user(userbuf, count, 0, &node);
+	if (ret)
+		return ret;
+
+	if (node >= hygon_node_num())
+		return -ENODEV;
+
+	hygon_smn_debug_node = node;
+	return count;
+}
+
+static int hygon_smn_node_show(struct seq_file *m, void *v)
+{
+	seq_printf(m, "0x%08x\n", hygon_smn_debug_node);
+	return 0;
+}
+
+static ssize_t hygon_smn_address_write(struct file *file,
+				       const char __user *userbuf,
+				       size_t count, loff_t *ppos)
+{
+	int ret;
+
+	ret = kstrtouint_from_user(userbuf, count, 0, &hygon_smn_debug_address);
+	if (ret)
+		return ret;
+
+	return count;
+}
+
+static int hygon_smn_address_show(struct seq_file *m, void *v)
+{
+	seq_printf(m, "0x%08x\n", hygon_smn_debug_address);
+	return 0;
+}
+
+static int hygon_smn_value_show(struct seq_file *m, void *v)
+{
+	u32 val;
+	int ret;
+
+	ret = hygon_smn_read(hygon_smn_debug_node,
+			     hygon_smn_debug_address, &val);
+	if (ret)
+		return ret;
+
+	seq_printf(m, "0x%08x\n", val);
+	return 0;
+}
+
+static ssize_t hygon_smn_value_write(struct file *file,
+				     const char __user *userbuf,
+				     size_t count, loff_t *ppos)
+{
+	u32 val;
+	int ret;
+
+	ret = kstrtouint_from_user(userbuf, count, 0, &val);
+	if (ret)
+		return ret;
+
+	add_taint(TAINT_CPU_OUT_OF_SPEC, LOCKDEP_STILL_OK);
+
+	ret = hygon_smn_write(hygon_smn_debug_node,
+			      hygon_smn_debug_address, val);
+	if (ret)
+		return ret;
+
+	return count;
+}
+
+DEFINE_SHOW_STORE_ATTRIBUTE(hygon_smn_node);
+DEFINE_SHOW_STORE_ATTRIBUTE(hygon_smn_address);
+DEFINE_SHOW_STORE_ATTRIBUTE(hygon_smn_value);
+
+static void __init hygon_smn_debugfs_init(void)
+{
+	if (!hygon_smn_enable_dfs)
+		return;
+
+	hygon_smn_debugfs_dir = debugfs_create_dir("hygon_smn",
+						   arch_debugfs_dir);
+
+	debugfs_create_file("topology", 0400, hygon_smn_debugfs_dir, NULL,
+			    &hygon_smn_topology_fops);
+	debugfs_create_file("node", 0600, hygon_smn_debugfs_dir, NULL,
+			    &hygon_smn_node_fops);
+	debugfs_create_file("address", 0600, hygon_smn_debugfs_dir, NULL,
+			    &hygon_smn_address_fops);
+	debugfs_create_file("value", 0600, hygon_smn_debugfs_dir, NULL,
+			    &hygon_smn_value_fops);
+}
+
 /*
  * Hygon SMN setup.
  *
@@ -1032,6 +1212,9 @@ static int __init hygon_smn_setup(void)
 	hygon_smn_roots = roots;
 	hygon_smn_num_nodes = num_nodes;
 	hygon_smn_exclusive = true;
+
+	hygon_smn_debugfs_init();
+
 	return 0;
 
 err_release:
-- 
2.43.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH v3 2/6] x86/amd_nb: Restrict NB framework init to AMD vendor
  2026-07-09  7:55 ` [PATCH v3 2/6] x86/amd_nb: Restrict NB framework init to AMD vendor Lin Wang
@ 2026-07-14 13:10   ` Yazen Ghannam
  0 siblings, 0 replies; 8+ messages in thread
From: Yazen Ghannam @ 2026-07-14 13:10 UTC (permalink / raw)
  To: Lin Wang
  Cc: mario.limonciello, Borislav Petkov, tglx, mingo, x86, linux-kernel

On Thu, Jul 09, 2026 at 03:55:45PM +0800, Lin Wang wrote:
> Drop the X86_VENDOR_HYGON branch from init_amd_nbs() and
> early_is_amd_nb().  Both functions are entry points to the AMD NB
> framework, which uses the legacy fixed-slot PCI enumeration model
> (slot 0x18+i for node i) and the AMD-only amd_nb_misc_ids[] device-ID
> table.
> 
> On Hygon Family 0x18, these paths do not find usable AMD NB entries:
> amd_cache_northbridges() probes the AMD fixed slots, while
> early_is_amd_nb() matches only AMD IDs from amd_nb_misc_ids[].
> 
> Restrict these AMD framework entry points to AMD vendor.  Hygon DF
> enumeration is added separately in arch/x86/kernel/hygon_node.c.
> 
> No user-visible functional regression on Hygon Fam18h.
> 
> amd_get_mmconfig_range() is intentionally left unchanged: it reads
> the MSR_FAM10H_MMIO_CONF_BASE shared CPU MSR and serves
> arch/x86/pci/amd_bus.c and drivers/pnp/quirks.c on Hygon platforms.
> 
> Signed-off-by: Lin Wang <wanglin@open-hieco.net>

Reviewed-by: Yazen Ghannam <yazen.ghannam@amd.com>

Thanks,
Yazen

^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2026-07-14 13:10 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-07-09  7:55 [PATCH v3 0/6] x86/hygon: Add Family 0x18 DF node enumeration and SMN access Lin Wang
2026-07-09  7:55 ` [PATCH v3 1/6] pci_ids: Add Hygon Family 0x18 DF F3/F4 device IDs Lin Wang
2026-07-09  7:55 ` [PATCH v3 2/6] x86/amd_nb: Restrict NB framework init to AMD vendor Lin Wang
2026-07-14 13:10   ` Yazen Ghannam
2026-07-09  7:55 ` [PATCH v3 3/6] x86/amd_node: Restrict SMN root enumeration " Lin Wang
2026-07-09  7:55 ` [PATCH v3 4/6] x86/hygon: Add Family 0x18 DF node enumeration Lin Wang
2026-07-09  7:55 ` [PATCH v3 5/6] x86/hygon: Add Family 0x18 SMN access Lin Wang
2026-07-09  7:55 ` [PATCH v3 6/6] x86/hygon: Add SMN debugfs interface Lin Wang

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox