diff --git a/.clang-format b/.clang-format
index 1154c76b1399..391cd3420947 100644
--- a/.clang-format
+++ b/.clang-format
@@ -79,6 +79,7 @@ ExperimentalAutoDetectBinPacking: false
 ForEachMacros:
   - 'cxl_memdev_foreach'
   - 'cxl_bus_foreach'
+  - 'cxl_port_foreach'
   - 'daxctl_dev_foreach'
   - 'daxctl_mapping_foreach'
   - 'daxctl_region_foreach'
diff --git a/Documentation/cxl/cxl-list.txt b/Documentation/cxl/cxl-list.txt
index be131ae7b02a..3076deb43b9e 100644
--- a/Documentation/cxl/cxl-list.txt
+++ b/Documentation/cxl/cxl-list.txt
@@ -176,6 +176,30 @@ OPTIONS
 	names to filter the listing. The supported provider names are "ACPI.CXL"
 	and "cxl_test".
 
+-P::
+--ports::
+	Include port objects (CXL / PCIe root ports + Upstream Switch Ports) in
+	the listing.
+
+-p::
+--port=::
+	Specify CXL Port device name(s), device id(s), and or port type
+	names to filter the listing. The supported port type names are "root"
+	and "switch". Note that since a bus object is also a port, the following
+	two syntaxes are equivalent:
+----
+# cxl list -B
+# cxl list -P -p root
+----
+	By default, only 'switch' ports are listed.
+
+-S::
+--single::
+	Specify whether the listing should emit all the objects that are
+	descendants of a port that matches the port filter, or only direct
+	descendants of the individual ports that match the filter. By default
+	all descendant objects are listed.
+
 include::human-option.txt[]
 
 include::verbose-option.txt[]
diff --git a/Documentation/cxl/lib/libcxl.txt b/Documentation/cxl/lib/libcxl.txt
index 84af66a3a7bd..804e9ca1500e 100644
--- a/Documentation/cxl/lib/libcxl.txt
+++ b/Documentation/cxl/lib/libcxl.txt
@@ -164,6 +164,48 @@ discovery order. The possible provider names are 'ACPI.CXL' and
 the kernel device names that are subject to change based on discovery
 order.
 
+PORTS
+-----
+CXL ports track the PCIe hierarchy between a platform firmware CXL root
+object, through CXL / PCIe Host Bridges, CXL / PCIe Root Ports, and CXL
+/ PCIe Switch Ports.
+
+=== PORT: Enumeration
+----
+struct cxl_port *cxl_bus_get_port(struct cxl_bus *bus);
+struct cxl_port *cxl_port_get_first(struct cxl_port *parent);
+struct cxl_port *cxl_port_get_next(struct cxl_port *port);
+struct cxl_port *cxl_port_get_parent(struct cxl_port *port);
+struct cxl_bus *cxl_port_get_bus(struct cxl_port *port);
+struct cxl_ctx *cxl_port_get_ctx(struct cxl_port *port);
+
+#define cxl_port_foreach(parent, port)                                      \
+       for (port = cxl_port_get_first(parent); port != NULL;                \
+            port = cxl_port_get_next(port))
+----
+A bus object encapsulates a CXL port object. Use cxl_bus_get_port() to
+use generic port APIs on root objects.
+
+Ports are hierarchical. All but the a root object have another CXL port
+as a parent object retrievable via cxl_port_get_parent().
+
+The root port of a hiearchy can be retrieved via any port instance in
+that hierarchy via cxl_port_get_bus().
+
+=== PORT: Attributes
+----
+const char *cxl_port_get_devname(struct cxl_port *port);
+int cxl_port_get_id(struct cxl_port *port);
+int cxl_port_is_enabled(struct cxl_port *port);
+bool cxl_port_is_root(struct cxl_port *port);
+bool cxl_port_is_switch(struct cxl_port *port);
+----
+The port type is communicated via cxl_port_is_<type>(). An 'enabled' port
+is one that has succeeded in discovering the CXL component registers in
+the host device and has enumerated its downstream ports. In order for a
+memdev to be enabled for CXL memory operation all CXL ports in its
+ancestry must also be enabled.
+
 include::../../copyright.txt[]
 
 SEE ALSO
