diff --git a/Documentation/ABI/testing/sysfs-driver-ucsi b/Documentation/ABI/testing/sysfs-driver-ucsi
new file mode 100644
index 000000000000..9da15577f4ae
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-ucsi
@@ -0,0 +1,20 @@
+What:		/sys/class/typec/<port>/device/ucsi
+Date:		February 2025
+Contact:	Heikki Krogerus <heikki.krogerus@linux.intel.com>
+Description:
+		Command mailbox for UCSI (USB Type-C System Software Interface).
+
+		The mailbox contains a copy of the main UCSI data structure.
+		Sending a command happens by writing the command specific data
+		structure to the CONTROL offset just like defined in the UCSI
+		specification. When a command is written to the mailbox, it is
+		automatically forwarded to the Platform Policy Manager (PPM) of
+		the UCSI instance.
+
+		After writing the command to the mailbox, the result can be read
+		directly from the CCI and MESSAGE_IN offsets. The mailbox takes
+		care of command completion acknowledges automatically.
+
+		Note. The mailbox is meant, and can only be used for, sending
+		commands. I.e. the mailbox is not updated when the UCSI receives
+		asynchronous events.
diff --git a/drivers/usb/typec/ucsi/Makefile b/drivers/usb/typec/ucsi/Makefile
index be98a879104d..5ebc74e3055b 100644
--- a/drivers/usb/typec/ucsi/Makefile
+++ b/drivers/usb/typec/ucsi/Makefile
@@ -3,7 +3,7 @@ CFLAGS_trace.o				:= -I$(src)
 
 obj-$(CONFIG_TYPEC_UCSI)		+= typec_ucsi.o
 
-typec_ucsi-y				:= ucsi.o
+typec_ucsi-y				:= ucsi.o sysfs.o
 
 typec_ucsi-$(CONFIG_DEBUG_FS)		+= debugfs.o
 
diff --git a/drivers/usb/typec/ucsi/sysfs.c b/drivers/usb/typec/ucsi/sysfs.c
new file mode 100644
index 000000000000..06ea1b54aefa
--- /dev/null
+++ b/drivers/usb/typec/ucsi/sysfs.c
@@ -0,0 +1,127 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * UCSI sysfs mailbox.
+ *
+ * Copyright (C) 2025, Intel Corporation
+ */
+
+#include <linux/mutex.h>
+#include <linux/overflow.h>
+#include <linux/slab.h>
+#include <linux/sysfs.h>
+#include <linux/string.h>
+#include <linux/types.h>
+#include <linux/unaligned.h>
+#include "ucsi.h"
+
+#define UCSI_MAILBOX_SIZE(ucsi) ((ucsi)->version < UCSI_VERSION_2_0 ? 48 : 528)
+
+struct ucsi_sysfs {
+	struct bin_attribute	bin_attr;
+	struct ucsi		*ucsi;
+	struct mutex		lock; /* mailbox lock */
+	u8			mailbox[];
+};
+
+static ssize_t ucsi_read(struct file *filp, struct kobject *kobj,
+			 const struct bin_attribute *attr,
+			 char *buf, loff_t off, size_t count)
+{
+	struct ucsi_sysfs *sysfs = attr->private;
+
+	mutex_lock(&sysfs->lock);
+	memcpy(buf, sysfs->mailbox + off, count);
+	mutex_unlock(&sysfs->lock);
+
+	return count;
+}
+
+static ssize_t ucsi_write(struct file *filp, struct kobject *kobj,
+			  const struct bin_attribute *attr,
+			  char *buf, loff_t off, size_t count)
+{
+	struct ucsi_sysfs *sysfs = attr->private;
+	struct ucsi *ucsi = sysfs->ucsi;
+	int ret;
+
+	u64 *control = (u64 *)&sysfs->mailbox[UCSI_CONTROL];
+	u32 *cci = (u32 *)&sysfs->mailbox[UCSI_CCI];
+	void *data = &sysfs->mailbox[UCSI_MESSAGE_IN];
+
+	/* TODO: MESSAGE_OUT. */
+	if (off != UCSI_CONTROL || count != sizeof(*control))
+		return -EFAULT;
+
+	mutex_lock(&sysfs->lock);
+
+	memset(data, 0, UCSI_MAX_DATA_LENGTH(ucsi));
+
+	/* PPM_RESET has to be handled separately. */
+	*control = get_unaligned_le64(buf);
+	if (UCSI_COMMAND(*control) == UCSI_PPM_RESET) {
+		ret = ucsi_reset_ppm(ucsi, cci);
+		goto out_unlock_sysfs;
+	}
+
+	mutex_lock(&ucsi->ppm_lock);
+
+	ret = ucsi->ops->sync_control(ucsi, *control, cci, NULL, 0);
+	if (ret)
+		goto out_unlock_ppm;
+
+	if (UCSI_CCI_LENGTH(*cci) && ucsi->ops->read_message_in(ucsi, data, UCSI_CCI_LENGTH(*cci)))
+		dev_err(ucsi->dev, "failed to read MESSAGE_IN\n");
+
+	ret = ucsi->ops->sync_control(ucsi, UCSI_ACK_CC_CI | UCSI_ACK_COMMAND_COMPLETE,
+				      NULL, NULL, 0);
+out_unlock_ppm:
+	mutex_unlock(&ucsi->ppm_lock);
+out_unlock_sysfs:
+	mutex_unlock(&sysfs->lock);
+
+	return ret ?: count;
+}
+
+int ucsi_sysfs_register(struct ucsi *ucsi)
+{
+	struct ucsi_sysfs *sysfs;
+	int ret;
+
+	sysfs = kzalloc(struct_size(sysfs, mailbox, UCSI_MAILBOX_SIZE(ucsi)), GFP_KERNEL);
+	if (!sysfs)
+		return -ENOMEM;
+
+	sysfs->ucsi = ucsi;
+	mutex_init(&sysfs->lock);
+	memcpy(sysfs->mailbox, &ucsi->version, sizeof(ucsi->version));
+
+	sysfs_bin_attr_init(&sysfs->bin_attr);
+
+	sysfs->bin_attr.attr.name = "ucsi";
+	sysfs->bin_attr.attr.mode = 0644;
+
+	sysfs->bin_attr.size = UCSI_MAILBOX_SIZE(ucsi);
+	sysfs->bin_attr.private = sysfs;
+	sysfs->bin_attr.read_new = ucsi_read;
+	sysfs->bin_attr.write_new = ucsi_write;
+
+	ret = sysfs_create_bin_file(&ucsi->dev->kobj, &sysfs->bin_attr);
+	if (ret)
+		kfree(sysfs);
+	else
+		ucsi->sysfs = sysfs;
+
+	return ret;
+}
+
+void ucsi_sysfs_unregister(struct ucsi *ucsi)
+{
+	struct ucsi_sysfs *sysfs = ucsi->sysfs;
+
+	if (!sysfs)
+		return;
+
+	sysfs_remove_bin_file(&ucsi->dev->kobj, &sysfs->bin_attr);
+	ucsi->sysfs = NULL;
+	kfree(sysfs);
+}
diff --git a/drivers/usb/typec/ucsi/ucsi.c b/drivers/usb/typec/ucsi/ucsi.c
index 559390a07a4e..9dadfe879319 100644
--- a/drivers/usb/typec/ucsi/ucsi.c
+++ b/drivers/usb/typec/ucsi/ucsi.c
@@ -1340,16 +1340,18 @@ static int ucsi_reset_connector(struct ucsi_connector *con, bool hard)
 	return ucsi_send_command(con->ucsi, command, NULL, 0);
 }
 
