* [PATCH 1/7] platform/x86: asus-wmi: dump buffer replies in the debugfs dsts file
2026-11-01 10:46 [PATCH 0/7] platform/x86: asus-wmi: expose the firmware fan floor tables Bartu Alev
@ 2026-11-01 10:46 ` Bartu Alev
2026-11-01 10:51 ` sashiko-bot
2026-11-01 10:46 ` [PATCH 2/7] Documentation: ABI: document the ASUS custom fan curve attributes Bartu Alev
` (5 subsequent siblings)
6 siblings, 1 reply; 15+ messages in thread
From: Bartu Alev @ 2026-11-01 10:46 UTC (permalink / raw)
To: Corentin Chary, Luke D. Jones, Denis Benato, Hans de Goede,
Ilpo Järvinen
Cc: platform-driver-x86, linux-kernel, linux-hwmon, Guenter Roeck,
linux-api, Bartu Alev
Reading dsts calls asus_wmi_get_devstate(), which uses
asus_wmi_evaluate_method(). Because that helper handles only
ACPI_TYPE_INTEGER and leaves other return types zeroed, methods
returning an ACPI buffer read as 0x0. This affects fan floor tables
(0x00110026, 0x00110027, 0x00110033), fan curve getters (0x00110024,
0x00110025), and TUF RGB readback (0x0010005B).
Evaluate the method through asus_wmi_evaluate_method_locked() directly,
matching the call node, which keeps execution serialized against other
WMI operations. Format integer replies as before, dump buffer
contents with their byte length, and print any other type as t:<type>:
DSTS(0x110026) = 16 bytes:
00000000: 00 00 00 00 19 30 3a 4a 00 00 00 00 00 00 00 00
The sentinels 0xfffffffe and 0xffffffff continue to return -ENODEV.
Signed-off-by: Bartu Alev <bartualev@gmail.com>
---
drivers/platform/x86/asus-wmi.c | 46 ++++++++++++++++++++++++++++++++++-------
1 file changed, 39 insertions(+), 7 deletions(-)
diff --git a/drivers/platform/x86/asus-wmi.c b/drivers/platform/x86/asus-wmi.c
index b33ebdb21aa21..c55f469cb03dc 100644
--- a/drivers/platform/x86/asus-wmi.c
+++ b/drivers/platform/x86/asus-wmi.c
@@ -5038,16 +5038,48 @@ struct asus_wmi_debugfs_node {
static int show_dsts(struct seq_file *m, void *data)
{
struct asus_wmi *asus = m->private;
- int err;
- u32 retval = -1;
+ struct bios_args args = {
+ .arg0 = asus->debug.dev_id,
+ };
+ struct acpi_buffer input = { (acpi_size) sizeof(args), &args };
+ struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL };
+ union acpi_object *obj;
+ acpi_status status;
+ u32 retval;
+ int err = 0;
- err = asus_wmi_get_devstate(asus, asus->debug.dev_id, &retval);
- if (err < 0)
- return err;
+ status = asus_wmi_evaluate_method_locked(asus->dsts_id, &input, &output);
+ if (ACPI_FAILURE(status))
+ return -EIO;
- seq_printf(m, "DSTS(%#x) = %#x\n", asus->debug.dev_id, retval);
+ obj = (union acpi_object *)output.pointer;
+ if (!obj)
+ return -ENODATA;
- return 0;
+ switch (obj->type) {
+ case ACPI_TYPE_BUFFER:
+ seq_printf(m, "DSTS(%#x) = %u bytes:\n", asus->debug.dev_id,
+ obj->buffer.length);
+ seq_hex_dump(m, " ", DUMP_PREFIX_OFFSET, 16, 1,
+ obj->buffer.pointer, obj->buffer.length, false);
+ break;
+ case ACPI_TYPE_INTEGER:
+ retval = (u32)obj->integer.value;
+ if (retval == ASUS_WMI_UNSUPPORTED_METHOD || retval == (u32)~0)
+ err = -ENODEV;
+ else
+ seq_printf(m, "DSTS(%#x) = %#x\n", asus->debug.dev_id,
+ retval);
+ break;
+ default:
+ seq_printf(m, "DSTS(%#x) = t:%d\n", asus->debug.dev_id,
+ obj->type);
+ break;
+ }
+
+ kfree(obj);
+
+ return err;
}
static int show_devs(struct seq_file *m, void *data)
--
2.56.0
^ permalink raw reply related [flat|nested] 15+ messages in thread* Re: [PATCH 1/7] platform/x86: asus-wmi: dump buffer replies in the debugfs dsts file
2026-11-01 10:46 ` [PATCH 1/7] platform/x86: asus-wmi: dump buffer replies in the debugfs dsts file Bartu Alev
@ 2026-11-01 10:51 ` sashiko-bot
0 siblings, 0 replies; 15+ messages in thread
From: sashiko-bot @ 2026-11-01 10:51 UTC (permalink / raw)
To: Bartu Alev; +Cc: linux-hwmon
> Reading dsts calls asus_wmi_get_devstate(), which uses
> asus_wmi_evaluate_method(). Because that helper handles only
> ACPI_TYPE_INTEGER and leaves other return types zeroed, methods
> returning an ACPI buffer read as 0x0. This affects fan floor tables
> (0x00110026, 0x00110027, 0x00110033), fan curve getters (0x00110024,
> 0x00110025), and TUF RGB readback (0x0010005B).
>
> Evaluate the method through asus_wmi_evaluate_method_locked() directly,
> matching the call node, which keeps execution serialized against other
> WMI operations. Format integer replies as before, dump buffer
> contents with their byte length, and print any other type as t:<type>:
>
> DSTS(0x110026) = 16 bytes:
> 00000000: 00 00 00 00 19 30 3a 4a 00 00 00 00 00 00 00 00
>
> The sentinels 0xfffffffe and 0xffffffff continue to return -ENODEV.
>
> Signed-off-by: Bartu Alev <bartualev@gmail.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261101-asus-fan-floors-v4-v1-0-b8d1c31a99a9@gmail.com?part=1
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH 2/7] Documentation: ABI: document the ASUS custom fan curve attributes
2026-11-01 10:46 [PATCH 0/7] platform/x86: asus-wmi: expose the firmware fan floor tables Bartu Alev
2026-11-01 10:46 ` [PATCH 1/7] platform/x86: asus-wmi: dump buffer replies in the debugfs dsts file Bartu Alev
@ 2026-11-01 10:46 ` Bartu Alev
2026-11-01 10:50 ` sashiko-bot
2026-11-01 10:46 ` [PATCH 3/7] platform/x86: asus-wmi: define the fan floor table DEVIDs Bartu Alev
` (4 subsequent siblings)
6 siblings, 1 reply; 15+ messages in thread
From: Bartu Alev @ 2026-11-01 10:46 UTC (permalink / raw)
To: Corentin Chary, Luke D. Jones, Denis Benato, Hans de Goede,
Ilpo Järvinen
Cc: platform-driver-x86, linux-kernel, linux-hwmon, Guenter Roeck,
linux-api, Bartu Alev
asus-wmi has exposed custom fan curve attributes since 5.17 for CPU
and GPU fans, and since 6.6 for the mid fan, but the sysfs ABI was
never documented. Document the existing attributes before adding the
firmware fan floor tables.
This includes several embedded controller quirks: point Z is limited
to the temperature decade [30 + 10 * (Z - 1), 39 + 10 * (Z - 1)]
degrees Celsius, the EC clamps uploaded thresholds to that decade
while reads return the raw byte written, fan speeds step without
interpolation, and downward transitions use 5 degrees Celsius of
hysteresis.
Signed-off-by: Bartu Alev <bartualev@gmail.com>
---
Documentation/ABI/testing/sysfs-platform-asus-wmi | 59 +++++++++++++++++++++++
1 file changed, 59 insertions(+)
diff --git a/Documentation/ABI/testing/sysfs-platform-asus-wmi b/Documentation/ABI/testing/sysfs-platform-asus-wmi
index f9825c6150b5c..775ea88625c0d 100644
--- a/Documentation/ABI/testing/sysfs-platform-asus-wmi
+++ b/Documentation/ABI/testing/sysfs-platform-asus-wmi
@@ -238,3 +238,62 @@ Description:
Set if the MCU can go in to low-power mode on system sleep
* 0 - False,
* 1 - True
+
+What: /sys/class/hwmon/hwmonX/pwm[12]_enable
+Date: Oct 2026
+KernelVersion: 5.17
+Contact: Luke D. Jones <luke@ljones.dev>, Denis Benato <denis.benato@linux.dev>, Bartu Alev <bartualev@gmail.com>
+Description:
+ Enable or disable the custom fan curve of the CPU fan (pwm1)
+ or the GPU fan (pwm2). Reads return 1 while the custom curve
+ is enabled and 2 while the firmware controls the fan. Writes:
+
+ * 1 - enable the stored curve,
+ * 2 - disable the curve and let the firmware control the
+ fan again,
+ * 3 - reset the stored curve to the firmware defaults and
+ disable it.
+
+ Enabling validates the curve: temperatures and duties must
+ not decrease from point to point, otherwise the write fails
+ with -EINVAL and the curve stays disabled. Writing any
+ writable pwmY_auto_pointZ_temp or pwmY_auto_pointZ_pwm
+ attribute disables the curve, so a changed curve has to be
+ enabled again.
+
+What: /sys/class/hwmon/hwmonX/pwm3_enable
+Date: Oct 2026
+KernelVersion: 6.6
+Contact: Luke D. Jones <luke@ljones.dev>, Denis Benato <denis.benato@linux.dev>, Bartu Alev <bartualev@gmail.com>
+Description:
+ Same as pwm1_enable and pwm2_enable, for the mid fan (pwm3)
+ of laptops whose firmware exposes a third fan curve.
+
+What: /sys/class/hwmon/hwmonX/pwm[12]_auto_point[1-8]_temp
+What: /sys/class/hwmon/hwmonX/pwm[12]_auto_point[1-8]_pwm
+Date: Oct 2026
+KernelVersion: 5.17
+Contact: Luke D. Jones <luke@ljones.dev>, Denis Benato <denis.benato@linux.dev>, Bartu Alev <bartualev@gmail.com>
+Description:
+ One point of the custom fan curve of the CPU fan (pwm1) or
+ the GPU fan (pwm2). _temp is the temperature threshold of the
+ point in degrees Celsius, _pwm is its duty in pwm units
+ (0-255); the driver scales the duty to the percentage the
+ firmware expects when the curve is enabled.
+
+ The embedded controller binds point Z (Z = 1..8) to the
+ temperature decade [30 + 10 * (Z - 1), 39 + 10 * (Z - 1)]
+ degrees Celsius and silently clamps the threshold into that
+ decade when the curve is applied, while reads return the
+ value that was written. On TUF series, the controller steps
+ from one point to the next without interpolating, and a point
+ is left downwards with 5 degrees Celsius of hysteresis.
+
+What: /sys/class/hwmon/hwmonX/pwm3_auto_point[1-8]_temp
+What: /sys/class/hwmon/hwmonX/pwm3_auto_point[1-8]_pwm
+Date: Oct 2026
+KernelVersion: 6.6
+Contact: Luke D. Jones <luke@ljones.dev>, Denis Benato <denis.benato@linux.dev>, Bartu Alev <bartualev@gmail.com>
+Description:
+ Same as the pwm1 and pwm2 curve points, for the mid fan
+ (pwm3) of laptops whose firmware exposes a third fan curve.
--
2.56.0
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH 3/7] platform/x86: asus-wmi: define the fan floor table DEVIDs
2026-11-01 10:46 [PATCH 0/7] platform/x86: asus-wmi: expose the firmware fan floor tables Bartu Alev
2026-11-01 10:46 ` [PATCH 1/7] platform/x86: asus-wmi: dump buffer replies in the debugfs dsts file Bartu Alev
2026-11-01 10:46 ` [PATCH 2/7] Documentation: ABI: document the ASUS custom fan curve attributes Bartu Alev
@ 2026-11-01 10:46 ` Bartu Alev
2026-11-01 10:49 ` sashiko-bot
2026-11-01 10:46 ` [PATCH 4/7] platform/x86: asus-wmi: read the firmware fan floor tables Bartu Alev
` (3 subsequent siblings)
6 siblings, 1 reply; 15+ messages in thread
From: Bartu Alev @ 2026-11-01 10:46 UTC (permalink / raw)
To: Corentin Chary, Luke D. Jones, Denis Benato, Hans de Goede,
Ilpo Järvinen
Cc: platform-driver-x86, linux-kernel, linux-hwmon, Guenter Roeck,
linux-api, Bartu Alev
On some ASUS laptops, the embedded controller enforces minimum fan
duties on top of custom fan curves and reports them through three
read-only DSTS methods:
0x00110026 CPU fan floor table
0x00110027 GPU fan floor table
0x00110033 mid fan floor table
The mid fan floor uses 0x00110033 rather than the adjacent 0x00110028.
On examined firmware (FA507, G614PR), 0x00110028 and 0x00110029 are
stubs; while GV601 exposes a GFLB(0x28) AML selector, it is unrelated
to the DSTS devid.
Define these DEVIDs so a follow-up commit can read the tables.
Signed-off-by: Bartu Alev <bartualev@gmail.com>
---
include/linux/platform_data/x86/asus-wmi.h | 3 +++
1 file changed, 3 insertions(+)
diff --git a/include/linux/platform_data/x86/asus-wmi.h b/include/linux/platform_data/x86/asus-wmi.h
index 66c7ab35d6c81..44189c5e6f2cb 100644
--- a/include/linux/platform_data/x86/asus-wmi.h
+++ b/include/linux/platform_data/x86/asus-wmi.h
@@ -102,7 +102,10 @@
#define ASUS_WMI_DEVID_MID_FAN_CTRL 0x00110031
#define ASUS_WMI_DEVID_CPU_FAN_CURVE 0x00110024
#define ASUS_WMI_DEVID_GPU_FAN_CURVE 0x00110025
+#define ASUS_WMI_DEVID_CPU_FAN_FLOOR 0x00110026
+#define ASUS_WMI_DEVID_GPU_FAN_FLOOR 0x00110027
#define ASUS_WMI_DEVID_MID_FAN_CURVE 0x00110032
+#define ASUS_WMI_DEVID_MID_FAN_FLOOR 0x00110033
/* Tunables for AUS ROG laptops */
#define ASUS_WMI_DEVID_PPT_PL2_SPPT 0x001200A0
--
2.56.0
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH 4/7] platform/x86: asus-wmi: read the firmware fan floor tables
2026-11-01 10:46 [PATCH 0/7] platform/x86: asus-wmi: expose the firmware fan floor tables Bartu Alev
` (2 preceding siblings ...)
2026-11-01 10:46 ` [PATCH 3/7] platform/x86: asus-wmi: define the fan floor table DEVIDs Bartu Alev
@ 2026-11-01 10:46 ` Bartu Alev
2026-11-01 11:02 ` sashiko-bot
2026-11-01 10:46 ` [PATCH 5/7] platform/x86: asus-wmi: reject short fan curve replies Bartu Alev
` (2 subsequent siblings)
6 siblings, 1 reply; 15+ messages in thread
From: Bartu Alev @ 2026-11-01 10:46 UTC (permalink / raw)
To: Corentin Chary, Luke D. Jones, Denis Benato, Hans de Goede,
Ilpo Järvinen
Cc: platform-driver-x86, linux-kernel, linux-hwmon, Guenter Roeck,
linux-api, Bartu Alev
On some ASUS laptops, the embedded controller enforces minimum fan
duties on top of custom fan curves written via CPU_FAN_CURVE
(0x00110024), GPU_FAN_CURVE (0x00110025), and the mid fan curve
methods. The floors are exposed via read-only DSTS methods 0x00110026,
0x00110027, and 0x00110033.
The floor table is indexed by curve point: entry i (0-based, sysfs
point i + 1) provides the minimum duty for that point. The EC maps
each point to a temperature decade [30 + 10 * i, 39 + 10 * i] degrees
Celsius, stepping between points with 5 degrees Celsius of hysteresis
without interpolation on TUF series.
EC firmware dumps from an FA507 show floor tables of {0, 0, 0, 0, 25,
48, 58, 74}% for the CPU fan and {0, 0, 0, 0, 26, 57, 68, 91}% for the
GPU fan, verified via live DSTS reads on an FA507NV.
Read the tables at probe and refresh them after power profile changes
or curve resets, as the EC can reload defaults when operating modes
change. Profile switches did not alter table bytes on the FA507, so
this refresh is precautionary.
Export the floors without altering user curves, rejecting replies
shorter than eight bytes or containing values exceeding 100%. If a
table contains decreasing values along its points, warn once but still
export it so userspace can inspect what firmware enforces. When
firmware returns 0xFFFFFFFE, mark the table unsupported for that fan
to avoid repeating failed WMI calls on future refreshes; unreadable
tables remain zeroed.
Protect the cached floor tables with a mutex against concurrent sysfs
reads.
Also fix the stale comment claiming that firmware interpolates between
curve points.
Signed-off-by: Bartu Alev <bartualev@gmail.com>
---
drivers/platform/x86/asus-wmi.c | 170 ++++++++++++++++++++++++++++++++++++++--
1 file changed, 165 insertions(+), 5 deletions(-)
diff --git a/drivers/platform/x86/asus-wmi.c b/drivers/platform/x86/asus-wmi.c
index c55f469cb03dc..b6541183d4568 100644
--- a/drivers/platform/x86/asus-wmi.c
+++ b/drivers/platform/x86/asus-wmi.c
@@ -239,6 +239,10 @@ struct fan_curve_data {
u32 device_id;
u8 temps[FAN_CURVE_POINTS];
u8 percents[FAN_CURVE_POINTS];
+ /* EC minimum duty per point, pwm 0-255, not percent */
+ u8 floors[FAN_CURVE_POINTS];
+ bool floors_valid;
+ bool floors_unsupported;
};
struct asus_wmi {
@@ -320,6 +324,9 @@ struct asus_wmi {
bool gpu_fan_curve_available;
bool mid_fan_curve_available;
struct fan_curve_data custom_fan_curves[3];
+ /* protects custom_fan_curves[].floors */
+ struct mutex fan_curve_lock;
+ struct device *fan_curve_hwmon;
struct device *ppdev;
@@ -466,7 +473,8 @@ static int asus_wmi_evaluate_method5(u32 method_id,
* means that the method called is unsupported.
*/
static int asus_wmi_evaluate_method_buf(u32 method_id,
- u32 arg0, u32 arg1, u8 *ret_buffer, size_t size)
+ u32 arg0, u32 arg1, u8 *ret_buffer, size_t size,
+ size_t *ret_len)
{
struct bios_args args = {
.arg0 = arg0,
@@ -479,6 +487,9 @@ static int asus_wmi_evaluate_method_buf(u32 method_id,
union acpi_object *obj;
int err = 0;
+ if (ret_len)
+ *ret_len = 0;
+
status = asus_wmi_evaluate_method_locked(method_id, &input, &output);
pr_debug("%s called (0x%08x) with args: 0x%08x, 0x%08x\n",
@@ -503,6 +514,8 @@ static int asus_wmi_evaluate_method_buf(u32 method_id,
}
memcpy(ret_buffer, obj->buffer.pointer, obj->buffer.length);
+ if (ret_len)
+ *ret_len = obj->buffer.length;
break;
case ACPI_TYPE_INTEGER:
err = (u32)obj->integer.value;
@@ -3631,7 +3644,7 @@ static int fan_curve_get_factory_default(struct asus_wmi *asus, u32 fan_dev)
mode = 2;
err = asus_wmi_evaluate_method_buf(asus->dsts_id, fan_dev, mode, buf,
- FAN_CURVE_BUF_LEN);
+ FAN_CURVE_BUF_LEN, NULL);
if (err) {
pr_warn("%s (0x%08x) failed: %d\n", __func__, fan_dev, err);
return err;
@@ -3651,7 +3664,136 @@ static int fan_curve_get_factory_default(struct asus_wmi *asus, u32 fan_dev)
return 0;
}
-/* Check if capability exists, and populate defaults */
+/*
+ * Read the firmware fan floor table. The EC enforces a minimum duty per
+ * curve point on top of the custom curve. Reading is best effort: at
+ * probe time a failed read leaves the table zeroed and invalid, and a
+ * failed refresh keeps the last table that was read.
+ */
+static int fan_curve_load_floors(struct asus_wmi *asus, u32 fan_dev)
+{
+ struct fan_curve_data *curves;
+ u8 buf[FAN_CURVE_BUF_LEN] = {};
+ size_t len;
+ int err, fan_idx, i;
+
+ switch (fan_dev) {
+ case ASUS_WMI_DEVID_CPU_FAN_CURVE:
+ fan_idx = FAN_CURVE_DEV_CPU;
+ fan_dev = ASUS_WMI_DEVID_CPU_FAN_FLOOR;
+ break;
+ case ASUS_WMI_DEVID_GPU_FAN_CURVE:
+ fan_idx = FAN_CURVE_DEV_GPU;
+ fan_dev = ASUS_WMI_DEVID_GPU_FAN_FLOOR;
+ break;
+ case ASUS_WMI_DEVID_MID_FAN_CURVE:
+ fan_idx = FAN_CURVE_DEV_MID;
+ fan_dev = ASUS_WMI_DEVID_MID_FAN_FLOOR;
+ break;
+ default:
+ return -EOPNOTSUPP;
+ }
+
+ curves = &asus->custom_fan_curves[fan_idx];
+ if (curves->floors_unsupported)
+ return -ENODEV;
+
+ err = asus_wmi_evaluate_method_buf(asus->dsts_id, fan_dev, 0, buf,
+ FAN_CURVE_BUF_LEN, &len);
+ if (err) {
+ pr_debug("%s (0x%08x) failed: %d\n", __func__, fan_dev, err);
+ /* 0xFFFFFFFE means the firmware has no such method */
+ if (err == -ENODEV)
+ curves->floors_unsupported = true;
+ return err;
+ }
+
+ if (len < FAN_CURVE_POINTS) {
+ pr_debug("%s (0x%08x): short reply of %zu bytes\n",
+ __func__, fan_dev, len);
+ return -ENODATA;
+ }
+
+ /* A percentage above 100 would overflow the pwm scale below */
+ for (i = 0; i < FAN_CURVE_POINTS; i++) {
+ if (buf[i] > 100) {
+ pr_debug("%s (0x%08x): implausible floor table\n",
+ __func__, fan_dev);
+ return -EINVAL;
+ }
+ }
+
+ /*
+ * The floors observed so far do not decrease along the points, but
+ * that is only proven on the FA507. Do not reject a table that
+ * does: the values are still usable as per-point minimums, and
+ * hiding them would leave userspace no way to see what the
+ * firmware enforces.
+ */
+ for (i = 1; i < FAN_CURVE_POINTS; i++) {
+ if (buf[i] < buf[i - 1]) {
+ dev_warn_once(&asus->platform_device->dev,
+ "fan curve (0x%08x): floor table decreases at point %d\n",
+ fan_dev, i + 1);
+ break;
+ }
+ }
+
+ guard(mutex)(&asus->fan_curve_lock);
+ for (i = 0; i < FAN_CURVE_POINTS; i++)
+ curves->floors[i] = DIV_ROUND_UP(255 * buf[i], 100);
+ curves->floors_valid = true;
+
+ return 0;
+}
+
+static void fan_curve_notify_floors(struct asus_wmi *asus)
+{
+ struct device *hwmon = asus->fan_curve_hwmon;
+ bool valid[ARRAY_SIZE(asus->custom_fan_curves)];
+ char name[32];
+ int fan, point;
+
+ if (!hwmon)
+ return;
+
+ scoped_guard(mutex, &asus->fan_curve_lock) {
+ for (fan = 0; fan < ARRAY_SIZE(valid); fan++)
+ valid[fan] = asus->custom_fan_curves[fan].floors_valid;
+ }
+
+ for (fan = 0; fan < ARRAY_SIZE(valid); fan++) {
+ if (!valid[fan])
+ continue;
+
+ for (point = 0; point < FAN_CURVE_POINTS; point++) {
+ snprintf(name, sizeof(name),
+ "pwm%d_auto_point%d_floor_pwm",
+ fan + 1, point + 1);
+ sysfs_notify(&hwmon->kobj, NULL, name);
+ }
+ }
+}
+
+/*
+ * The EC re-runs its defaults reload when the mode byte it keys on
+ * changes, which a thermal profile switch can do. Re-read the tables
+ * so that the exported values follow the firmware; on the FA507 the
+ * tables are profile independent, so this is defensive.
+ */
+static void fan_curve_refresh_floors(struct asus_wmi *asus)
+{
+ if (asus->cpu_fan_curve_available)
+ fan_curve_load_floors(asus, ASUS_WMI_DEVID_CPU_FAN_CURVE);
+ if (asus->gpu_fan_curve_available)
+ fan_curve_load_floors(asus, ASUS_WMI_DEVID_GPU_FAN_CURVE);
+ if (asus->mid_fan_curve_available)
+ fan_curve_load_floors(asus, ASUS_WMI_DEVID_MID_FAN_CURVE);
+
+ fan_curve_notify_floors(asus);
+}
+
+/* Check if the capability exists, populate its defaults and read its floor table */
static int fan_curve_check_present(struct asus_wmi *asus, bool *available,
u32 fan_dev)
{
@@ -3668,6 +3810,16 @@ static int fan_curve_check_present(struct asus_wmi *asus, bool *available,
}
*available = true;
+
+ err = fan_curve_load_floors(asus, fan_dev);
+ if (err == -ENODEV)
+ dev_dbg(&asus->platform_device->dev,
+ "fan curve (0x%08x): no floor table\n", fan_dev);
+ else if (err)
+ dev_warn(&asus->platform_device->dev,
+ "fan curve (0x%08x): unusable floor table (%d)\n",
+ fan_dev, err);
+
return 0;
}
@@ -3738,8 +3890,9 @@ static int fan_curve_write(struct asus_wmi *asus,
}
/*
- * A fan curve is a set of points the firmware interpolates between, so it
- * only makes sense if neither temperature nor PWM ever decreases along it.
+ * A fan curve is a set of points the firmware steps through, without
+ * interpolating between them, so it only makes sense if neither temperature
+ * nor PWM ever decreases along it.
*/
static int fan_curve_validate(struct device *dev, struct fan_curve_data *data)
{
@@ -3870,6 +4023,7 @@ static ssize_t fan_curve_enable_store(struct device *dev,
err = asus_fan_set_auto(asus);
if (err)
return err;
+ fan_curve_refresh_floors(asus);
} else {
/* Safeguard against fautly ACPI tables */
err = fan_curve_get_factory_default(asus, data->device_id);
@@ -3878,6 +4032,7 @@ static ssize_t fan_curve_enable_store(struct device *dev,
err = fan_curve_write(asus, data);
if (err)
return err;
+ fan_curve_refresh_floors(asus);
}
}
return count;
@@ -4125,6 +4280,8 @@ static int asus_wmi_custom_fan_curve_init(struct asus_wmi *asus)
return PTR_ERR(hwmon);
}
+ asus->fan_curve_hwmon = hwmon;
+
return 0;
}
@@ -4195,6 +4352,8 @@ static int throttle_thermal_policy_write(struct asus_wmi *asus)
if (asus->mid_fan_curve_available)
asus->custom_fan_curves[FAN_CURVE_DEV_MID].enabled = false;
+ fan_curve_refresh_floors(asus);
+
return 0;
}
@@ -5202,6 +5361,7 @@ static int asus_wmi_add(struct platform_device *pdev)
asus->platform_device = pdev;
wdrv->platform_device = pdev;
platform_set_drvdata(asus->platform_device, asus);
+ mutex_init(&asus->fan_curve_lock);
if (wdrv->detect_quirks)
wdrv->detect_quirks(asus->driver);
--
2.56.0
^ permalink raw reply related [flat|nested] 15+ messages in thread* Re: [PATCH 4/7] platform/x86: asus-wmi: read the firmware fan floor tables
2026-11-01 10:46 ` [PATCH 4/7] platform/x86: asus-wmi: read the firmware fan floor tables Bartu Alev
@ 2026-11-01 11:02 ` sashiko-bot
0 siblings, 0 replies; 15+ messages in thread
From: sashiko-bot @ 2026-11-01 11:02 UTC (permalink / raw)
To: Bartu Alev; +Cc: linux-hwmon
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] platform/x86: asus-wmi: unhandled DSTS return in fan_curve_load_floors()
--
commit 2c4e7fb8cca0f513f1d86b6eff6061e09cba0940
Author: Bartu Alev <bartualev@gmail.com>
platform/x86: asus-wmi: read the firmware fan floor tables
This commit reads the firmware fan floor tables via DSTS methods at probe
and refreshes them when power profiles change or custom fan curves are
reset. The cached floor values are validated and protected by a mutex for
sysfs access.
> diff --git a/drivers/platform/x86/asus-wmi.c b/drivers/platform/x86/asus-wmi.c
> index c55f469cb03dc..b6541183d4568 100644
> --- a/drivers/platform/x86/asus-wmi.c
> +++ b/drivers/platform/x86/asus-wmi.c
[ ... ]
> @@ -3651,7 +3664,136 @@ static int fan_curve_get_factory_default(struct asus_wmi *asus, u32 fan_dev)
> return 0;
> }
>
> -/* Check if capability exists, and populate defaults */
> +/*
> + * Read the firmware fan floor table. The EC enforces a minimum duty per
> + * curve point on top of the custom curve. Reading is best effort: at
> + * probe time a failed read leaves the table zeroed and invalid, and a
> + * failed refresh keeps the last table that was read.
> + */
> +static int fan_curve_load_floors(struct asus_wmi *asus, u32 fan_dev)
> +{
> + struct fan_curve_data *curves;
> + u8 buf[FAN_CURVE_BUF_LEN] = {};
> + size_t len;
> + int err, fan_idx, i;
> +
> + switch (fan_dev) {
> + case ASUS_WMI_DEVID_CPU_FAN_CURVE:
> + fan_idx = FAN_CURVE_DEV_CPU;
> + fan_dev = ASUS_WMI_DEVID_CPU_FAN_FLOOR;
> + break;
> + case ASUS_WMI_DEVID_GPU_FAN_CURVE:
> + fan_idx = FAN_CURVE_DEV_GPU;
> + fan_dev = ASUS_WMI_DEVID_GPU_FAN_FLOOR;
> + break;
> + case ASUS_WMI_DEVID_MID_FAN_CURVE:
> + fan_idx = FAN_CURVE_DEV_MID;
> + fan_dev = ASUS_WMI_DEVID_MID_FAN_FLOOR;
> + break;
> + default:
> + return -EOPNOTSUPP;
> + }
> +
> + curves = &asus->custom_fan_curves[fan_idx];
> + if (curves->floors_unsupported)
> + return -ENODEV;
> +
> + err = asus_wmi_evaluate_method_buf(asus->dsts_id, fan_dev, 0, buf,
> + FAN_CURVE_BUF_LEN, &len);
> + if (err) {
> + pr_debug("%s (0x%08x) failed: %d\n", __func__, fan_dev, err);
> + /* 0xFFFFFFFE means the firmware has no such method */
> + if (err == -ENODEV)
> + curves->floors_unsupported = true;
> + return err;
> + }
[ ... ]
> @@ -3668,6 +3810,16 @@ static int fan_curve_check_present(struct asus_wmi *asus, bool *available,
> }
>
> *available = true;
> +
> + err = fan_curve_load_floors(asus, fan_dev);
> + if (err == -ENODEV)
> + dev_dbg(&asus->platform_device->dev,
> + "fan curve (0x%08x): no floor table\n", fan_dev);
> + else if (err)
> + dev_warn(&asus->platform_device->dev,
> + "fan curve (0x%08x): unusable floor table (%d)\n",
> + fan_dev, err);
> +
> return 0;
> }
[Severity: Medium]
What happens in asus_wmi_evaluate_method_buf() and fan_curve_load_floors()
on laptops with custom fan curves whose firmware does not return
ASUS_WMI_UNSUPPORTED_METHOD (0xFFFFFFFE) for the new floor device IDs?
First, in drivers/platform/x86/asus-wmi.c, asus_wmi_evaluate_method_buf()
does not check whether output.pointer is NULL before dereferencing
obj->type:
drivers/platform/x86/asus-wmi.c:asus_wmi_evaluate_method_buf() {
...
status = asus_wmi_evaluate_method_locked(method_id, &input, &output);
if (ACPI_FAILURE(status))
return -EIO;
obj = (union acpi_object *)output.pointer;
switch (obj->type) {
...
}
Unlike asus_wmi_evaluate_method3(), asus_wmi_evaluate_method5(),
show_dsts(), and show_call() in the same file (which all check if (!obj)),
if firmware returns AE_OK with no return object for an unknown method ID
(as noted in asus_wmi_platform_init() for some TUF series laptops), could
calling asus_wmi_evaluate_method_buf() via
asus_wmi_add() -> asus_wmi_custom_fan_curve_init() ->
fan_curve_check_present() -> fan_curve_load_floors() trigger a NULL pointer
dereference?
Second, when DSTS evaluation for an unimplemented floor method returns an
ACPI_TYPE_INTEGER other than 0xFFFFFFFE, asus_wmi_evaluate_method_buf()
only translates ASUS_WMI_UNSUPPORTED_METHOD to -ENODEV:
case ACPI_TYPE_INTEGER:
err = (u32)obj->integer.value;
if (err == ASUS_WMI_UNSUPPORTED_METHOD)
err = -ENODEV;
/*
* At least one method returns a 0 with no buffer if no arg
* is provided, such as ASUS_WMI_DEVID_CPU_FAN_CURVE
*/
if (err == 0)
err = -ENODATA;
break;
On firmwares that return 0 (mapped to -ENODATA), 0xFFFFFFFF (which
asus_wmi_get_devstate() and show_dsts() treat as -ENODEV, but here becomes
-1), or ASUS_WMI_DSTS_UNKNOWN_BIT (0x2, returned as +2) for unsupported
DSTS device IDs, fan_curve_load_floors() will not set
curves->floors_unsupported = true, and fan_curve_check_present() will log a
false dev_warn() at boot and continue evaluating the unsupported WMI
methods on every fan_curve_refresh_floors() call.
Should asus_wmi_evaluate_method_buf() guard against !obj, and should
integer returns from DSTS in fan_curve_load_floors() also be treated as
unsupported?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261101-asus-fan-floors-v4-v1-0-b8d1c31a99a9@gmail.com?part=4
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH 5/7] platform/x86: asus-wmi: reject short fan curve replies
2026-11-01 10:46 [PATCH 0/7] platform/x86: asus-wmi: expose the firmware fan floor tables Bartu Alev
` (3 preceding siblings ...)
2026-11-01 10:46 ` [PATCH 4/7] platform/x86: asus-wmi: read the firmware fan floor tables Bartu Alev
@ 2026-11-01 10:46 ` Bartu Alev
2026-11-01 10:55 ` sashiko-bot
2026-11-01 10:46 ` [PATCH 6/7] platform/x86: asus-wmi: expose the fan floor tables via hwmon Bartu Alev
2026-11-01 10:46 ` [PATCH 7/7] platform/x86: asus-wmi: show the effective duty for enabled curves Bartu Alev
6 siblings, 1 reply; 15+ messages in thread
From: Bartu Alev @ 2026-11-01 10:46 UTC (permalink / raw)
To: Corentin Chary, Luke D. Jones, Denis Benato, Hans de Goede,
Ilpo Järvinen
Cc: platform-driver-x86, linux-kernel, linux-hwmon, Guenter Roeck,
linux-api, Bartu Alev
fan_curve_get_factory_default() passes a 32-byte stack buffer to
asus_wmi_evaluate_method_buf() and copies the first 16 bytes into the
curve without checking the returned length. Because the helper only
copies obj->buffer.length bytes, a shorter firmware reply leaves the
tail of the buffer uninitialized, populating the curve with stack
garbage.
Zero-initialize the buffer and reject replies shorter than the 16
bytes required for the curve. Known curve getters return 32 bytes,
well above this threshold.
Signed-off-by: Bartu Alev <bartualev@gmail.com>
---
drivers/platform/x86/asus-wmi.c | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/drivers/platform/x86/asus-wmi.c b/drivers/platform/x86/asus-wmi.c
index b6541183d4568..dc1e69b964c99 100644
--- a/drivers/platform/x86/asus-wmi.c
+++ b/drivers/platform/x86/asus-wmi.c
@@ -3631,7 +3631,8 @@ static void fan_curve_copy_from_buf(struct fan_curve_data *data, u8 *buf)
static int fan_curve_get_factory_default(struct asus_wmi *asus, u32 fan_dev)
{
struct fan_curve_data *curves;
- u8 buf[FAN_CURVE_BUF_LEN];
+ u8 buf[FAN_CURVE_BUF_LEN] = {};
+ size_t len;
int err, fan_idx;
u8 mode = 0;
@@ -3644,12 +3645,18 @@ static int fan_curve_get_factory_default(struct asus_wmi *asus, u32 fan_dev)
mode = 2;
err = asus_wmi_evaluate_method_buf(asus->dsts_id, fan_dev, mode, buf,
- FAN_CURVE_BUF_LEN, NULL);
+ FAN_CURVE_BUF_LEN, &len);
if (err) {
pr_warn("%s (0x%08x) failed: %d\n", __func__, fan_dev, err);
return err;
}
+ if (len < 2 * FAN_CURVE_POINTS) {
+ pr_debug("%s (0x%08x): short reply of %zu bytes\n",
+ __func__, fan_dev, len);
+ return -ENODATA;
+ }
+
fan_idx = FAN_CURVE_DEV_CPU;
if (fan_dev == ASUS_WMI_DEVID_GPU_FAN_CURVE)
fan_idx = FAN_CURVE_DEV_GPU;
--
2.56.0
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH 6/7] platform/x86: asus-wmi: expose the fan floor tables via hwmon
2026-11-01 10:46 [PATCH 0/7] platform/x86: asus-wmi: expose the firmware fan floor tables Bartu Alev
` (4 preceding siblings ...)
2026-11-01 10:46 ` [PATCH 5/7] platform/x86: asus-wmi: reject short fan curve replies Bartu Alev
@ 2026-11-01 10:46 ` Bartu Alev
2026-11-01 10:52 ` sashiko-bot
2026-11-01 10:46 ` [PATCH 7/7] platform/x86: asus-wmi: show the effective duty for enabled curves Bartu Alev
6 siblings, 1 reply; 15+ messages in thread
From: Bartu Alev @ 2026-11-01 10:46 UTC (permalink / raw)
To: Corentin Chary, Luke D. Jones, Denis Benato, Hans de Goede,
Ilpo Järvinen
Cc: platform-driver-x86, linux-kernel, linux-hwmon, Guenter Roeck,
linux-api, Bartu Alev
Firmware enforces minimum duties on fan curves without updating the
reported curve buffers, leaving userspace unable to determine which
duty ranges are unreachable.
Export read-only pwm[1-3]_auto_point[1-8]_floor_pwm attributes on the
hwmon device. Each file returns the minimum duty (0-255 PWM) enforced
for that curve point. The attributes are registered for every
supported fan curve and return -ENODATA when firmware provides no
floor table.
Standard hwmon curve attributes (pwmY_auto_pointZ_pwm) assume
writable points and cannot represent a separate, hardware enforced
floor. Dedicated read-only floor attributes provide these values
while adhering to the one value per file rule.
Document these attributes in
Documentation/ABI/testing/sysfs-platform-asus-wmi.
Signed-off-by: Bartu Alev <bartualev@gmail.com>
---
Documentation/ABI/testing/sysfs-platform-asus-wmi | 23 ++++
drivers/platform/x86/asus-wmi.c | 141 +++++++++++++++++++++-
2 files changed, 163 insertions(+), 1 deletion(-)
diff --git a/Documentation/ABI/testing/sysfs-platform-asus-wmi b/Documentation/ABI/testing/sysfs-platform-asus-wmi
index 775ea88625c0d..24931cc97ca86 100644
--- a/Documentation/ABI/testing/sysfs-platform-asus-wmi
+++ b/Documentation/ABI/testing/sysfs-platform-asus-wmi
@@ -297,3 +297,26 @@ Contact: Luke D. Jones <luke@ljones.dev>, Denis Benato <denis.benato@linux.dev>,
Description:
Same as the pwm1 and pwm2 curve points, for the mid fan
(pwm3) of laptops whose firmware exposes a third fan curve.
+
+What: /sys/class/hwmon/hwmonX/pwm[1-3]_auto_point[1-8]_floor_pwm
+Date: Nov 2026
+KernelVersion: 7.5
+Contact: Bartu Alev <bartualev@gmail.com>
+Description:
+ Read-only minimum duty, in pwm units (0-255), that the
+ embedded controller enforces for curve point Z of fan Y.
+ The firmware reports the floor as a percentage; the driver
+ converts it with DIV_ROUND_UP(255 * percent, 100), which maps
+ every 0-100 percentage back to itself.
+ Point Z (Z = 1..8) is bound to the temperature decade
+ [30 + 10 * (Z - 1), 39 + 10 * (Z - 1)] degrees Celsius and
+ the value is the floor of that decade. The controller applies
+ max(curve, floor) for every point on each thermal tick, so
+ the floor is a hardware limit on top of the curve written
+ through pwmY_auto_pointZ_pwm.
+
+ The attribute exists for every fan with a custom fan curve.
+ Reading it returns -ENODATA when the firmware of that fan
+ does not provide a floor table. A zero value is a real
+ floor: the controller then enforces no minimum duty for that
+ point.
diff --git a/drivers/platform/x86/asus-wmi.c b/drivers/platform/x86/asus-wmi.c
index dc1e69b964c99..74476904cb99c 100644
--- a/drivers/platform/x86/asus-wmi.c
+++ b/drivers/platform/x86/asus-wmi.c
@@ -3868,6 +3868,23 @@ static ssize_t fan_curve_show(struct device *dev,
return sysfs_emit(buf, "%d\n", value);
}
+static ssize_t fan_curve_floor_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct sensor_device_attribute_2 *dev_attr = to_sensor_dev_attr_2(attr);
+ struct asus_wmi *asus = dev_get_drvdata(dev);
+ struct fan_curve_data *data = &asus->custom_fan_curves[dev_attr->nr];
+ u8 floor;
+
+ guard(mutex)(&asus->fan_curve_lock);
+ if (!data->floors_valid)
+ return -ENODATA;
+
+ floor = data->floors[dev_attr->index];
+
+ return sysfs_emit(buf, "%u\n", floor);
+}
+
/*
* "fan_dev" is the related WMI method such as ASUS_WMI_DEVID_CPU_FAN_CURVE.
*/
@@ -4153,6 +4170,58 @@ static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point7_pwm, fan_curve,
static SENSOR_DEVICE_ATTR_2_RW(pwm3_auto_point8_pwm, fan_curve,
FAN_CURVE_DEV_MID | FAN_CURVE_PWM_MASK, 7);
+/* Firmware enforced minimum duty of each point, read-only */
+static SENSOR_DEVICE_ATTR_2_RO(pwm1_auto_point1_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_CPU, 0);
+static SENSOR_DEVICE_ATTR_2_RO(pwm1_auto_point2_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_CPU, 1);
+static SENSOR_DEVICE_ATTR_2_RO(pwm1_auto_point3_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_CPU, 2);
+static SENSOR_DEVICE_ATTR_2_RO(pwm1_auto_point4_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_CPU, 3);
+static SENSOR_DEVICE_ATTR_2_RO(pwm1_auto_point5_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_CPU, 4);
+static SENSOR_DEVICE_ATTR_2_RO(pwm1_auto_point6_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_CPU, 5);
+static SENSOR_DEVICE_ATTR_2_RO(pwm1_auto_point7_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_CPU, 6);
+static SENSOR_DEVICE_ATTR_2_RO(pwm1_auto_point8_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_CPU, 7);
+
+static SENSOR_DEVICE_ATTR_2_RO(pwm2_auto_point1_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_GPU, 0);
+static SENSOR_DEVICE_ATTR_2_RO(pwm2_auto_point2_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_GPU, 1);
+static SENSOR_DEVICE_ATTR_2_RO(pwm2_auto_point3_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_GPU, 2);
+static SENSOR_DEVICE_ATTR_2_RO(pwm2_auto_point4_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_GPU, 3);
+static SENSOR_DEVICE_ATTR_2_RO(pwm2_auto_point5_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_GPU, 4);
+static SENSOR_DEVICE_ATTR_2_RO(pwm2_auto_point6_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_GPU, 5);
+static SENSOR_DEVICE_ATTR_2_RO(pwm2_auto_point7_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_GPU, 6);
+static SENSOR_DEVICE_ATTR_2_RO(pwm2_auto_point8_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_GPU, 7);
+
+static SENSOR_DEVICE_ATTR_2_RO(pwm3_auto_point1_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_MID, 0);
+static SENSOR_DEVICE_ATTR_2_RO(pwm3_auto_point2_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_MID, 1);
+static SENSOR_DEVICE_ATTR_2_RO(pwm3_auto_point3_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_MID, 2);
+static SENSOR_DEVICE_ATTR_2_RO(pwm3_auto_point4_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_MID, 3);
+static SENSOR_DEVICE_ATTR_2_RO(pwm3_auto_point5_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_MID, 4);
+static SENSOR_DEVICE_ATTR_2_RO(pwm3_auto_point6_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_MID, 5);
+static SENSOR_DEVICE_ATTR_2_RO(pwm3_auto_point7_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_MID, 6);
+static SENSOR_DEVICE_ATTR_2_RO(pwm3_auto_point8_floor_pwm, fan_curve_floor,
+ FAN_CURVE_DEV_MID, 7);
+
static struct attribute *asus_fan_curve_attr[] = {
/* CPU */
&sensor_dev_attr_pwm1_enable.dev_attr.attr,
@@ -4211,6 +4280,37 @@ static struct attribute *asus_fan_curve_attr[] = {
NULL
};
+static struct attribute *asus_fan_curve_floor_attr[] = {
+ /* CPU */
+ &sensor_dev_attr_pwm1_auto_point1_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm1_auto_point2_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm1_auto_point3_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm1_auto_point4_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm1_auto_point5_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm1_auto_point6_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm1_auto_point7_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm1_auto_point8_floor_pwm.dev_attr.attr,
+ /* GPU */
+ &sensor_dev_attr_pwm2_auto_point1_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm2_auto_point2_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm2_auto_point3_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm2_auto_point4_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm2_auto_point5_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm2_auto_point6_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm2_auto_point7_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm2_auto_point8_floor_pwm.dev_attr.attr,
+ /* MID */
+ &sensor_dev_attr_pwm3_auto_point1_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm3_auto_point2_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm3_auto_point3_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm3_auto_point4_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm3_auto_point5_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm3_auto_point6_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm3_auto_point7_floor_pwm.dev_attr.attr,
+ &sensor_dev_attr_pwm3_auto_point8_floor_pwm.dev_attr.attr,
+ NULL
+};
+
static umode_t asus_fan_curve_is_visible(struct kobject *kobj,
struct attribute *attr, int idx)
{
@@ -4233,11 +4333,50 @@ static umode_t asus_fan_curve_is_visible(struct kobject *kobj,
return 0;
}
+static bool fan_curve_available(struct asus_wmi *asus, int fan_idx)
+{
+ switch (fan_idx) {
+ case FAN_CURVE_DEV_CPU:
+ return asus->cpu_fan_curve_available;
+ case FAN_CURVE_DEV_GPU:
+ return asus->gpu_fan_curve_available;
+ case FAN_CURVE_DEV_MID:
+ return asus->mid_fan_curve_available;
+ default:
+ return false;
+ }
+}
+
+static umode_t asus_fan_curve_floor_is_visible(struct kobject *kobj,
+ struct attribute *attr, int idx)
+{
+ struct device *dev = kobj_to_dev(kobj);
+ struct asus_wmi *asus = dev_get_drvdata(dev->parent);
+ struct device_attribute *dattr =
+ container_of(attr, struct device_attribute, attr);
+ int fan = to_sensor_dev_attr_2(dattr)->nr;
+
+ if (!fan_curve_available(asus, fan))
+ return 0;
+
+ return 0444;
+}
+
static const struct attribute_group asus_fan_curve_attr_group = {
.is_visible = asus_fan_curve_is_visible,
.attrs = asus_fan_curve_attr,
};
-__ATTRIBUTE_GROUPS(asus_fan_curve_attr);
+
+static const struct attribute_group asus_fan_curve_floor_attr_group = {
+ .is_visible = asus_fan_curve_floor_is_visible,
+ .attrs = asus_fan_curve_floor_attr,
+};
+
+static const struct attribute_group *asus_fan_curve_attr_groups[] = {
+ &asus_fan_curve_attr_group,
+ &asus_fan_curve_floor_attr_group,
+ NULL,
+};
/*
* Must be initialised after throttle_thermal_policy_dev is set as
--
2.56.0
^ permalink raw reply related [flat|nested] 15+ messages in thread* Re: [PATCH 6/7] platform/x86: asus-wmi: expose the fan floor tables via hwmon
2026-11-01 10:46 ` [PATCH 6/7] platform/x86: asus-wmi: expose the fan floor tables via hwmon Bartu Alev
@ 2026-11-01 10:52 ` sashiko-bot
0 siblings, 0 replies; 15+ messages in thread
From: sashiko-bot @ 2026-11-01 10:52 UTC (permalink / raw)
To: Bartu Alev; +Cc: linux-hwmon
> Firmware enforces minimum duties on fan curves without updating the
> reported curve buffers, leaving userspace unable to determine which
> duty ranges are unreachable.
>
> Export read-only pwm[1-3]_auto_point[1-8]_floor_pwm attributes on the
> hwmon device. Each file returns the minimum duty (0-255 PWM) enforced
> for that curve point. The attributes are registered for every
> supported fan curve and return -ENODATA when firmware provides no
> floor table.
>
> Standard hwmon curve attributes (pwmY_auto_pointZ_pwm) assume
> writable points and cannot represent a separate, hardware enforced
> floor. Dedicated read-only floor attributes provide these values
> while adhering to the one value per file rule.
>
> Document these attributes in
> Documentation/ABI/testing/sysfs-platform-asus-wmi.
>
> Signed-off-by: Bartu Alev <bartualev@gmail.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261101-asus-fan-floors-v4-v1-0-b8d1c31a99a9@gmail.com?part=6
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH 7/7] platform/x86: asus-wmi: show the effective duty for enabled curves
2026-11-01 10:46 [PATCH 0/7] platform/x86: asus-wmi: expose the firmware fan floor tables Bartu Alev
` (5 preceding siblings ...)
2026-11-01 10:46 ` [PATCH 6/7] platform/x86: asus-wmi: expose the fan floor tables via hwmon Bartu Alev
@ 2026-11-01 10:46 ` Bartu Alev
2026-11-01 11:00 ` sashiko-bot
6 siblings, 1 reply; 15+ messages in thread
From: Bartu Alev @ 2026-11-01 10:46 UTC (permalink / raw)
To: Corentin Chary, Luke D. Jones, Denis Benato, Hans de Goede,
Ilpo Järvinen
Cc: platform-driver-x86, linux-kernel, linux-hwmon, Guenter Roeck,
linux-api, Bartu Alev
When a custom fan curve is enabled, the embedded controller executes
each point at max(written_duty, floor_duty). However,
pwmY_auto_pointZ_pwm reports only the stored value, diverging from
the duty applied by hardware.
Return max(stored, floor) from pwmY_auto_pointZ_pwm when the curve is
enabled and floor data is valid. When disabled, continue returning
the stored value.
Because the controller operates in integer percentages while sysfs
uses PWM units (0-255), the reported value is an upper bound of the
applied duty. Consequently, reading an enabled curve is not
read/modify/write safe: writing back a read value commits the
firmware floor into the stored curve.
Trigger sysfs notifications on pwmY_auto_pointZ_pwm on enable and
disable transitions, as well as floor refreshes, so pollers detect
changes in effective duty.
Signed-off-by: Bartu Alev <bartualev@gmail.com>
---
Documentation/ABI/testing/sysfs-platform-asus-wmi | 14 +++++++++++
drivers/platform/x86/asus-wmi.c | 30 +++++++++++++++++++----
2 files changed, 39 insertions(+), 5 deletions(-)
diff --git a/Documentation/ABI/testing/sysfs-platform-asus-wmi b/Documentation/ABI/testing/sysfs-platform-asus-wmi
index 24931cc97ca86..36b633a5dfc49 100644
--- a/Documentation/ABI/testing/sysfs-platform-asus-wmi
+++ b/Documentation/ABI/testing/sysfs-platform-asus-wmi
@@ -281,6 +281,20 @@ Description:
(0-255); the driver scales the duty to the percentage the
firmware expects when the curve is enabled.
+ While the curve is enabled, _pwm reports the effective duty,
+ which is the higher of the stored duty and the firmware floor
+ of the point (pwmY_auto_pointZ_floor_pwm). A disabled curve
+ reports the stored duty. The controller works in whole
+ percent, so the reported value is an upper bound: the applied
+ duty can be up to 2.5 pwm units lower.
+
+ Reading while the curve is enabled is therefore not
+ read-modify-write safe: a curve editor that reads the points
+ and writes them back stores the effective duty as the new
+ curve, and the duty the user originally wrote is lost. Read
+ the curve while it is disabled, or write back the values that
+ were written rather than the ones that were read.
+
The embedded controller binds point Z (Z = 1..8) to the
temperature decade [30 + 10 * (Z - 1), 39 + 10 * (Z - 1)]
degrees Celsius and silently clamps the threshold into that
diff --git a/drivers/platform/x86/asus-wmi.c b/drivers/platform/x86/asus-wmi.c
index 74476904cb99c..bae5a528ac5d8 100644
--- a/drivers/platform/x86/asus-wmi.c
+++ b/drivers/platform/x86/asus-wmi.c
@@ -324,7 +324,7 @@ struct asus_wmi {
bool gpu_fan_curve_available;
bool mid_fan_curve_available;
struct fan_curve_data custom_fan_curves[3];
- /* protects custom_fan_curves[].floors */
+ /* protects custom_fan_curves[].floors and .floors_valid */
struct mutex fan_curve_lock;
struct device *fan_curve_hwmon;
@@ -3754,7 +3754,7 @@ static int fan_curve_load_floors(struct asus_wmi *asus, u32 fan_dev)
return 0;
}
-static void fan_curve_notify_floors(struct asus_wmi *asus)
+static void fan_curve_notify(struct asus_wmi *asus)
{
struct device *hwmon = asus->fan_curve_hwmon;
bool valid[ARRAY_SIZE(asus->custom_fan_curves)];
@@ -3778,6 +3778,10 @@ static void fan_curve_notify_floors(struct asus_wmi *asus)
"pwm%d_auto_point%d_floor_pwm",
fan + 1, point + 1);
sysfs_notify(&hwmon->kobj, NULL, name);
+ snprintf(name, sizeof(name),
+ "pwm%d_auto_point%d_pwm",
+ fan + 1, point + 1);
+ sysfs_notify(&hwmon->kobj, NULL, name);
}
}
}
@@ -3797,7 +3801,7 @@ static void fan_curve_refresh_floors(struct asus_wmi *asus)
if (asus->mid_fan_curve_available)
fan_curve_load_floors(asus, ASUS_WMI_DEVID_MID_FAN_CURVE);
- fan_curve_notify_floors(asus);
+ fan_curve_notify(asus);
}
/* Check if the capability exists, populate its defaults and read its floor table */
@@ -3855,15 +3859,28 @@ static ssize_t fan_curve_show(struct device *dev,
struct asus_wmi *asus = dev_get_drvdata(dev);
struct fan_curve_data *data;
int value, pwm, index;
+ bool enabled;
data = fan_curve_attr_2_select(asus, attr);
pwm = dev_attr->nr & FAN_CURVE_PWM_MASK;
index = dev_attr->index;
- if (pwm)
+ if (pwm) {
+ enabled = data->enabled;
value = data->percents[index];
- else
+
+ /*
+ * The EC runs the point at the higher of the curve and the
+ * firmware floor, so report the duty it applies. Only the
+ * floor table is protected by fan_curve_lock; enabled and
+ * percents are written without it, so this is a snapshot.
+ */
+ guard(mutex)(&asus->fan_curve_lock);
+ if (enabled && data->floors_valid)
+ value = max(value, (int)data->floors[index]);
+ } else {
value = data->temps[index];
+ }
return sysfs_emit(buf, "%d\n", value);
}
@@ -4059,6 +4076,9 @@ static ssize_t fan_curve_enable_store(struct device *dev,
fan_curve_refresh_floors(asus);
}
}
+
+ fan_curve_notify(asus);
+
return count;
}
--
2.56.0
^ permalink raw reply related [flat|nested] 15+ messages in thread* Re: [PATCH 7/7] platform/x86: asus-wmi: show the effective duty for enabled curves
2026-11-01 10:46 ` [PATCH 7/7] platform/x86: asus-wmi: show the effective duty for enabled curves Bartu Alev
@ 2026-11-01 11:00 ` sashiko-bot
0 siblings, 0 replies; 15+ messages in thread
From: sashiko-bot @ 2026-11-01 11:00 UTC (permalink / raw)
To: Bartu Alev; +Cc: linux-hwmon
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] platform/x86: asus-wmi: missing fan_curve_notify() on curve disable paths
- [Low] platform/x86: asus-wmi: duplicate fan_curve_notify() on curve disable
--
commit d8679565f78ed87d40eb465628808366a670ad22
Author: Bartu Alev <bartualev@gmail.com>
platform/x86: asus-wmi: show the effective duty for enabled curves
This commit changes fan_curve_show() to return the maximum of the stored duty
and the firmware floor duty for pwmY_auto_pointZ_pwm when a custom fan curve
is enabled, and adds sysfs notifications for pwmY_auto_pointZ_pwm on curve
enable, disable, and floor refresh transitions.
> diff --git a/drivers/platform/x86/asus-wmi.c b/drivers/platform/x86/asus-wmi.c
> index 74476904cb99c..bae5a528ac5d8 100644
> --- a/drivers/platform/x86/asus-wmi.c
> +++ b/drivers/platform/x86/asus-wmi.c
[ ... ]
> @@ -3754,7 +3754,7 @@ static int fan_curve_load_floors(struct asus_wmi *asus, u32 fan_dev)
> return 0;
> }
>
> -static void fan_curve_notify_floors(struct asus_wmi *asus)
> +static void fan_curve_notify(struct asus_wmi *asus)
> {
> struct device *hwmon = asus->fan_curve_hwmon;
> bool valid[ARRAY_SIZE(asus->custom_fan_curves)];
> @@ -3778,6 +3778,10 @@ static void fan_curve_notify_floors(struct asus_wmi *asus)
> "pwm%d_auto_point%d_floor_pwm",
> fan + 1, point + 1);
> sysfs_notify(&hwmon->kobj, NULL, name);
> + snprintf(name, sizeof(name),
> + "pwm%d_auto_point%d_pwm",
> + fan + 1, point + 1);
> + sysfs_notify(&hwmon->kobj, NULL, name);
> }
> }
> }
> @@ -3797,7 +3801,7 @@ static void fan_curve_refresh_floors(struct asus_wmi *asus)
> if (asus->mid_fan_curve_available)
> fan_curve_load_floors(asus, ASUS_WMI_DEVID_MID_FAN_CURVE);
>
> - fan_curve_notify_floors(asus);
> + fan_curve_notify(asus);
> }
>
> /* Check if the capability exists, populate its defaults and read its floor table */
> @@ -3855,15 +3859,28 @@ static ssize_t fan_curve_show(struct device *dev,
> struct asus_wmi *asus = dev_get_drvdata(dev);
> struct fan_curve_data *data;
> int value, pwm, index;
> + bool enabled;
>
> data = fan_curve_attr_2_select(asus, attr);
> pwm = dev_attr->nr & FAN_CURVE_PWM_MASK;
> index = dev_attr->index;
>
> - if (pwm)
> + if (pwm) {
> + enabled = data->enabled;
> value = data->percents[index];
> - else
> +
> + /*
> + * The EC runs the point at the higher of the curve and the
> + * firmware floor, so report the duty it applies. Only the
> + * floor table is protected by fan_curve_lock; enabled and
> + * percents are written without it, so this is a snapshot.
> + */
> + guard(mutex)(&asus->fan_curve_lock);
> + if (enabled && data->floors_valid)
> + value = max(value, (int)data->floors[index]);
> + } else {
> value = data->temps[index];
> + }
>
> return sysfs_emit(buf, "%d\n", value);
> }
[Severity: Medium]
Now that fan_curve_show() returns either the floor-clamped duty or the raw
stored duty depending on data->enabled, are sysfs notifications missing on
other paths that clear data->enabled?
Writing to any point via fan_curve_store() disables the curve without calling
fan_curve_notify():
drivers/platform/x86/asus-wmi.c:fan_curve_store() {
...
if (pwm)
data->percents[index] = value;
else
data->temps[index] = value;
data->enabled = false;
return count;
}
Toggling the main hwmon fan mode in pwm1_enable_store() also disables all
custom fan curves without calling fan_curve_notify():
drivers/platform/x86/asus-wmi.c:pwm1_enable_store() {
...
/* Must set to disabled if mode is toggled */
if (asus->cpu_fan_curve_available)
asus->custom_fan_curves[FAN_CURVE_DEV_CPU].enabled = false;
if (asus->gpu_fan_curve_available)
asus->custom_fan_curves[FAN_CURVE_DEV_GPU].enabled = false;
if (asus->mid_fan_curve_available)
asus->custom_fan_curves[FAN_CURVE_DEV_MID].enabled = false;
return count;
}
Similarly, in fan_curve_enable_store(), if data->enabled was true and gets
set to false before an early error return (such as in fan_curve_validate() or
the reset branches), fan_curve_notify() at the end of the function is
bypassed.
Should these paths also notify pollers when an enabled curve transitions to
disabled so they do not keep stale floor-clamped values?
> @@ -4059,6 +4076,9 @@ static ssize_t fan_curve_enable_store(struct device *dev,
> fan_curve_refresh_floors(asus);
> }
> }
> +
> + fan_curve_notify(asus);
> +
> return count;
> }
[Severity: Low]
Does calling fan_curve_notify(asus) unconditionally at the end of
fan_curve_enable_store() emit duplicate notifications whenever a curve is
disabled?
When data->enabled is false, all three branches of the else block already
invoke fan_curve_refresh_floors(asus) (either directly or through
throttle_thermal_policy_write(asus) -> fan_curve_refresh_floors(asus)), which
itself calls fan_curve_notify(asus):
drivers/platform/x86/asus-wmi.c:fan_curve_enable_store() {
...
if (data->enabled) {
...
} else {
if (asus->throttle_thermal_policy_dev) {
err = throttle_thermal_policy_write(asus);
if (err)
return err;
} else if (asus->fan_type == FAN_TYPE_SPEC83) {
err = asus_fan_set_auto(asus);
if (err)
return err;
fan_curve_refresh_floors(asus);
} else {
...
fan_curve_refresh_floors(asus);
}
}
fan_curve_notify(asus);
}
In addition, when enabling a single fan curve, fan_curve_notify(asus) notifies
both pwmY_auto_pointZ_floor_pwm and pwmY_auto_pointZ_pwm across all fans with
valid floors, even though the floor tables were not reloaded and the other
fans did not change state. Could the enable path notify only the affected
fan's pwm attributes, and avoid the second fan_curve_notify(asus) call on the
disable path?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261101-asus-fan-floors-v4-v1-0-b8d1c31a99a9@gmail.com?part=7
^ permalink raw reply [flat|nested] 15+ messages in thread