diff --git a/cxl/filter.c b/cxl/filter.c
index 5f4844b4cab9..8b79db3dae8d 100644
--- a/cxl/filter.c
+++ b/cxl/filter.c
@@ -21,6 +21,101 @@ static const char *which_sep(const char *filter)
 	return " ";
 }
 
+bool cxl_filter_has(const char *__filter, const char *needle)
+{
+	char *filter, *save;
+	const char *arg;
+
+	if (!needle)
+		return true;
+
+	if (!__filter)
+		return false;
+
+	filter = strdup(__filter);
+	if (!filter)
+		return false;
+
+	for (arg = strtok_r(filter, which_sep(__filter), &save); arg;
+	     arg = strtok_r(NULL, which_sep(__filter), &save))
+		if (strstr(arg, needle))
+			break;
+
+	free(filter);
+	if (arg)
+		return true;
+	return false;
+}
+
+static struct cxl_port *__util_cxl_port_filter(struct cxl_port *port,
+					     const char *__ident)
+{
+	char *ident, *save;
+	const char *arg;
+	int port_id;
+
+	if (!__ident)
+		return port;
+
+	ident = strdup(__ident);
+	if (!ident)
+		return NULL;
+
+	for (arg = strtok_r(ident, which_sep(__ident), &save); arg;
+	     arg = strtok_r(NULL, which_sep(__ident), &save)) {
+		if (strcmp(arg, "all") == 0)
+			break;
+
+		if (strcmp(arg, "root") == 0 && cxl_port_is_root(port))
+			break;
+
+		if (strcmp(arg, "switch") == 0 && cxl_port_is_switch(port))
+			break;
+
+		if ((sscanf(arg, "%d", &port_id) == 1 ||
+		     sscanf(arg, "port%d", &port_id) == 1) &&
+		    cxl_port_get_id(port) == port_id)
+			break;
+
+		if (strcmp(arg, cxl_port_get_devname(port)) == 0)
+			break;
+	}
+
+	free(ident);
+	if (arg)
+		return port;
+	return NULL;
+}
+
+enum cxl_port_filter_mode {
+	CXL_PF_SINGLE,
+	CXL_PF_ANCESTRY,
+};
+
+static enum cxl_port_filter_mode pf_mode(struct cxl_filter_params *p)
+{
+	if (p->single)
+		return CXL_PF_SINGLE;
+	return CXL_PF_ANCESTRY;
+}
+
+static struct cxl_port *util_cxl_port_filter(struct cxl_port *port,
+					     const char *ident,
+					     enum cxl_port_filter_mode mode)
+{
+	struct cxl_port *iter = port;
+
+	while (iter) {
+		if (__util_cxl_port_filter(iter, ident))
+			return port;
+		if (mode == CXL_PF_SINGLE)
+			return NULL;
+		iter = cxl_port_get_parent(iter);
+	}
+
+	return NULL;
+}
+
 static struct cxl_bus *util_cxl_bus_filter(struct cxl_bus *bus,
 					   const char *__ident)
 {
@@ -58,6 +153,31 @@ static struct cxl_bus *util_cxl_bus_filter(struct cxl_bus *bus,
 	return NULL;
 }
 
+static struct cxl_port *util_cxl_port_filter_by_bus(struct cxl_port *port,
+						    const char *__ident)
+{
+	struct cxl_ctx *ctx = cxl_port_get_ctx(port);
+	struct cxl_bus *bus;
+
+	if (!__ident)
+		return port;
+
+	if (cxl_port_is_root(port)) {
+		bus = cxl_port_to_bus(port);
+		bus = util_cxl_bus_filter(bus, __ident);
+		return bus ? port : NULL;
+	}
+
+	cxl_bus_foreach(ctx, bus) {
+		if (!util_cxl_bus_filter(bus, __ident))
+			continue;
+		if (bus == cxl_port_get_bus(port))
+			return port;
+	}
+
+	return NULL;
+}
+
 static struct cxl_memdev *
 util_cxl_memdev_serial_filter(struct cxl_memdev *memdev, const char *__serials)
 {
@@ -169,10 +289,82 @@ static void splice_array(struct cxl_filter_params *p, struct json_object *jobjs,
 	json_object_put(jobjs);
 }
 
+static bool cond_add_put_array(struct json_object *jobj, const char *key,
+			       struct json_object *array)
+{
+	if (jobj && array && json_object_array_length(array) > 0) {
+		json_object_object_add(jobj, key, array);
+		return true;
+	} else {
+		json_object_put(array);
+		return false;
+	}
+}
+
+static bool cond_add_put_array_suffix(struct json_object *jobj, const char *key,
+				      const char *suffix,
+				      struct json_object *array)
+{
+	char *name;
+	bool rc;
+
+	if (asprintf(&name, "%s:%s", key, suffix) < 0)
+		return false;
+	rc = cond_add_put_array(jobj, name, array);
+	free(name);
+	return rc;
+}
+
+static struct json_object *pick_array(struct json_object *child,
+				      struct json_object *container)
+{
+	if (child)
+		return child;
+	if (container)
+		return container;
+	return NULL;
+}
+
+static void walk_child_ports(struct cxl_port *parent_port,
+			     struct cxl_filter_params *p,
+			     struct json_object *jports,
+			     unsigned long flags)
+{
+	struct cxl_port *port;
+
+	cxl_port_foreach(parent_port, port) {
+		const char *devname = cxl_port_get_devname(port);
+		struct json_object *jport = NULL;
+		struct json_object *jchildports = NULL;
+
+		if (!util_cxl_port_filter(port, p->port_filter, pf_mode(p)))
+			goto walk_children;
+		if (!util_cxl_port_filter_by_bus(port, p->bus_filter))
+			goto walk_children;
+		if (!p->idle && !cxl_port_is_enabled(port))
+			continue;
+		if (p->ports)
+			jport = util_cxl_port_to_json(port, flags);
+		if (!jport)
+			continue;
+		json_object_array_add(jports, jport);
+		jchildports = json_object_new_array();
+		if (!jchildports) {
+			err(p, "%s: failed to enumerate child ports\n",
+			    devname);
+			continue;
+		}
+walk_children:
+		walk_child_ports(port, p, pick_array(jchildports, jports),
+				 flags);
+		cond_add_put_array_suffix(jport, "ports", devname, jchildports);
+	}
+}
+
 int cxl_filter_walk(struct cxl_ctx *ctx, struct cxl_filter_params *p)
 {
+	struct json_object *jdevs = NULL, *jbuses = NULL, *jports = NULL;
 	struct json_object *jplatform = json_object_new_array();
-	struct json_object *jdevs = NULL, *jbuses = NULL;
 	unsigned long flags = params_to_flags(p);
 	struct cxl_memdev *memdev;
 	int top_level_objs = 0;
@@ -191,6 +383,10 @@ int cxl_filter_walk(struct cxl_ctx *ctx, struct cxl_filter_params *p)
 	if (!jbuses)
 		goto err;
 
+	jports = json_object_new_array();
+	if (!jports)
+		goto err;
+
 	cxl_memdev_foreach(ctx, memdev) {
 		struct json_object *jdev;
 
@@ -208,10 +404,15 @@ int cxl_filter_walk(struct cxl_ctx *ctx, struct cxl_filter_params *p)
 	}
 
 	cxl_bus_foreach(ctx, bus) {
-		struct json_object *jbus;
+		struct json_object *jbus = NULL;
+		struct json_object *jchildports = NULL;
+		struct cxl_port *port = cxl_bus_get_port(bus);
+		const char *devname = cxl_bus_get_devname(bus);
 
 		if (!util_cxl_bus_filter(bus, p->bus_filter))
-			continue;
+			goto walk_children;
+		if (!util_cxl_port_filter(port, p->port_filter, pf_mode(p)))
+			goto walk_children;
 		if (p->buses) {
 			jbus = util_cxl_bus_to_json(bus, flags);
 			if (!jbus) {
@@ -219,16 +420,32 @@ int cxl_filter_walk(struct cxl_ctx *ctx, struct cxl_filter_params *p)
 				continue;
 			}
 			json_object_array_add(jbuses, jbus);
+			if (p->ports) {
+				jchildports = json_object_new_array();
+				if (!jchildports) {
+					err(p,
+					    "%s: failed to enumerate child ports\n",
+					    devname);
+					continue;
+				}
+			}
 		}
+walk_children:
+		walk_child_ports(port, p, pick_array(jchildports, jports),
+				 flags);
+		cond_add_put_array_suffix(jbus, "ports", devname, jchildports);
 	}
 
 	if (json_object_array_length(jdevs))
 		top_level_objs++;
 	if (json_object_array_length(jbuses))
 		top_level_objs++;
+	if (json_object_array_length(jports))
+		top_level_objs++;
 
 	splice_array(p, jdevs, jplatform, "anon memdevs", top_level_objs > 1);
 	splice_array(p, jbuses, jplatform, "buses", top_level_objs > 1);
+	splice_array(p, jports, jplatform, "ports", top_level_objs > 1);
 
 	util_display_json_array(stdout, jplatform, flags);
 
@@ -236,6 +453,7 @@ int cxl_filter_walk(struct cxl_ctx *ctx, struct cxl_filter_params *p)
 err:
 	json_object_put(jdevs);
 	json_object_put(jbuses);
+	json_object_put(jports);
 	json_object_put(jplatform);
 	return -ENOMEM;
 }
diff --git a/cxl/filter.h b/cxl/filter.h
index d41e757cf51d..0d83304bd612 100644
--- a/cxl/filter.h
+++ b/cxl/filter.h
@@ -10,7 +10,10 @@ struct cxl_filter_params {
 	const char *memdev_filter;
 	const char *serial_filter;
 	const char *bus_filter;
+	const char *port_filter;
+	bool single;
 	bool memdevs;
+	bool ports;
 	bool buses;
 	bool idle;
 	bool human;
@@ -22,4 +25,5 @@ struct cxl_memdev *util_cxl_memdev_filter(struct cxl_memdev *memdev,
 					  const char *__ident,
 					  const char *serials);
 int cxl_filter_walk(struct cxl_ctx *ctx, struct cxl_filter_params *param);
+bool cxl_filter_has(const char *needle, const char *__filter);
 #endif /* _CXL_UTIL_FILTER_H_ */
diff --git a/cxl/json.c b/cxl/json.c
index a58459482360..d9f864edb86b 100644
--- a/cxl/json.c
+++ b/cxl/json.c
@@ -242,3 +242,26 @@ struct json_object *util_cxl_bus_to_json(struct cxl_bus *bus,
 
 	return jbus;
 }
+
+struct json_object *util_cxl_port_to_json(struct cxl_port *port,
+					  unsigned long flags)
+{
+	const char *devname = cxl_port_get_devname(port);
+	struct json_object *jport, *jobj;
+
+	jport = json_object_new_object();
+	if (!jport)
+		return NULL;
+
+	jobj = json_object_new_string(devname);
+	if (jobj)
+		json_object_object_add(jport, "port", jobj);
+
+	if (!cxl_port_is_enabled(port)) {
+		jobj = json_object_new_string("disabled");
+		if (jobj)
+			json_object_object_add(jport, "state", jobj);
+	}
+
+	return jport;
+}
diff --git a/cxl/json.h b/cxl/json.h
index 4abf6e500f0b..36653db6ef6a 100644
--- a/cxl/json.h
+++ b/cxl/json.h
@@ -8,4 +8,7 @@ struct json_object *util_cxl_memdev_to_json(struct cxl_memdev *memdev,
 struct cxl_bus;
 struct json_object *util_cxl_bus_to_json(struct cxl_bus *bus,
 					 unsigned long flags);
+struct cxl_port;
+struct json_object *util_cxl_port_to_json(struct cxl_port *port,
+					  unsigned long flags);
 #endif /* __CXL_UTIL_JSON_H__ */
diff --git a/cxl/lib/libcxl.c b/cxl/lib/libcxl.c
index 8548a458b131..03eff3cd6073 100644
--- a/cxl/lib/libcxl.c
+++ b/cxl/lib/libcxl.c
@@ -66,15 +66,27 @@ static void free_memdev(struct cxl_memdev *memdev, struct list_head *head)
 	free(memdev);
 }
 
+static void free_port(struct cxl_port *port, struct list_head *head);
 static void __free_port(struct cxl_port *port, struct list_head *head)
 {
+	struct cxl_port *child, *_c;
+
 	if (head)
 		list_del_from(head, &port->list);
+	list_for_each_safe(&port->child_ports, child, _c, list)
+		free_port(child, &port->child_ports);
+	kmod_module_unref(port->module);
 	free(port->dev_buf);
 	free(port->dev_path);
 	free(port->uport);
 }
 
+static void free_port(struct cxl_port *port, struct list_head *head)
+{
+	__free_port(port, head);
+	free(port);
+}
+
 static void free_bus(struct cxl_bus *bus, struct list_head *head)
 {
 	__free_port(&bus->port, head);
@@ -471,10 +483,12 @@ CXL_EXPORT int cxl_memdev_nvdimm_bridge_active(struct cxl_memdev *memdev)
 	return is_enabled(path);
 }
 
-static int cxl_port_init(struct cxl_port *port, struct cxl_ctx *ctx, int id,
+static int cxl_port_init(struct cxl_port *port, struct cxl_port *parent_port,
+			 enum cxl_port_type type, struct cxl_ctx *ctx, int id,
 			 const char *cxlport_base)
 {
 	char *path = calloc(1, strlen(cxlport_base) + 100);
+	char buf[SYSFS_ATTR_SIZE];
 	size_t rc;
 
 	if (!path)
@@ -482,6 +496,10 @@ static int cxl_port_init(struct cxl_port *port, struct cxl_ctx *ctx, int id,
 
 	port->id = id;
 	port->ctx = ctx;
+	port->type = type;
+	port->parent = parent_port;
+
+	list_head_init(&port->child_ports);
 
 	port->dev_path = strdup(cxlport_base);
 	if (!port->dev_path)
@@ -499,6 +517,10 @@ static int cxl_port_init(struct cxl_port *port, struct cxl_ctx *ctx, int id,
 	if (!port->uport)
 		goto err;
 
+	sprintf(path, "%s/modalias", cxlport_base);
+	if (sysfs_read_attr(ctx, path, buf) == 0)
+		port->module = util_modalias_to_module(ctx, buf);
+
 	return 0;
 err:
 	free(port->dev_path);
@@ -507,6 +529,135 @@ err:
 	return -ENOMEM;
 }
 
+static void *add_cxl_port(void *parent, int id, const char *cxlport_base)
+{
+	const char *devname = devpath_to_devname(cxlport_base);
+	struct cxl_port *port, *port_dup;
+	struct cxl_port *parent_port = parent;
+	struct cxl_ctx *ctx = cxl_port_get_ctx(parent_port);
+	int rc;
+
+	dbg(ctx, "%s: base: \'%s\'\n", devname, cxlport_base);
+
+	port = calloc(1, sizeof(*port));
+	if (!port)
+		return NULL;
+
+	rc = cxl_port_init(port, parent_port, CXL_PORT_SWITCH, ctx, id,
+			   cxlport_base);
+	if (rc)
+		goto err;
+
+	cxl_port_foreach(parent_port, port_dup)
+		if (port_dup->id == port->id) {
+			free_port(port, NULL);
+			return port_dup;
+		}
+
+	list_add(&parent_port->child_ports, &port->list);
+	return port;
+
+err:
+	free(port);
+	return NULL;
+
+}
+
+static void cxl_ports_init(struct cxl_port *port)
+{
+	struct cxl_ctx *ctx = cxl_port_get_ctx(port);
+
+	if (port->ports_init)
+		return;
+
+	port->ports_init = 1;
+
+	sysfs_device_parse(ctx, port->dev_path, "port", port, add_cxl_port);
+}
+
+CXL_EXPORT struct cxl_ctx *cxl_port_get_ctx(struct cxl_port *port)
+{
+	return port->ctx;
+}
+
+CXL_EXPORT struct cxl_port *cxl_port_get_first(struct cxl_port *port)
+{
+	cxl_ports_init(port);
+
+	return list_top(&port->child_ports, struct cxl_port, list);
+}
+
+CXL_EXPORT struct cxl_port *cxl_port_get_next(struct cxl_port *port)
+{
+	struct cxl_port *parent_port = port->parent;
+
+	return list_next(&parent_port->child_ports, port, list);
+}
+
+CXL_EXPORT const char *cxl_port_get_devname(struct cxl_port *port)
+{
+	return devpath_to_devname(port->dev_path);
+}
+
+CXL_EXPORT int cxl_port_get_id(struct cxl_port *port)
+{
+	return port->id;
+}
+
+CXL_EXPORT struct cxl_port *cxl_port_get_parent(struct cxl_port *port)
+{
+	return port->parent;
+}
+
+CXL_EXPORT bool cxl_port_is_root(struct cxl_port *port)
+{
+	return port->type == CXL_PORT_ROOT;
+}
+
+CXL_EXPORT bool cxl_port_is_switch(struct cxl_port *port)
+{
+	return port->type == CXL_PORT_SWITCH;
+}
+
+CXL_EXPORT struct cxl_bus *cxl_port_get_bus(struct cxl_port *port)
+{
+	struct cxl_bus *bus;
+
+	if (!cxl_port_is_enabled(port))
+		return NULL;
+
+	if (port->bus)
+		return port->bus;
+
+	while (port->parent)
+		port = port->parent;
+
+	bus = container_of(port, typeof(*bus), port);
+	port->bus = bus;
+	return bus;
+}
+
+CXL_EXPORT int cxl_port_is_enabled(struct cxl_port *port)
+{
+	struct cxl_ctx *ctx = cxl_port_get_ctx(port);
+	char *path = port->dev_buf;
+	int len = port->buf_len;
+
+	if (snprintf(path, len, "%s/driver", port->dev_path) >= len) {
+		err(ctx, "%s: buffer too small!\n", cxl_port_get_devname(port));
+		return 0;
+	}
+
+	return is_enabled(path);
+}
+
+CXL_EXPORT struct cxl_bus *cxl_port_to_bus(struct cxl_port *port)
+{
+	if (!cxl_port_is_root(port))
+		return NULL;
+	return container_of(port, struct cxl_bus, port);
+}
+
 static void *add_cxl_bus(void *parent, int id, const char *cxlbus_base)
 {
 	const char *devname = devpath_to_devname(cxlbus_base);
@@ -522,7 +673,7 @@ static void *add_cxl_bus(void *parent, int id, const char *cxlbus_base)
 		return NULL;
 
 	port = &bus->port;
-	rc = cxl_port_init(port, ctx, id, cxlbus_base);
+	rc = cxl_port_init(port, NULL, CXL_PORT_ROOT, ctx, id, cxlbus_base);
 	if (rc)
 		goto err;
 
@@ -579,6 +730,11 @@ CXL_EXPORT int cxl_bus_get_id(struct cxl_bus *bus)
 	return port->id;
 }
 
+CXL_EXPORT struct cxl_port *cxl_bus_get_port(struct cxl_bus *bus)
+{
+	return &bus->port;
+}
+
 CXL_EXPORT const char *cxl_bus_get_provider(struct cxl_bus *bus)
 {
 	struct cxl_port *port = &bus->port;
diff --git a/cxl/lib/libcxl.sym b/cxl/lib/libcxl.sym
index 781ff996af73..a7e923f7d721 100644
--- a/cxl/lib/libcxl.sym
+++ b/cxl/lib/libcxl.sym
@@ -82,4 +82,16 @@ global:
 	cxl_bus_get_provider;
 	cxl_bus_get_devname;
 	cxl_bus_get_id;
+	cxl_bus_get_port;
+	cxl_port_get_first;
+	cxl_port_get_next;
+	cxl_port_get_devname;
+	cxl_port_get_id;
+	cxl_port_get_ctx;
+	cxl_port_is_enabled;
+	cxl_port_get_parent;
+	cxl_port_is_root;
+	cxl_port_is_switch;
+	cxl_port_to_bus;
+	cxl_port_get_bus;
 } LIBCXL_1;
diff --git a/cxl/lib/private.h b/cxl/lib/private.h
index 0758d0578acf..637f90d8dfa2 100644
--- a/cxl/lib/private.h
+++ b/cxl/lib/private.h
@@ -34,14 +34,25 @@ struct cxl_memdev {
 	unsigned long long serial;
 };
 
+enum cxl_port_type {
+	CXL_PORT_ROOT,
+	CXL_PORT_SWITCH,
+};
+
 struct cxl_port {
 	int id;
 	void *dev_buf;
 	size_t buf_len;
 	char *dev_path;
 	char *uport;
+	int ports_init;
 	struct cxl_ctx *ctx;
+	struct cxl_bus *bus;
+	enum cxl_port_type type;
+	struct cxl_port *parent;
+	struct kmod_module *module;
 	struct list_node list;
+	struct list_head child_ports;
 };
 
 struct cxl_bus {
diff --git a/cxl/libcxl.h b/cxl/libcxl.h
index da66eb298ab8..efbb397eb2be 100644
--- a/cxl/libcxl.h
+++ b/cxl/libcxl.h
@@ -5,6 +5,7 @@
 
 #include <stdarg.h>
 #include <unistd.h>
+#include <stdbool.h>
 
 #ifdef HAVE_UUID
 #include <uuid/uuid.h>
@@ -63,11 +64,29 @@ struct cxl_bus *cxl_bus_get_next(struct cxl_bus *bus);
 const char *cxl_bus_get_provider(struct cxl_bus *bus);
 const char *cxl_bus_get_devname(struct cxl_bus *bus);
 int cxl_bus_get_id(struct cxl_bus *bus);
+struct cxl_port *cxl_bus_get_port(struct cxl_bus *bus);
 
 #define cxl_bus_foreach(ctx, bus)                                              \
 	for (bus = cxl_bus_get_first(ctx); bus != NULL;                        \
 	     bus = cxl_bus_get_next(bus))
 
+struct cxl_port;
+struct cxl_port *cxl_port_get_first(struct cxl_port *parent);
+struct cxl_port *cxl_port_get_next(struct cxl_port *port);
+const char *cxl_port_get_devname(struct cxl_port *port);
+int cxl_port_get_id(struct cxl_port *port);
+struct cxl_ctx *cxl_port_get_ctx(struct cxl_port *port);
+int cxl_port_is_enabled(struct cxl_port *port);
+struct cxl_port *cxl_port_get_parent(struct cxl_port *port);
+bool cxl_port_is_root(struct cxl_port *port);
+bool cxl_port_is_switch(struct cxl_port *port);
+struct cxl_bus *cxl_port_to_bus(struct cxl_port *port);
+struct cxl_bus *cxl_port_get_bus(struct cxl_port *port);
+
+#define cxl_port_foreach(parent, port)                                         \
+	for (port = cxl_port_get_first(parent); port != NULL;                  \
+	     port = cxl_port_get_next(port))
+
 struct cxl_cmd;
 const char *cxl_cmd_get_devname(struct cxl_cmd *cmd);
 struct cxl_cmd *cxl_cmd_new_raw(struct cxl_memdev *memdev, int opcode);
diff --git a/cxl/list.c b/cxl/list.c
index 9500e610e0ca..1ef91b44dca1 100644
--- a/cxl/list.c
+++ b/cxl/list.c
@@ -25,6 +25,11 @@ static const struct option options[] = {
 	OPT_STRING('b', "bus", &param.bus_filter, "bus device name",
 		   "filter by CXL bus device name(s)"),
 	OPT_BOOLEAN('B', "buses", &param.buses, "include CXL bus info"),
+	OPT_STRING('p', "port", &param.port_filter, "port device name",
+		   "filter by CXL port device name(s)"),
+	OPT_BOOLEAN('P', "ports", &param.ports, "include CXL port info"),
+	OPT_BOOLEAN('S', "single", &param.single,
+		    "skip listing descendant objects"),
 	OPT_BOOLEAN('i', "idle", &param.idle, "include disabled devices"),
 	OPT_BOOLEAN('u', "human", &param.human,
 		    "use human friendly number formats "),
@@ -35,7 +40,7 @@ static const struct option options[] = {
 
 static int num_list_flags(void)
 {
-       return !!param.memdevs + !!param.buses;
+       return !!param.memdevs + !!param.buses + !!param.ports;
 }
 
 int cmd_list(int argc, const char **argv, struct cxl_ctx *ctx)
@@ -53,11 +58,18 @@ int cmd_list(int argc, const char **argv, struct cxl_ctx *ctx)
 	if (argc)
 		usage_with_options(u, options);
 
+	if (param.single && !param.port_filter) {
+		error("-S/--single expects a port filter: -p/--port=\n");
+		usage_with_options(u, options);
+	}
+
 	if (num_list_flags() == 0) {
 		if (param.memdev_filter || param.serial_filter)
 			param.memdevs = true;
 		if (param.bus_filter)
 			param.buses = true;
+		if (param.port_filter)
+			param.ports = true;
 		if (num_list_flags() == 0) {
 			/*
 			 * TODO: We likely want to list regions by default if
@@ -73,5 +85,8 @@ int cmd_list(int argc, const char **argv, struct cxl_ctx *ctx)
 
 	log_init(&param.ctx, "cxl list", "CXL_LIST_LOG");
 
+	if (cxl_filter_has(param.port_filter, "root") && param.ports)
+		param.buses = true;
+
 	return cxl_filter_walk(ctx, &param);
 }