-static int ucsi_reset_ppm(struct ucsi *ucsi)
+int ucsi_reset_ppm(struct ucsi *ucsi, u32 *cci)
 {
 	u64 command;
 	unsigned long tmo;
-	u32 cci;
+	u32 _cci;
 	int ret;
 
+	cci = cci ?: &_cci;
+
 	mutex_lock(&ucsi->ppm_lock);
 
-	ret = ucsi->ops->read_cci(ucsi, &cci);
+	ret = ucsi->ops->read_cci(ucsi, cci);
 	if (ret < 0)
 		goto out;
 
@@ -1359,7 +1361,7 @@ static int ucsi_reset_ppm(struct ucsi *ucsi)
 	 * UCSI_SET_NOTIFICATION_ENABLE command to achieve this.
 	 * Ignore a timeout and try the reset anyway if this fails.
 	 */
-	if (cci & UCSI_CCI_RESET_COMPLETE) {
+	if (*cci & UCSI_CCI_RESET_COMPLETE) {
 		command = UCSI_SET_NOTIFICATION_ENABLE;
 		ret = ucsi->ops->async_control(ucsi, command);
 		if (ret < 0)
@@ -1367,17 +1369,17 @@ static int ucsi_reset_ppm(struct ucsi *ucsi)
 
 		tmo = jiffies + msecs_to_jiffies(UCSI_TIMEOUT_MS);
 		do {
-			ret = ucsi->ops->read_cci(ucsi, &cci);
+			ret = ucsi->ops->read_cci(ucsi, cci);
 			if (ret < 0)
 				goto out;
-			if (cci & UCSI_CCI_COMMAND_COMPLETE)
+			if (*cci & UCSI_CCI_COMMAND_COMPLETE)
 				break;
 			if (time_is_before_jiffies(tmo))
 				break;
 			msleep(20);
 		} while (1);
 
-		WARN_ON(cci & UCSI_CCI_RESET_COMPLETE);
+		WARN_ON(*cci & UCSI_CCI_RESET_COMPLETE);
 	}
 
 	command = UCSI_PPM_RESET;
@@ -1396,18 +1398,18 @@ static int ucsi_reset_ppm(struct ucsi *ucsi)
 		/* Give the PPM time to process a reset before reading CCI */
 		msleep(20);
 
-		ret = ucsi->ops->read_cci(ucsi, &cci);
+		ret = ucsi->ops->read_cci(ucsi, cci);
 		if (ret)
 			goto out;
 
 		/* If the PPM is still doing something else, reset it again. */
-		if (cci & ~UCSI_CCI_RESET_COMPLETE) {
+		if (*cci & ~UCSI_CCI_RESET_COMPLETE) {
 			ret = ucsi->ops->async_control(ucsi, command);
 			if (ret < 0)
 				goto out;
 		}
 
-	} while (!(cci & UCSI_CCI_RESET_COMPLETE));
+	} while (!(*cci & UCSI_CCI_RESET_COMPLETE));
 
 out:
 	mutex_unlock(&ucsi->ppm_lock);
@@ -1423,7 +1425,7 @@ static int ucsi_role_cmd(struct ucsi_connector *con, u64 command)
 		u64 c;
 
 		/* PPM most likely stopped responding. Resetting everything. */
-		ucsi_reset_ppm(con->ucsi);
+		ucsi_reset_ppm(con->ucsi, NULL);
 
 		c = UCSI_SET_NOTIFICATION_ENABLE | con->ucsi->ntfy;
 		ucsi_send_command(con->ucsi, c, NULL, 0);
@@ -1766,7 +1768,7 @@ static int ucsi_init(struct ucsi *ucsi)
 	int i;
 
 	/* Reset the PPM */
-	ret = ucsi_reset_ppm(ucsi);
+	ret = ucsi_reset_ppm(ucsi, NULL);
 	if (ret) {
 		dev_err(ucsi->dev, "failed to reset PPM!\n");
 		goto err;
@@ -1846,7 +1848,7 @@ static int ucsi_init(struct ucsi *ucsi)
 	kfree(connector);
 err_reset:
 	memset(&ucsi->cap, 0, sizeof(ucsi->cap));
-	ucsi_reset_ppm(ucsi);
+	ucsi_reset_ppm(ucsi, NULL);
 err:
 	return ret;
 }
@@ -1958,6 +1960,7 @@ EXPORT_SYMBOL_GPL(ucsi_create);
 void ucsi_destroy(struct ucsi *ucsi)
 {
 	ucsi_debugfs_unregister(ucsi);
+	ucsi_sysfs_unregister(ucsi);
 	kfree(ucsi);
 }
 EXPORT_SYMBOL_GPL(ucsi_destroy);
@@ -1989,6 +1992,8 @@ int ucsi_register(struct ucsi *ucsi)
 	queue_delayed_work(system_long_wq, &ucsi->work, 0);
 
 	ucsi_debugfs_register(ucsi);
+	ucsi_sysfs_register(ucsi);
+
 	return 0;
 }
 EXPORT_SYMBOL_GPL(ucsi_register);
