diff --git a/Documentation/ndctl/ndctl-list.txt b/Documentation/ndctl/ndctl-list.txt
index 7c7e3ac9d05c..b8d517d784b4 100644
--- a/Documentation/ndctl/ndctl-list.txt
+++ b/Documentation/ndctl/ndctl-list.txt
@@ -132,16 +132,35 @@ include::xable-bus-options.txt[]
 
 -F::
 --firmware::
-	Include dimm firmware info in the listing. For example:
-[verse]
-{
-  "dev":"nmem0",
-  "firmware":{
-      "current_version":0,
-      "next_version":1,
-      "need_powercycle":true
-  }
-}
+	Include firmware info in the listing, including the state and
+	capability of runtime firmware activation:
+
+----
+# ndctl list -BDF
+[
+  {
+    "provider":"nfit_test.0",
+    "dev":"ndbus2",
+    "scrub_state":"idle",
+    "firmware":{
+      "activate_method":"suspend",
+      "activate_state":"idle"
+    },
+    "dimms":[
+      {
+        "dev":"nmem1",
+        "id":"cdab-0a-07e0-ffffffff",
+        "handle":0,
+        "phys_id":0,
+        "security":"disabled",
+        "firmware":{
+          "current_version":0,
+          "can_update":true
+        }
+      },
+...
+]
+----
 
 -X::
 --device-dax::
diff --git a/ndctl/bus.c b/ndctl/bus.c
index 86bbd5178df9..6d5bafb86fe4 100644
--- a/ndctl/bus.c
+++ b/ndctl/bus.c
@@ -92,7 +92,7 @@ static int bus_action(int argc, const char **argv, const char *usage,
 			rc = scrub_action(bus, action);
 			if (rc == 0) {
 				success++;
-				jbus = util_bus_to_json(bus);
+				jbus = util_bus_to_json(bus, 0);
 				if (jbus)
 					json_object_array_add(jbuses, jbus);
 			} else if (!fail)
diff --git a/ndctl/lib/libndctl.c b/ndctl/lib/libndctl.c
index ee737cbbfe3e..f03635e99a83 100644
--- a/ndctl/lib/libndctl.c
+++ b/ndctl/lib/libndctl.c
@@ -819,6 +819,19 @@ static void parse_dimm_flags(struct ndctl_dimm *dimm, char *flags)
 				ndctl_dimm_get_devname(dimm), flags);
 }
 
+static enum ndctl_fwa_state fwa_to_state(const char *fwa)
+{
+	if (strcmp(fwa, "idle") == 0)
+		return NDCTL_FWA_IDLE;
+	if (strcmp(fwa, "busy") == 0)
+		return NDCTL_FWA_BUSY;
+	if (strcmp(fwa, "armed") == 0)
+		return NDCTL_FWA_ARMED;
+	if (strcmp(fwa, "overflow") == 0)
+		return NDCTL_FWA_ARM_OVERFLOW;
+	return NDCTL_FWA_INVALID;
+}
+
 static void *add_bus(void *parent, int id, const char *ctl_base)
 {
 	char buf[SYSFS_ATTR_SIZE];
@@ -880,6 +893,12 @@ static void *add_bus(void *parent, int id, const char *ctl_base)
 	if (!bus->scrub_path)
 		goto err_read;
 
+	sprintf(path, "%s/device/firmware_activate", ctl_base);
+	if (sysfs_read_attr(ctx, path, buf) < 0)
+		bus->fwa_state = NDCTL_FWA_INVALID;
+	else
+		bus->fwa_state = fwa_to_state(buf);
+
 	bus->bus_path = parent_dev_path("char", bus->major, bus->minor);
 	if (!bus->bus_path)
 		goto err_dev_path;
@@ -1436,6 +1455,74 @@ NDCTL_EXPORT int ndctl_bus_wait_for_scrub_completion(struct ndctl_bus *bus)
 	return ndctl_bus_poll_scrub_completion(bus, 0, 0);
 }
 
+NDCTL_EXPORT enum ndctl_fwa_state ndctl_bus_get_fw_activate_state(
+		struct ndctl_bus *bus)
+{
+	struct ndctl_ctx *ctx = ndctl_bus_get_ctx(bus);
+	char *path = bus->bus_buf;
+	char buf[SYSFS_ATTR_SIZE];
+	int len = bus->buf_len;
+
+	if (bus->fwa_state == NDCTL_FWA_INVALID)
+		return NDCTL_FWA_INVALID;
+
+	if (snprintf(path, len, "%s/firmware_activate", bus->bus_path) >= len) {
+		err(ctx, "%s: buffer too small!\n",
+				ndctl_bus_get_devname(bus));
+		return NDCTL_FWA_INVALID;
+	}
+
+	if (sysfs_read_attr(ctx, path, buf) < 0)
+		return NDCTL_FWA_INVALID;
+
+	bus->fwa_state = fwa_to_state(buf);
+
+	return bus->fwa_state;
+}
+
+NDCTL_EXPORT enum ndctl_fwa_method ndctl_bus_get_fw_activate_method(
+		struct ndctl_bus *bus)
+{
+	struct ndctl_ctx *ctx = ndctl_bus_get_ctx(bus);
+	char *path = bus->bus_buf;
+	int len = bus->buf_len;
+	struct stat st;
+	int rc;
+
+	if (bus->fwa_state == NDCTL_FWA_INVALID)
+		return NDCTL_FWA_METHOD_RESET;
+
+	if (snprintf(path, len, "%s/firmware_activate", bus->bus_path) >= len) {
+		err(ctx, "%s: buffer too small!\n",
+				ndctl_bus_get_devname(bus));
+		return NDCTL_FWA_INVALID;
+	}
+
+	rc = stat(path, &st);
+	if (rc < 0)
+		return NDCTL_FWA_METHOD_RESET;
+	if ((st.st_mode & 0600) == 0600)
+		return NDCTL_FWA_METHOD_LIVE;
+	if ((st.st_mode & 0400) == 0400)
+		return NDCTL_FWA_METHOD_SUSPEND;
+	return NDCTL_FWA_METHOD_RESET;
+}
+
+static enum ndctl_fwa_result fwa_result_to_result(const char *result)
+{
+	if (strcmp(result, "none") == 0)
+		return NDCTL_FWA_RESULT_NONE;
+	if (strcmp(result, "success") == 0)
+		return NDCTL_FWA_RESULT_SUCCESS;
+	if (strcmp(result, "fail") == 0)
+		return NDCTL_FWA_RESULT_FAIL;
+	if (strcmp(result, "not_staged") == 0)
+		return NDCTL_FWA_RESULT_NOTSTAGED;
+	if (strcmp(result, "need_reset") == 0)
+		return NDCTL_FWA_RESULT_NEEDRESET;
+	return NDCTL_FWA_RESULT_INVALID;
+}
+
 static int ndctl_bind(struct ndctl_ctx *ctx, struct kmod_module *module,
 		const char *devname);
 static int ndctl_unbind(struct ndctl_ctx *ctx, const char *devpath);
@@ -1515,6 +1602,18 @@ static void *add_dimm(void *parent, int id, const char *dimm_base)
 	} else
 		parse_dimm_flags(dimm, buf);
 
