diff --git a/cxl/lib/libcxl.c b/cxl/lib/libcxl.c
index bab7343e8a4a..8bc0394543dc 100644
--- a/cxl/lib/libcxl.c
+++ b/cxl/lib/libcxl.c
@@ -46,11 +46,13 @@ struct cxl_ctx {
 	void *userdata;
 	int memdevs_init;
 	int buses_init;
+	int features_init;
 	unsigned long timeout;
 	struct udev *udev;
 	struct udev_queue *udev_queue;
 	struct list_head memdevs;
 	struct list_head buses;
+	struct list_head features;
 	struct kmod_ctx *kmod_ctx;
 	struct daxctl_ctx *daxctl_ctx;
 	void *private_data;
@@ -91,6 +93,23 @@ static void free_memdev(struct cxl_memdev *memdev, struct list_head *head)
 	free(memdev);
 }
 
+static void __free_features(struct cxl_features *feat)
+{
+	free(feat->dev_buf);
+	free(feat->dev_path);
+	free(feat->host_path);
+	free(feat->hostname);
+	free(feat);
+}
+
+static void free_features(struct cxl_features *feat, struct list_head *head)
+{
+	if (head)
+		list_del_from(head, &feat->list);
+	kmod_module_unref(feat->module);
+	__free_features(feat);
+}
+
 static void free_target(struct cxl_target *target, struct list_head *head)
 {
 	if (head)
@@ -289,6 +308,7 @@ CXL_EXPORT int cxl_new(struct cxl_ctx **ctx)
 	*ctx = c;
 	list_head_init(&c->memdevs);
 	list_head_init(&c->buses);
+	list_head_init(&c->features);
 	c->kmod_ctx = kmod_ctx;
 	c->daxctl_ctx = daxctl_ctx;
 	c->udev = udev;
@@ -329,6 +349,7 @@ CXL_EXPORT struct cxl_ctx *cxl_ref(struct cxl_ctx *ctx)
  */
 CXL_EXPORT void cxl_unref(struct cxl_ctx *ctx)
 {
+	struct cxl_features *feat, *_f;
 	struct cxl_memdev *memdev, *_d;
 	struct cxl_bus *bus, *_b;
 
@@ -344,6 +365,9 @@ CXL_EXPORT void cxl_unref(struct cxl_ctx *ctx)
 	list_for_each_safe(&ctx->buses, bus, _b, port.list)
 		free_bus(bus, &ctx->buses);
 
+	list_for_each_safe(&ctx->features, feat, _f, list)
+		free_features(feat, &ctx->features);
+
 	udev_queue_unref(ctx->udev_queue);
 	udev_unref(ctx->udev);
 	kmod_unref(ctx->kmod_ctx);
@@ -4694,3 +4718,159 @@ CXL_EXPORT struct cxl_cmd *cxl_cmd_new_set_alert_config(struct cxl_memdev *memde
 {
 	return cxl_cmd_new_generic(memdev, CXL_MEM_COMMAND_ID_SET_ALERT_CONFIG);
 }
+
+static const char fwctl_prefix[] = "fwctl";
+static int get_feature_chardev(const char *base, char *chardev_path)
+{
+	char *path = calloc(1, strlen(base) + 100);
+	struct dirent *entry;
+	DIR *dir;
+	int rc;
+
+	if (!path)
+		return -ENOMEM;
+
+	sprintf(path, "%s/fwctl/", base);
+	dir = opendir(path);
+	if (!dir) {
+		rc = -errno;
+		goto err;
+	}
+
+	while ((entry = readdir(dir)) != NULL)
+		if (strncmp(entry->d_name, fwctl_prefix, strlen(fwctl_prefix)) == 0)
+			break;
+
+	if (!entry) {
+		rc = -ENOENT;
+		goto read_err;
+	}
+
+	sprintf(chardev_path, "/dev/fwctl/%s", entry->d_name);
+	closedir(dir);
+
+	return 0;
+
+read_err:
+	closedir(dir);
+err:
+	free(path);
+	return rc;
+}
+
+static void *add_cxl_features(void *parent, int id, const char *cxlfeat_base)
+{
+	const char *devname = devpath_to_devname(cxlfeat_base);
+	char *path = calloc(1, strlen(cxlfeat_base) + 100);
+	struct cxl_features *feat, *feat_dupe;
+	char *host, *host_path, *hostname;
+	struct cxl_ctx *ctx = parent;
+	char buf[SYSFS_ATTR_SIZE];
+	struct stat st;
+	int rc;
+
+	if (!path)
+		return NULL;
+
+	dbg(ctx, "%s: base: \'%s\'\n", devname, cxlfeat_base);
+
+	feat = calloc(1, sizeof(*feat));
+	if (!feat)
+		goto err_dev;
+
+	feat->id = id;
+	feat->ctx = ctx;
+	sprintf(path, "/dev/cxl/%s", devname);
+
+	rc = get_feature_chardev(cxlfeat_base, path);
+	if (rc) {
+		/* No fwctl entry, no need to enumerate the "device" */
+		__free_features(feat);
+		free(path);
+		return NULL;
+	}
+
+	if (stat(path, &st) < 0)
+		goto err_read;
+
+	feat->major = major(st.st_rdev);
+	feat->minor = minor(st.st_rdev);
+
+	feat->dev_path = strdup(cxlfeat_base);
+	if (!feat->dev_path)
+		goto err_read;
+
+	feat->host_path = realpath(cxlfeat_base, NULL);
+	if (!feat->host_path)
+		goto err_read;
+	host = strrchr(feat->host_path, '/');
+	if (!host)
+		goto err_read;
+	host[0] = '\0';
+
+	feat->dev_buf = calloc(1, strlen(cxlfeat_base) + 50);
+	if (!feat->dev_buf)
+		goto err_read;
+	feat->buf_len = strlen(cxlfeat_base) + 50;
+
+	sprintf(path, "%s/features", cxlfeat_base);
+	if (sysfs_read_attr(ctx, path, buf) < 0)
+		goto err_read;
+	feat->features = atoi(buf);
+
+	/*
+	 * Extract host device name for the features "device". This is used for
+	 * matching up with an endpoint uport.
+	 */
+	host_path = strdup(feat->host_path);
+	if (!host_path)
+		goto err_read;
+
+	hostname = strdup(devpath_to_devname(host_path));
+	if (!hostname)
+		goto err_read;
+
+	free(host_path);
+	feat->hostname = hostname;
+
+	cxl_features_foreach(ctx, feat_dupe)
+		if (feat_dupe->id == feat->id) {
+			__free_features(feat);
+			free(path);
+			return feat_dupe;
+		}
+
+	list_add(&ctx->features, &feat->list);
+	free(path);
+	return feat;
+
+ err_read:
+	__free_features(feat);
+ err_dev:
+	free(path);
+	return NULL;
+}
+
+static void cxl_features_init(struct cxl_ctx *ctx)
+{
+	if (ctx->features_init)
+		return;
+
+	ctx->features_init = 1;
+
+	device_parse(ctx, "/sys/bus/cxl/devices", "features", ctx, add_cxl_features);
+}
+
+CXL_EXPORT struct cxl_features *cxl_features_get_first(struct cxl_ctx *ctx)
+{
+	cxl_features_init(ctx);
+
+	return list_top(&ctx->features, struct cxl_features, list);
+}
+
+CXL_EXPORT struct cxl_features *cxl_features_get_next(struct cxl_features *features)
+{
+	struct cxl_ctx *ctx = features->ctx;
+
+	return list_next(&ctx->features, features, list);
+}
diff --git a/cxl/lib/libcxl.sym b/cxl/lib/libcxl.sym
index 1fc33cc6e1a4..9b1708d8e86a 100644
--- a/cxl/lib/libcxl.sym
+++ b/cxl/lib/libcxl.sym
@@ -292,4 +292,6 @@ global:
 LIBCXL_9 {
 global:
 	cxl_bus_get_by_provider;
+	cxl_features_get_first;
+	cxl_features_get_next;
 } LIBECXL_8;
diff --git a/cxl/lib/private.h b/cxl/lib/private.h
index b6cd910e9335..34785af4e64f 100644
--- a/cxl/lib/private.h
+++ b/cxl/lib/private.h
@@ -34,6 +34,19 @@ enum cxl_fwl_loading {
 	CXL_FWL_LOADING_START,
 };
 
+struct cxl_features {
+	int id, major, minor;
+	struct cxl_ctx *ctx;
+	void *dev_buf;
+	size_t buf_len;
+	char *host_path;
+	char *dev_path;
+	char *hostname;
+	struct kmod_module *module;
+	struct list_node list;
+	int features;
+};
+
 struct cxl_endpoint;
 struct cxl_memdev {
 	int id, major, minor;
diff --git a/cxl/libcxl.h b/cxl/libcxl.h
index 3b309968a808..7e94eb8bce24 100644
--- a/cxl/libcxl.h
+++ b/cxl/libcxl.h
@@ -489,6 +489,15 @@ int cxl_cmd_alert_config_set_enable_alert_actions(struct cxl_cmd *cmd,
 						  int enable);
 struct cxl_cmd *cxl_cmd_new_set_alert_config(struct cxl_memdev *memdev);
 
+struct cxl_features;
+#define cxl_features_foreach(ctx, features) \
+        for (features = cxl_features_get_first(ctx); \
+             features != NULL; \
+             features = cxl_features_get_next(features))
+
+struct cxl_features *cxl_features_get_first(struct cxl_ctx *ctx);
+struct cxl_features *cxl_features_get_next(struct cxl_features *features);
+
 #ifdef __cplusplus
 } /* extern "C" */
 #endif