diff --git a/drivers/usb/typec/ucsi/ucsi.h b/drivers/usb/typec/ucsi/ucsi.h
index feb012db4c89..d5b90e57cf42 100644
--- a/drivers/usb/typec/ucsi/ucsi.h
+++ b/drivers/usb/typec/ucsi/ucsi.h
@@ -468,6 +468,8 @@ struct ucsi {
 	unsigned long quirks;
 #define UCSI_NO_PARTNER_PDOS	BIT(0)	/* Don't read partner's PDOs */
 #define UCSI_DELAY_DEVICE_PDOS	BIT(1)	/* Reading PDOs fails until the parter is in PD mode */
+
+	void *sysfs;
 };
 
 #define UCSI_MAX_DATA_LENGTH(u) (((u)->version < UCSI_VERSION_2_0) ? 0x10 : 0xff)
@@ -535,6 +537,8 @@ void ucsi_notify_common(struct ucsi *ucsi, u32 cci);
 int ucsi_sync_control_common(struct ucsi *ucsi, u64 command, u32 *cci,
 			     void *data, size_t size);
 
+int ucsi_reset_ppm(struct ucsi *ucsi, u32 *cci);
+
 #if IS_ENABLED(CONFIG_POWER_SUPPLY)
 int ucsi_register_port_psy(struct ucsi_connector *con);
 void ucsi_unregister_port_psy(struct ucsi_connector *con);
@@ -578,6 +582,9 @@ static inline void ucsi_debugfs_register(struct ucsi *ucsi) { }
 static inline void ucsi_debugfs_unregister(struct ucsi *ucsi) { }
 #endif /* CONFIG_DEBUG_FS */
 
+int ucsi_sysfs_register(struct ucsi *ucsi);
+void ucsi_sysfs_unregister(struct ucsi *ucsi);
+
 /*
  * NVIDIA VirtualLink (svid 0x955) has two altmode. VirtualLink
  * DP mode with vdo=0x1 and NVIDIA test mode with vdo=0x3