+	sprintf(path, "%s/firmware_activate", dimm_base);
+	if (sysfs_read_attr(ctx, path, buf) < 0)
+		dimm->fwa_state = NDCTL_FWA_INVALID;
+	else
+		dimm->fwa_state = fwa_to_state(buf);
+
+	sprintf(path, "%s/firmware_activate_result", dimm_base);
+	if (sysfs_read_attr(ctx, path, buf) < 0)
+		dimm->fwa_result = NDCTL_FWA_RESULT_INVALID;
+	else
+		dimm->fwa_result = fwa_result_to_result(buf);
+
 	if (!ndctl_bus_has_nfit(bus))
 		goto out;
 
@@ -1998,6 +2097,55 @@ NDCTL_EXPORT int ndctl_dimm_enable(struct ndctl_dimm *dimm)
 	return 0;
 }
 
+NDCTL_EXPORT enum ndctl_fwa_state ndctl_dimm_get_fw_activate_state(
+		struct ndctl_dimm *dimm)
+{
+	struct ndctl_ctx *ctx = ndctl_dimm_get_ctx(dimm);
+	char *path = dimm->dimm_buf;
+	char buf[SYSFS_ATTR_SIZE];
+	int len = dimm->buf_len;
+
+	if (dimm->fwa_state == NDCTL_FWA_INVALID)
+		return NDCTL_FWA_INVALID;
+
+	if (snprintf(path, len, "%s/firmware_activate",
+				dimm->dimm_path) >= len) {
+		err(ctx, "%s: buffer too small!\n",
+				ndctl_dimm_get_devname(dimm));
+		return NDCTL_FWA_INVALID;
+	}
+
+	if (sysfs_read_attr(ctx, path, buf) < 0)
+		return NDCTL_FWA_INVALID;
+
+	dimm->fwa_state = fwa_to_state(buf);
+	return dimm->fwa_state;
+}
+
+NDCTL_EXPORT enum ndctl_fwa_result ndctl_dimm_get_fw_activate_result(
+		struct ndctl_dimm *dimm)
+{
+	struct ndctl_ctx *ctx = ndctl_dimm_get_ctx(dimm);
+	char *path = dimm->dimm_buf;
+	char buf[SYSFS_ATTR_SIZE];
+	int len = dimm->buf_len;
+
+	if (dimm->fwa_result == NDCTL_FWA_RESULT_INVALID)
+		return NDCTL_FWA_RESULT_INVALID;
+
+	if (snprintf(path, len, "%s/firmware_activate_result",
+				dimm->dimm_path) >= len) {
+		err(ctx, "%s: buffer too small!\n",
+				ndctl_dimm_get_devname(dimm));
+		return NDCTL_FWA_RESULT_INVALID;
+	}
+
+	if (sysfs_read_attr(ctx, path, buf) < 0)
+		return NDCTL_FWA_RESULT_INVALID;
+
+	return fwa_result_to_result(buf);
+}
+
 NDCTL_EXPORT struct ndctl_dimm *ndctl_dimm_get_by_handle(struct ndctl_bus *bus,
 		unsigned int handle)
 {
diff --git a/ndctl/lib/libndctl.sym b/ndctl/lib/libndctl.sym
index ac575a23d035..37217036b0d8 100644
--- a/ndctl/lib/libndctl.sym
+++ b/ndctl/lib/libndctl.sym
@@ -431,3 +431,10 @@ LIBNDCTL_23 {
 	ndctl_region_get_align;
 	ndctl_region_set_align;
 } LIBNDCTL_22;
+
+LIBNDCTL_24 {
+	ndctl_dimm_get_fw_activate_state;
+	ndctl_dimm_get_fw_activate_result;
+	ndctl_bus_get_fw_activate_state;
+	ndctl_bus_get_fw_activate_method;
+} LIBNDCTL_23;
diff --git a/ndctl/lib/private.h b/ndctl/lib/private.h
index 2e537f0a8649..e1d61f52fed5 100644
--- a/ndctl/lib/private.h
+++ b/ndctl/lib/private.h
@@ -79,6 +79,8 @@ struct ndctl_dimm {
 	unsigned long cmd_mask;
 	unsigned long nfit_dsm_mask;
 	long long dirty_shutdown;
+	enum ndctl_fwa_state fwa_state;
+	enum ndctl_fwa_result fwa_result;
 	char *unique_id;
 	char *dimm_path;
 	char *dimm_buf;
@@ -174,6 +176,7 @@ struct ndctl_bus {
 	char *scrub_path;
 	unsigned long cmd_mask;
 	unsigned long nfit_dsm_mask;
+	enum ndctl_fwa_state fwa_state;
 };
 
 /**
diff --git a/ndctl/libndctl.h b/ndctl/libndctl.h
index 2580f433ade8..e66a52029481 100644
--- a/ndctl/libndctl.h
+++ b/ndctl/libndctl.h
@@ -110,6 +110,20 @@ enum ndctl_persistence_domain {
 	PERSISTENCE_UNKNOWN = INT_MAX,
 };
 
+enum ndctl_fwa_state {
+	NDCTL_FWA_INVALID,
+	NDCTL_FWA_IDLE,
+	NDCTL_FWA_ARMED,
+	NDCTL_FWA_BUSY,
+	NDCTL_FWA_ARM_OVERFLOW,
+};
+
+enum ndctl_fwa_method {
+	NDCTL_FWA_METHOD_RESET,
+	NDCTL_FWA_METHOD_SUSPEND,
+	NDCTL_FWA_METHOD_LIVE,
+};
+
 struct ndctl_bus;
 struct ndctl_bus *ndctl_bus_get_first(struct ndctl_ctx *ctx);
 struct ndctl_bus *ndctl_bus_get_next(struct ndctl_bus *bus);
@@ -139,6 +153,8 @@ unsigned int ndctl_bus_get_scrub_count(struct ndctl_bus *bus);
 int ndctl_bus_get_scrub_state(struct ndctl_bus *bus);
 int ndctl_bus_start_scrub(struct ndctl_bus *bus);
 int ndctl_bus_has_error_injection(struct ndctl_bus *bus);
+enum ndctl_fwa_state ndctl_bus_get_fw_activate_state(struct ndctl_bus *bus);
+enum ndctl_fwa_method ndctl_bus_get_fw_activate_method(struct ndctl_bus *bus);
 
 struct ndctl_dimm;
 struct ndctl_dimm *ndctl_dimm_get_first(struct ndctl_bus *bus);
@@ -702,6 +718,18 @@ enum ND_FW_STATUS ndctl_cmd_fw_xlat_firmware_status(struct ndctl_cmd *cmd);
 struct ndctl_cmd *ndctl_dimm_cmd_new_ack_shutdown_count(struct ndctl_dimm *dimm);
 int ndctl_dimm_fw_update_supported(struct ndctl_dimm *dimm);
 
+enum ndctl_fwa_result {
+        NDCTL_FWA_RESULT_INVALID,
+        NDCTL_FWA_RESULT_NONE,
+        NDCTL_FWA_RESULT_SUCCESS,
+        NDCTL_FWA_RESULT_NOTSTAGED,
+        NDCTL_FWA_RESULT_NEEDRESET,
+        NDCTL_FWA_RESULT_FAIL,
+};
+
+enum ndctl_fwa_state ndctl_dimm_get_fw_activate_state(struct ndctl_dimm *dimm);
+enum ndctl_fwa_result ndctl_dimm_get_fw_activate_result(struct ndctl_dimm *dimm);
+
 int ndctl_cmd_xlat_firmware_status(struct ndctl_cmd *cmd);
 int ndctl_cmd_submit_xlat(struct ndctl_cmd *cmd);
 
diff --git a/ndctl/list.c b/ndctl/list.c
index 1f7cc8ee1deb..f98148aea479 100644
--- a/ndctl/list.c
+++ b/ndctl/list.c
@@ -403,11 +403,12 @@ static bool filter_bus(struct ndctl_bus *bus, struct util_filter_ctx *ctx)
 		}
 	}
 
-	lfa->jbus = util_bus_to_json(bus);
+	lfa->jbus = util_bus_to_json(bus, lfa->flags);
 	if (!lfa->jbus) {
 		fail("\n");
 		return false;
 	}
+
 	json_object_array_add(lfa->jbuses, lfa->jbus);
 	return true;
 }
diff --git a/util/json.c b/util/json.c
index 59a3d07cc4a6..77bd4781551d 100644
--- a/util/json.c
+++ b/util/json.c
@@ -122,10 +122,10 @@ void util_display_json_array(FILE *f_out, struct json_object *jarray,
 	json_object_put(jarray);
 }
 
-struct json_object *util_bus_to_json(struct ndctl_bus *bus)
+struct json_object *util_bus_to_json(struct ndctl_bus *bus, unsigned long flags)
 {
 	struct json_object *jbus = json_object_new_object();
-	struct json_object *jobj;
+	struct json_object *jobj, *fw_obj = NULL;
 	int scrub;
 
 	if (!jbus)
@@ -150,6 +150,49 @@ struct json_object *util_bus_to_json(struct ndctl_bus *bus)
 		goto err;
 	json_object_object_add(jbus, "scrub_state", jobj);
 
+	if (flags & UTIL_JSON_FIRMWARE) {
+		struct ndctl_dimm *dimm;
+
+		/*
+		 * Skip displaying firmware activation capability if no
+		 * DIMMs support firmware update.
+		 */
+		ndctl_dimm_foreach(bus, dimm)
+			if (ndctl_dimm_fw_update_supported(dimm) == 0) {
+				fw_obj = json_object_new_object();
+				break;
+			}
+	}
+
+	if (fw_obj) {
+		enum ndctl_fwa_state state;
+		enum ndctl_fwa_method method;
+
+		jobj = NULL;
+		method = ndctl_bus_get_fw_activate_method(bus);
+		if (method == NDCTL_FWA_METHOD_RESET)
+			jobj = json_object_new_string("reset");
+		if (method == NDCTL_FWA_METHOD_SUSPEND)
+			jobj = json_object_new_string("suspend");
+		if (method == NDCTL_FWA_METHOD_LIVE)
+			jobj = json_object_new_string("live");
+		if (jobj)
+			json_object_object_add(fw_obj, "activate_method", jobj);
+
+		jobj = NULL;
+		state = ndctl_bus_get_fw_activate_state(bus);
+		if (state == NDCTL_FWA_ARMED)
+			jobj = json_object_new_string("armed");
+		if (state == NDCTL_FWA_IDLE)
+			jobj = json_object_new_string("idle");
+		if (state == NDCTL_FWA_ARM_OVERFLOW)
+			jobj = json_object_new_string("overflow");
+		if (jobj)
+			json_object_object_add(fw_obj, "activate_state", jobj);
+
+		json_object_object_add(jbus, "firmware", fw_obj);
+	}
+
 	return jbus;
  err:
 	json_object_put(jbus);
@@ -160,10 +203,13 @@ struct json_object *util_dimm_firmware_to_json(struct ndctl_dimm *dimm,
 		unsigned long flags)
 {
 	struct json_object *jfirmware = json_object_new_object();
+	bool can_update, need_powercycle;
+	enum ndctl_fwa_result result;
+	enum ndctl_fwa_state state;
 	struct json_object *jobj;
 	struct ndctl_cmd *cmd;
-	int rc;
 	uint64_t run, next;
+	int rc;
 
 	if (!jfirmware)
 		return NULL;
@@ -195,10 +241,12 @@ struct json_object *util_dimm_firmware_to_json(struct ndctl_dimm *dimm,
 		json_object_object_add(jfirmware, "current_version", jobj);
 
 	rc = ndctl_dimm_fw_update_supported(dimm);
-	jobj = json_object_new_boolean(rc == 0);
+	can_update = rc == 0;
+	jobj = json_object_new_boolean(can_update);
 	if (jobj)
 		json_object_object_add(jfirmware, "can_update", jobj);
 
+
 	next = ndctl_cmd_fw_info_get_updated_version(cmd);
 	if (next == ULLONG_MAX) {
 		jobj = util_json_object_hex(-1, flags);
@@ -208,16 +256,61 @@ struct json_object *util_dimm_firmware_to_json(struct ndctl_dimm *dimm,
 		goto out;
 	}
 
-	if (next != 0) {
-		jobj = util_json_object_hex(next, flags);
-		if (jobj)
-			json_object_object_add(jfirmware,
-					"next_version", jobj);
+	if (!next)
+		goto out;
+
+	jobj = util_json_object_hex(next, flags);
+	if (jobj)
+		json_object_object_add(jfirmware,
+				"next_version", jobj);
+
+	state = ndctl_dimm_get_fw_activate_state(dimm);
+	switch (state) {
+	case NDCTL_FWA_IDLE:
+		jobj = json_object_new_string("idle");
+		break;
+	case NDCTL_FWA_ARMED:
+		jobj = json_object_new_string("armed");
+		break;
+	case NDCTL_FWA_BUSY:
+		jobj = json_object_new_string("busy");
+		break;
+	default:
+		jobj = NULL;
+		break;
+	}
+	if (jobj)
+		json_object_object_add(jfirmware, "activate_state", jobj);
+
+	result = ndctl_dimm_get_fw_activate_result(dimm);
+	switch (result) {
+	case NDCTL_FWA_RESULT_NONE:
+	case NDCTL_FWA_RESULT_SUCCESS:
+	case NDCTL_FWA_RESULT_NOTSTAGED:
+		/*
+		 * If a 'next' firmware version is staged then this
+		 * result is stale, if the activation succeeds that is
+		 * indicated by not finding a 'next' entry.
+		 */
+		need_powercycle = false;
+		break;
+	case NDCTL_FWA_RESULT_NEEDRESET:
+	case NDCTL_FWA_RESULT_FAIL:
+	default:
+		/*
+		 * If the last activation failed, or if the activation
+		 * result is unavailable it is always the case that the
+		 * only remediation is powercycle.
+		 */
+		need_powercycle = true;
+		break;
+	}
 
+	if (need_powercycle) {
 		jobj = json_object_new_boolean(true);
-		if (jobj)
-			json_object_object_add(jfirmware,
-					"need_powercycle", jobj);
+		if (!jobj)
+			goto out;
+		json_object_object_add(jfirmware, "need_powercycle", jobj);
 	}
 
 	ndctl_cmd_unref(cmd);
diff --git a/util/json.h b/util/json.h
index fc91a8db034f..39a33789bac9 100644
--- a/util/json.h
+++ b/util/json.h
@@ -32,7 +32,8 @@ enum util_json_flags {
 struct json_object;
 void util_display_json_array(FILE *f_out, struct json_object *jarray,
 		unsigned long flags);
-struct json_object *util_bus_to_json(struct ndctl_bus *bus);
+struct json_object *util_bus_to_json(struct ndctl_bus *bus,
+		unsigned long flags);
 struct json_object *util_dimm_to_json(struct ndctl_dimm *dimm,
 		unsigned long flags);
 struct json_object *util_mapping_to_json(struct ndctl_mapping *mapping,
