* [PATCH 0/5] hwmon: Introduce uhwmon for userspace implementations
@ 2026-10-09 15:38 Jihong Min
2026-10-09 15:38 ` [PATCH 1/5] hwmon: Export sensor and attribute definitions to userspace Jihong Min
` (4 more replies)
0 siblings, 5 replies; 6+ messages in thread
From: Jihong Min @ 2026-10-09 15:38 UTC (permalink / raw)
To: Guenter Roeck, linux-hwmon
Cc: Jonathan Corbet, Shuah Khan, Randy Dunlap, linux-api, linux-doc,
linux-kernel
This series introduces uhwmon, a generic hwmon interface connecting
userspace implementations to the kernel. It exposes hwmon sensor and
attribute definitions to userspace through <linux/hwmon.h>, allowing
userspace drivers to create standard hwmon sysfs attributes. uhwmon
forwards reads and writes through /dev/uhwmon. The hwmon core handles
numeric text conversion and newlines for kernel and userspace drivers.
The series also adds kernel documentation explaining the interface and
how to use its UAPI.
In the October 2024 IT87 discussion [1], a read-only hwmon driver
configured from userspace was suggested for chips without datasheets and
for testing new chips. Guenter Roeck said he would need proposal
details, but questioned whether the hwmon sysfs ABI justified new kernel
infrastructure: userspace drivers would still need hardware knowledge
and access. He suggested an API between those drivers and libsensors as
an alternative.
But I see practical value in retaining hwmon: the use case below already
works with unmodified sensors through its existing hwmon interface. A
common bridge can similarly let other daemons expose measurements
without adding a new input path to existing hwmon applications, reducing
duplicated integration work. The idea is conceptually similar to
dummy_hcd with a userspace gadget interface.
uhwmon's goal is to let applications using the standard hwmon interface
access sensors without mainline drivers, without adding per-device
support to userspace libraries or building and loading separate DKMS
modules. In theory, projects such as wireview-hwmon [2] could replace
their device-specific DKMS module with uhwmon and adapt their daemon to
use it. My current use case is bridging the Tapo API [3] to hwmon, using
a TAPO P110M smart plug to measure my system's power draw at the wall in
place of an ACPI power meter. I have tested this setup in an Ubuntu VM,
including live readings, daemon disconnect and recovery, and reboot startup.
[1] https://lkml.iu.edu/2410.2/11841.html
[2] https://github.com/emaspa/wireview-hwmon
[3] https://github.com/mihai-dinculescu/tapo
Jihong Min (5):
hwmon: Export sensor and attribute definitions to userspace
hwmon: Expose attribute validation and string helpers
hwmon: Add a generic interface for userspace implementations
hwmon: Reject truncated attribute names
docs: hwmon: Document the uhwmon userspace interface
Documentation/hwmon/index.rst | 1 +
Documentation/hwmon/uhwmon.rst | 141 +++++
Documentation/userspace-api/ioctl/ioctl-number.rst | 1 +
MAINTAINERS | 2 +
drivers/hwmon/Kconfig | 9 +
drivers/hwmon/Makefile | 1 +
drivers/hwmon/hwmon.c | 38 +-
drivers/hwmon/uhwmon.c | 641 +++++++++++++++++++++
include/linux/hwmon.h | 356 +-----------
include/uapi/linux/hwmon.h | 372 ++++++++++++
include/uapi/linux/uhwmon.h | 64 ++
11 files changed, 1267 insertions(+), 359 deletions(-)
create mode 100644 Documentation/hwmon/uhwmon.rst
create mode 100644 drivers/hwmon/uhwmon.c
create mode 100644 include/uapi/linux/hwmon.h
create mode 100644 include/uapi/linux/uhwmon.h
base-commit: 7b63ef2d55f24519e7e9e5f4d15dbea03f126e40
Sincerely,
Jihong Min
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH 1/5] hwmon: Export sensor and attribute definitions to userspace
2026-10-09 15:38 [PATCH 0/5] hwmon: Introduce uhwmon for userspace implementations Jihong Min
@ 2026-10-09 15:38 ` Jihong Min
2026-10-09 15:38 ` [PATCH 2/5] hwmon: Expose attribute validation and string helpers Jihong Min
` (3 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Jihong Min @ 2026-10-09 15:38 UTC (permalink / raw)
To: Guenter Roeck, linux-hwmon
Cc: Jonathan Corbet, Shuah Khan, Randy Dunlap, linux-api, linux-doc,
linux-kernel
Move sensor types, attribute identifiers and configuration bits to
linux/hwmon.h in the UAPI so kernel and userspace drivers share them.
Keep existing values unchanged and use userspace-safe bit definitions.
Keep operations, channel structures and registration helpers in the
kernel header.
Assisted-by: Codex:gpt-6-astra
Signed-off-by: Jihong Min <hurryman2212@gmail.com>
---
MAINTAINERS | 1 +
include/linux/hwmon.h | 353 +----------------------------------
include/uapi/linux/hwmon.h | 372 +++++++++++++++++++++++++++++++++++++
3 files changed, 374 insertions(+), 352 deletions(-)
create mode 100644 include/uapi/linux/hwmon.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 65e8a4b5c90b..f3f0292832bb 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -11492,6 +11492,7 @@ F: Documentation/hwmon/
F: drivers/hwmon/
F: include/linux/hwmon*.h
F: include/trace/events/hwmon*.h
+F: include/uapi/linux/hwmon.h
K: (devm_)?hwmon_device_(un)?register(|_with_groups|_with_info)
HARDWARE RANDOM NUMBER GENERATOR CORE
diff --git a/include/linux/hwmon.h b/include/linux/hwmon.h
index dd713e193d0c..d48e0aa82bc5 100644
--- a/include/linux/hwmon.h
+++ b/include/linux/hwmon.h
@@ -14,362 +14,11 @@
#include <linux/bitops.h>
#include <linux/cleanup.h>
+#include <uapi/linux/hwmon.h>
struct device;
struct attribute_group;
-enum hwmon_sensor_types {
- hwmon_chip,
- hwmon_temp,
- hwmon_in,
- hwmon_curr,
- hwmon_power,
- hwmon_energy,
- hwmon_energy64,
- hwmon_humidity,
- hwmon_fan,
- hwmon_pwm,
- hwmon_intrusion,
- hwmon_max,
-};
-
-enum hwmon_chip_attributes {
- hwmon_chip_temp_reset_history,
- hwmon_chip_in_reset_history,
- hwmon_chip_curr_reset_history,
- hwmon_chip_power_reset_history,
- hwmon_chip_register_tz,
- hwmon_chip_update_interval,
- hwmon_chip_update_interval_us,
- hwmon_chip_alarms,
- hwmon_chip_samples,
- hwmon_chip_curr_samples,
- hwmon_chip_in_samples,
- hwmon_chip_power_samples,
- hwmon_chip_temp_samples,
- hwmon_chip_beep_enable,
- hwmon_chip_pec,
-};
-
-#define HWMON_C_TEMP_RESET_HISTORY BIT(hwmon_chip_temp_reset_history)
-#define HWMON_C_IN_RESET_HISTORY BIT(hwmon_chip_in_reset_history)
-#define HWMON_C_CURR_RESET_HISTORY BIT(hwmon_chip_curr_reset_history)
-#define HWMON_C_POWER_RESET_HISTORY BIT(hwmon_chip_power_reset_history)
-#define HWMON_C_REGISTER_TZ BIT(hwmon_chip_register_tz)
-#define HWMON_C_UPDATE_INTERVAL BIT(hwmon_chip_update_interval)
-#define HWMON_C_UPDATE_INTERVAL_US BIT(hwmon_chip_update_interval_us)
-#define HWMON_C_ALARMS BIT(hwmon_chip_alarms)
-#define HWMON_C_SAMPLES BIT(hwmon_chip_samples)
-#define HWMON_C_CURR_SAMPLES BIT(hwmon_chip_curr_samples)
-#define HWMON_C_IN_SAMPLES BIT(hwmon_chip_in_samples)
-#define HWMON_C_POWER_SAMPLES BIT(hwmon_chip_power_samples)
-#define HWMON_C_TEMP_SAMPLES BIT(hwmon_chip_temp_samples)
-#define HWMON_C_BEEP_ENABLE BIT(hwmon_chip_beep_enable)
-#define HWMON_C_PEC BIT(hwmon_chip_pec)
-
-enum hwmon_temp_attributes {
- hwmon_temp_enable,
- hwmon_temp_input,
- hwmon_temp_type,
- hwmon_temp_lcrit,
- hwmon_temp_lcrit_hyst,
- hwmon_temp_min,
- hwmon_temp_min_hyst,
- hwmon_temp_max,
- hwmon_temp_max_hyst,
- hwmon_temp_crit,
- hwmon_temp_crit_hyst,
- hwmon_temp_emergency,
- hwmon_temp_emergency_hyst,
- hwmon_temp_alarm,
- hwmon_temp_lcrit_alarm,
- hwmon_temp_min_alarm,
- hwmon_temp_max_alarm,
- hwmon_temp_crit_alarm,
- hwmon_temp_emergency_alarm,
- hwmon_temp_fault,
- hwmon_temp_offset,
- hwmon_temp_label,
- hwmon_temp_lowest,
- hwmon_temp_highest,
- hwmon_temp_reset_history,
- hwmon_temp_rated_min,
- hwmon_temp_rated_max,
- hwmon_temp_beep,
-};
-
-#define HWMON_T_ENABLE BIT(hwmon_temp_enable)
-#define HWMON_T_INPUT BIT(hwmon_temp_input)
-#define HWMON_T_TYPE BIT(hwmon_temp_type)
-#define HWMON_T_LCRIT BIT(hwmon_temp_lcrit)
-#define HWMON_T_LCRIT_HYST BIT(hwmon_temp_lcrit_hyst)
-#define HWMON_T_MIN BIT(hwmon_temp_min)
-#define HWMON_T_MIN_HYST BIT(hwmon_temp_min_hyst)
-#define HWMON_T_MAX BIT(hwmon_temp_max)
-#define HWMON_T_MAX_HYST BIT(hwmon_temp_max_hyst)
-#define HWMON_T_CRIT BIT(hwmon_temp_crit)
-#define HWMON_T_CRIT_HYST BIT(hwmon_temp_crit_hyst)
-#define HWMON_T_EMERGENCY BIT(hwmon_temp_emergency)
-#define HWMON_T_EMERGENCY_HYST BIT(hwmon_temp_emergency_hyst)
-#define HWMON_T_ALARM BIT(hwmon_temp_alarm)
-#define HWMON_T_MIN_ALARM BIT(hwmon_temp_min_alarm)
-#define HWMON_T_MAX_ALARM BIT(hwmon_temp_max_alarm)
-#define HWMON_T_CRIT_ALARM BIT(hwmon_temp_crit_alarm)
-#define HWMON_T_LCRIT_ALARM BIT(hwmon_temp_lcrit_alarm)
-#define HWMON_T_EMERGENCY_ALARM BIT(hwmon_temp_emergency_alarm)
-#define HWMON_T_FAULT BIT(hwmon_temp_fault)
-#define HWMON_T_OFFSET BIT(hwmon_temp_offset)
-#define HWMON_T_LABEL BIT(hwmon_temp_label)
-#define HWMON_T_LOWEST BIT(hwmon_temp_lowest)
-#define HWMON_T_HIGHEST BIT(hwmon_temp_highest)
-#define HWMON_T_RESET_HISTORY BIT(hwmon_temp_reset_history)
-#define HWMON_T_RATED_MIN BIT(hwmon_temp_rated_min)
-#define HWMON_T_RATED_MAX BIT(hwmon_temp_rated_max)
-#define HWMON_T_BEEP BIT(hwmon_temp_beep)
-
-enum hwmon_in_attributes {
- hwmon_in_enable,
- hwmon_in_input,
- hwmon_in_min,
- hwmon_in_max,
- hwmon_in_lcrit,
- hwmon_in_crit,
- hwmon_in_average,
- hwmon_in_lowest,
- hwmon_in_highest,
- hwmon_in_reset_history,
- hwmon_in_label,
- hwmon_in_alarm,
- hwmon_in_min_alarm,
- hwmon_in_max_alarm,
- hwmon_in_lcrit_alarm,
- hwmon_in_crit_alarm,
- hwmon_in_rated_min,
- hwmon_in_rated_max,
- hwmon_in_beep,
- hwmon_in_fault,
-};
-
-#define HWMON_I_ENABLE BIT(hwmon_in_enable)
-#define HWMON_I_INPUT BIT(hwmon_in_input)
-#define HWMON_I_MIN BIT(hwmon_in_min)
-#define HWMON_I_MAX BIT(hwmon_in_max)
-#define HWMON_I_LCRIT BIT(hwmon_in_lcrit)
-#define HWMON_I_CRIT BIT(hwmon_in_crit)
-#define HWMON_I_AVERAGE BIT(hwmon_in_average)
-#define HWMON_I_LOWEST BIT(hwmon_in_lowest)
-#define HWMON_I_HIGHEST BIT(hwmon_in_highest)
-#define HWMON_I_RESET_HISTORY BIT(hwmon_in_reset_history)
-#define HWMON_I_LABEL BIT(hwmon_in_label)
-#define HWMON_I_ALARM BIT(hwmon_in_alarm)
-#define HWMON_I_MIN_ALARM BIT(hwmon_in_min_alarm)
-#define HWMON_I_MAX_ALARM BIT(hwmon_in_max_alarm)
-#define HWMON_I_LCRIT_ALARM BIT(hwmon_in_lcrit_alarm)
-#define HWMON_I_CRIT_ALARM BIT(hwmon_in_crit_alarm)
-#define HWMON_I_RATED_MIN BIT(hwmon_in_rated_min)
-#define HWMON_I_RATED_MAX BIT(hwmon_in_rated_max)
-#define HWMON_I_BEEP BIT(hwmon_in_beep)
-#define HWMON_I_FAULT BIT(hwmon_in_fault)
-
-enum hwmon_curr_attributes {
- hwmon_curr_enable,
- hwmon_curr_input,
- hwmon_curr_min,
- hwmon_curr_max,
- hwmon_curr_lcrit,
- hwmon_curr_crit,
- hwmon_curr_average,
- hwmon_curr_lowest,
- hwmon_curr_highest,
- hwmon_curr_reset_history,
- hwmon_curr_label,
- hwmon_curr_alarm,
- hwmon_curr_min_alarm,
- hwmon_curr_max_alarm,
- hwmon_curr_lcrit_alarm,
- hwmon_curr_crit_alarm,
- hwmon_curr_rated_min,
- hwmon_curr_rated_max,
- hwmon_curr_beep,
-};
-
-#define HWMON_C_ENABLE BIT(hwmon_curr_enable)
-#define HWMON_C_INPUT BIT(hwmon_curr_input)
-#define HWMON_C_MIN BIT(hwmon_curr_min)
-#define HWMON_C_MAX BIT(hwmon_curr_max)
-#define HWMON_C_LCRIT BIT(hwmon_curr_lcrit)
-#define HWMON_C_CRIT BIT(hwmon_curr_crit)
-#define HWMON_C_AVERAGE BIT(hwmon_curr_average)
-#define HWMON_C_LOWEST BIT(hwmon_curr_lowest)
-#define HWMON_C_HIGHEST BIT(hwmon_curr_highest)
-#define HWMON_C_RESET_HISTORY BIT(hwmon_curr_reset_history)
-#define HWMON_C_LABEL BIT(hwmon_curr_label)
-#define HWMON_C_ALARM BIT(hwmon_curr_alarm)
-#define HWMON_C_MIN_ALARM BIT(hwmon_curr_min_alarm)
-#define HWMON_C_MAX_ALARM BIT(hwmon_curr_max_alarm)
-#define HWMON_C_LCRIT_ALARM BIT(hwmon_curr_lcrit_alarm)
-#define HWMON_C_CRIT_ALARM BIT(hwmon_curr_crit_alarm)
-#define HWMON_C_RATED_MIN BIT(hwmon_curr_rated_min)
-#define HWMON_C_RATED_MAX BIT(hwmon_curr_rated_max)
-#define HWMON_C_BEEP BIT(hwmon_curr_beep)
-
-enum hwmon_power_attributes {
- hwmon_power_enable,
- hwmon_power_average,
- hwmon_power_average_interval,
- hwmon_power_average_interval_max,
- hwmon_power_average_interval_min,
- hwmon_power_average_highest,
- hwmon_power_average_lowest,
- hwmon_power_average_max,
- hwmon_power_average_min,
- hwmon_power_input,
- hwmon_power_input_highest,
- hwmon_power_input_lowest,
- hwmon_power_reset_history,
- hwmon_power_accuracy,
- hwmon_power_cap,
- hwmon_power_cap_hyst,
- hwmon_power_cap_max,
- hwmon_power_cap_min,
- hwmon_power_min,
- hwmon_power_max,
- hwmon_power_crit,
- hwmon_power_lcrit,
- hwmon_power_label,
- hwmon_power_alarm,
- hwmon_power_cap_alarm,
- hwmon_power_min_alarm,
- hwmon_power_max_alarm,
- hwmon_power_lcrit_alarm,
- hwmon_power_crit_alarm,
- hwmon_power_rated_min,
- hwmon_power_rated_max,
-};
-
-#define HWMON_P_ENABLE BIT(hwmon_power_enable)
-#define HWMON_P_AVERAGE BIT(hwmon_power_average)
-#define HWMON_P_AVERAGE_INTERVAL BIT(hwmon_power_average_interval)
-#define HWMON_P_AVERAGE_INTERVAL_MAX BIT(hwmon_power_average_interval_max)
-#define HWMON_P_AVERAGE_INTERVAL_MIN BIT(hwmon_power_average_interval_min)
-#define HWMON_P_AVERAGE_HIGHEST BIT(hwmon_power_average_highest)
-#define HWMON_P_AVERAGE_LOWEST BIT(hwmon_power_average_lowest)
-#define HWMON_P_AVERAGE_MAX BIT(hwmon_power_average_max)
-#define HWMON_P_AVERAGE_MIN BIT(hwmon_power_average_min)
-#define HWMON_P_INPUT BIT(hwmon_power_input)
-#define HWMON_P_INPUT_HIGHEST BIT(hwmon_power_input_highest)
-#define HWMON_P_INPUT_LOWEST BIT(hwmon_power_input_lowest)
-#define HWMON_P_RESET_HISTORY BIT(hwmon_power_reset_history)
-#define HWMON_P_ACCURACY BIT(hwmon_power_accuracy)
-#define HWMON_P_CAP BIT(hwmon_power_cap)
-#define HWMON_P_CAP_HYST BIT(hwmon_power_cap_hyst)
-#define HWMON_P_CAP_MAX BIT(hwmon_power_cap_max)
-#define HWMON_P_CAP_MIN BIT(hwmon_power_cap_min)
-#define HWMON_P_MIN BIT(hwmon_power_min)
-#define HWMON_P_MAX BIT(hwmon_power_max)
-#define HWMON_P_LCRIT BIT(hwmon_power_lcrit)
-#define HWMON_P_CRIT BIT(hwmon_power_crit)
-#define HWMON_P_LABEL BIT(hwmon_power_label)
-#define HWMON_P_ALARM BIT(hwmon_power_alarm)
-#define HWMON_P_CAP_ALARM BIT(hwmon_power_cap_alarm)
-#define HWMON_P_MIN_ALARM BIT(hwmon_power_min_alarm)
-#define HWMON_P_MAX_ALARM BIT(hwmon_power_max_alarm)
-#define HWMON_P_LCRIT_ALARM BIT(hwmon_power_lcrit_alarm)
-#define HWMON_P_CRIT_ALARM BIT(hwmon_power_crit_alarm)
-#define HWMON_P_RATED_MIN BIT(hwmon_power_rated_min)
-#define HWMON_P_RATED_MAX BIT(hwmon_power_rated_max)
-
-enum hwmon_energy_attributes {
- hwmon_energy_enable,
- hwmon_energy_input,
- hwmon_energy_label,
-};
-
-#define HWMON_E_ENABLE BIT(hwmon_energy_enable)
-#define HWMON_E_INPUT BIT(hwmon_energy_input)
-#define HWMON_E_LABEL BIT(hwmon_energy_label)
-
-enum hwmon_humidity_attributes {
- hwmon_humidity_enable,
- hwmon_humidity_input,
- hwmon_humidity_label,
- hwmon_humidity_min,
- hwmon_humidity_min_hyst,
- hwmon_humidity_max,
- hwmon_humidity_max_hyst,
- hwmon_humidity_alarm,
- hwmon_humidity_fault,
- hwmon_humidity_rated_min,
- hwmon_humidity_rated_max,
- hwmon_humidity_min_alarm,
- hwmon_humidity_max_alarm,
-};
-
-#define HWMON_H_ENABLE BIT(hwmon_humidity_enable)
-#define HWMON_H_INPUT BIT(hwmon_humidity_input)
-#define HWMON_H_LABEL BIT(hwmon_humidity_label)
-#define HWMON_H_MIN BIT(hwmon_humidity_min)
-#define HWMON_H_MIN_HYST BIT(hwmon_humidity_min_hyst)
-#define HWMON_H_MAX BIT(hwmon_humidity_max)
-#define HWMON_H_MAX_HYST BIT(hwmon_humidity_max_hyst)
-#define HWMON_H_ALARM BIT(hwmon_humidity_alarm)
-#define HWMON_H_FAULT BIT(hwmon_humidity_fault)
-#define HWMON_H_RATED_MIN BIT(hwmon_humidity_rated_min)
-#define HWMON_H_RATED_MAX BIT(hwmon_humidity_rated_max)
-#define HWMON_H_MIN_ALARM BIT(hwmon_humidity_min_alarm)
-#define HWMON_H_MAX_ALARM BIT(hwmon_humidity_max_alarm)
-
-enum hwmon_fan_attributes {
- hwmon_fan_enable,
- hwmon_fan_input,
- hwmon_fan_label,
- hwmon_fan_min,
- hwmon_fan_max,
- hwmon_fan_div,
- hwmon_fan_pulses,
- hwmon_fan_target,
- hwmon_fan_alarm,
- hwmon_fan_min_alarm,
- hwmon_fan_max_alarm,
- hwmon_fan_fault,
- hwmon_fan_beep,
-};
-
-#define HWMON_F_ENABLE BIT(hwmon_fan_enable)
-#define HWMON_F_INPUT BIT(hwmon_fan_input)
-#define HWMON_F_LABEL BIT(hwmon_fan_label)
-#define HWMON_F_MIN BIT(hwmon_fan_min)
-#define HWMON_F_MAX BIT(hwmon_fan_max)
-#define HWMON_F_DIV BIT(hwmon_fan_div)
-#define HWMON_F_PULSES BIT(hwmon_fan_pulses)
-#define HWMON_F_TARGET BIT(hwmon_fan_target)
-#define HWMON_F_ALARM BIT(hwmon_fan_alarm)
-#define HWMON_F_MIN_ALARM BIT(hwmon_fan_min_alarm)
-#define HWMON_F_MAX_ALARM BIT(hwmon_fan_max_alarm)
-#define HWMON_F_FAULT BIT(hwmon_fan_fault)
-#define HWMON_F_BEEP BIT(hwmon_fan_beep)
-
-enum hwmon_pwm_attributes {
- hwmon_pwm_input,
- hwmon_pwm_enable,
- hwmon_pwm_mode,
- hwmon_pwm_freq,
- hwmon_pwm_auto_channels_temp,
-};
-
-#define HWMON_PWM_INPUT BIT(hwmon_pwm_input)
-#define HWMON_PWM_ENABLE BIT(hwmon_pwm_enable)
-#define HWMON_PWM_MODE BIT(hwmon_pwm_mode)
-#define HWMON_PWM_FREQ BIT(hwmon_pwm_freq)
-#define HWMON_PWM_AUTO_CHANNELS_TEMP BIT(hwmon_pwm_auto_channels_temp)
-
-enum hwmon_intrusion_attributes {
- hwmon_intrusion_alarm,
- hwmon_intrusion_beep,
-};
-#define HWMON_INTRUSION_ALARM BIT(hwmon_intrusion_alarm)
-#define HWMON_INTRUSION_BEEP BIT(hwmon_intrusion_beep)
-
/**
* struct hwmon_ops - hwmon device operations
* @visible: Static visibility. If non-zero, 'is_visible' is ignored.
diff --git a/include/uapi/linux/hwmon.h b/include/uapi/linux/hwmon.h
new file mode 100644
index 000000000000..274f9c9dcb87
--- /dev/null
+++ b/include/uapi/linux/hwmon.h
@@ -0,0 +1,372 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+/*
+ * Hardware monitoring sensor types, attributes and configuration bits.
+ *
+ * Copyright (C) 2005 Mark M. Hoffman <mhoffman@lightlink.com>
+ * Copyright (C) 2026 Jihong Min <hurryman2212@gmail.com>
+ */
+
+#ifndef _UAPI_LINUX_HWMON_H
+#define _UAPI_LINUX_HWMON_H
+
+#include <linux/const.h>
+
+/*
+ * Existing identifiers are ABI: never change their values. Append new attributes
+ * at the end of their enum and new sensor types before hwmon_max.
+ */
+
+enum hwmon_sensor_types {
+ hwmon_chip = 0,
+ hwmon_temp = 1,
+ hwmon_in = 2,
+ hwmon_curr = 3,
+ hwmon_power = 4,
+ hwmon_energy = 5,
+ hwmon_energy64 = 6,
+ hwmon_humidity = 7,
+ hwmon_fan = 8,
+ hwmon_pwm = 9,
+ hwmon_intrusion = 10,
+ hwmon_max = 11,
+};
+
+enum hwmon_chip_attributes {
+ hwmon_chip_temp_reset_history = 0,
+ hwmon_chip_in_reset_history = 1,
+ hwmon_chip_curr_reset_history = 2,
+ hwmon_chip_power_reset_history = 3,
+ hwmon_chip_register_tz = 4,
+ hwmon_chip_update_interval = 5,
+ hwmon_chip_update_interval_us = 6,
+ hwmon_chip_alarms = 7,
+ hwmon_chip_samples = 8,
+ hwmon_chip_curr_samples = 9,
+ hwmon_chip_in_samples = 10,
+ hwmon_chip_power_samples = 11,
+ hwmon_chip_temp_samples = 12,
+ hwmon_chip_beep_enable = 13,
+ hwmon_chip_pec = 14,
+};
+
+#define HWMON_C_TEMP_RESET_HISTORY _BITUL(hwmon_chip_temp_reset_history)
+#define HWMON_C_IN_RESET_HISTORY _BITUL(hwmon_chip_in_reset_history)
+#define HWMON_C_CURR_RESET_HISTORY _BITUL(hwmon_chip_curr_reset_history)
+#define HWMON_C_POWER_RESET_HISTORY _BITUL(hwmon_chip_power_reset_history)
+#define HWMON_C_REGISTER_TZ _BITUL(hwmon_chip_register_tz)
+#define HWMON_C_UPDATE_INTERVAL _BITUL(hwmon_chip_update_interval)
+#define HWMON_C_UPDATE_INTERVAL_US _BITUL(hwmon_chip_update_interval_us)
+#define HWMON_C_ALARMS _BITUL(hwmon_chip_alarms)
+#define HWMON_C_SAMPLES _BITUL(hwmon_chip_samples)
+#define HWMON_C_CURR_SAMPLES _BITUL(hwmon_chip_curr_samples)
+#define HWMON_C_IN_SAMPLES _BITUL(hwmon_chip_in_samples)
+#define HWMON_C_POWER_SAMPLES _BITUL(hwmon_chip_power_samples)
+#define HWMON_C_TEMP_SAMPLES _BITUL(hwmon_chip_temp_samples)
+#define HWMON_C_BEEP_ENABLE _BITUL(hwmon_chip_beep_enable)
+#define HWMON_C_PEC _BITUL(hwmon_chip_pec)
+
+enum hwmon_temp_attributes {
+ hwmon_temp_enable = 0,
+ hwmon_temp_input = 1,
+ hwmon_temp_type = 2,
+ hwmon_temp_lcrit = 3,
+ hwmon_temp_lcrit_hyst = 4,
+ hwmon_temp_min = 5,
+ hwmon_temp_min_hyst = 6,
+ hwmon_temp_max = 7,
+ hwmon_temp_max_hyst = 8,
+ hwmon_temp_crit = 9,
+ hwmon_temp_crit_hyst = 10,
+ hwmon_temp_emergency = 11,
+ hwmon_temp_emergency_hyst = 12,
+ hwmon_temp_alarm = 13,
+ hwmon_temp_lcrit_alarm = 14,
+ hwmon_temp_min_alarm = 15,
+ hwmon_temp_max_alarm = 16,
+ hwmon_temp_crit_alarm = 17,
+ hwmon_temp_emergency_alarm = 18,
+ hwmon_temp_fault = 19,
+ hwmon_temp_offset = 20,
+ hwmon_temp_label = 21,
+ hwmon_temp_lowest = 22,
+ hwmon_temp_highest = 23,
+ hwmon_temp_reset_history = 24,
+ hwmon_temp_rated_min = 25,
+ hwmon_temp_rated_max = 26,
+ hwmon_temp_beep = 27,
+};
+
+#define HWMON_T_ENABLE _BITUL(hwmon_temp_enable)
+#define HWMON_T_INPUT _BITUL(hwmon_temp_input)
+#define HWMON_T_TYPE _BITUL(hwmon_temp_type)
+#define HWMON_T_LCRIT _BITUL(hwmon_temp_lcrit)
+#define HWMON_T_LCRIT_HYST _BITUL(hwmon_temp_lcrit_hyst)
+#define HWMON_T_MIN _BITUL(hwmon_temp_min)
+#define HWMON_T_MIN_HYST _BITUL(hwmon_temp_min_hyst)
+#define HWMON_T_MAX _BITUL(hwmon_temp_max)
+#define HWMON_T_MAX_HYST _BITUL(hwmon_temp_max_hyst)
+#define HWMON_T_CRIT _BITUL(hwmon_temp_crit)
+#define HWMON_T_CRIT_HYST _BITUL(hwmon_temp_crit_hyst)
+#define HWMON_T_EMERGENCY _BITUL(hwmon_temp_emergency)
+#define HWMON_T_EMERGENCY_HYST _BITUL(hwmon_temp_emergency_hyst)
+#define HWMON_T_ALARM _BITUL(hwmon_temp_alarm)
+#define HWMON_T_MIN_ALARM _BITUL(hwmon_temp_min_alarm)
+#define HWMON_T_MAX_ALARM _BITUL(hwmon_temp_max_alarm)
+#define HWMON_T_CRIT_ALARM _BITUL(hwmon_temp_crit_alarm)
+#define HWMON_T_LCRIT_ALARM _BITUL(hwmon_temp_lcrit_alarm)
+#define HWMON_T_EMERGENCY_ALARM _BITUL(hwmon_temp_emergency_alarm)
+#define HWMON_T_FAULT _BITUL(hwmon_temp_fault)
+#define HWMON_T_OFFSET _BITUL(hwmon_temp_offset)
+#define HWMON_T_LABEL _BITUL(hwmon_temp_label)
+#define HWMON_T_LOWEST _BITUL(hwmon_temp_lowest)
+#define HWMON_T_HIGHEST _BITUL(hwmon_temp_highest)
+#define HWMON_T_RESET_HISTORY _BITUL(hwmon_temp_reset_history)
+#define HWMON_T_RATED_MIN _BITUL(hwmon_temp_rated_min)
+#define HWMON_T_RATED_MAX _BITUL(hwmon_temp_rated_max)
+#define HWMON_T_BEEP _BITUL(hwmon_temp_beep)
+
+enum hwmon_in_attributes {
+ hwmon_in_enable = 0,
+ hwmon_in_input = 1,
+ hwmon_in_min = 2,
+ hwmon_in_max = 3,
+ hwmon_in_lcrit = 4,
+ hwmon_in_crit = 5,
+ hwmon_in_average = 6,
+ hwmon_in_lowest = 7,
+ hwmon_in_highest = 8,
+ hwmon_in_reset_history = 9,
+ hwmon_in_label = 10,
+ hwmon_in_alarm = 11,
+ hwmon_in_min_alarm = 12,
+ hwmon_in_max_alarm = 13,
+ hwmon_in_lcrit_alarm = 14,
+ hwmon_in_crit_alarm = 15,
+ hwmon_in_rated_min = 16,
+ hwmon_in_rated_max = 17,
+ hwmon_in_beep = 18,
+ hwmon_in_fault = 19,
+};
+
+#define HWMON_I_ENABLE _BITUL(hwmon_in_enable)
+#define HWMON_I_INPUT _BITUL(hwmon_in_input)
+#define HWMON_I_MIN _BITUL(hwmon_in_min)
+#define HWMON_I_MAX _BITUL(hwmon_in_max)
+#define HWMON_I_LCRIT _BITUL(hwmon_in_lcrit)
+#define HWMON_I_CRIT _BITUL(hwmon_in_crit)
+#define HWMON_I_AVERAGE _BITUL(hwmon_in_average)
+#define HWMON_I_LOWEST _BITUL(hwmon_in_lowest)
+#define HWMON_I_HIGHEST _BITUL(hwmon_in_highest)
+#define HWMON_I_RESET_HISTORY _BITUL(hwmon_in_reset_history)
+#define HWMON_I_LABEL _BITUL(hwmon_in_label)
+#define HWMON_I_ALARM _BITUL(hwmon_in_alarm)
+#define HWMON_I_MIN_ALARM _BITUL(hwmon_in_min_alarm)
+#define HWMON_I_MAX_ALARM _BITUL(hwmon_in_max_alarm)
+#define HWMON_I_LCRIT_ALARM _BITUL(hwmon_in_lcrit_alarm)
+#define HWMON_I_CRIT_ALARM _BITUL(hwmon_in_crit_alarm)
+#define HWMON_I_RATED_MIN _BITUL(hwmon_in_rated_min)
+#define HWMON_I_RATED_MAX _BITUL(hwmon_in_rated_max)
+#define HWMON_I_BEEP _BITUL(hwmon_in_beep)
+#define HWMON_I_FAULT _BITUL(hwmon_in_fault)
+
+enum hwmon_curr_attributes {
+ hwmon_curr_enable = 0,
+ hwmon_curr_input = 1,
+ hwmon_curr_min = 2,
+ hwmon_curr_max = 3,
+ hwmon_curr_lcrit = 4,
+ hwmon_curr_crit = 5,
+ hwmon_curr_average = 6,
+ hwmon_curr_lowest = 7,
+ hwmon_curr_highest = 8,
+ hwmon_curr_reset_history = 9,
+ hwmon_curr_label = 10,
+ hwmon_curr_alarm = 11,
+ hwmon_curr_min_alarm = 12,
+ hwmon_curr_max_alarm = 13,
+ hwmon_curr_lcrit_alarm = 14,
+ hwmon_curr_crit_alarm = 15,
+ hwmon_curr_rated_min = 16,
+ hwmon_curr_rated_max = 17,
+ hwmon_curr_beep = 18,
+};
+
+#define HWMON_C_ENABLE _BITUL(hwmon_curr_enable)
+#define HWMON_C_INPUT _BITUL(hwmon_curr_input)
+#define HWMON_C_MIN _BITUL(hwmon_curr_min)
+#define HWMON_C_MAX _BITUL(hwmon_curr_max)
+#define HWMON_C_LCRIT _BITUL(hwmon_curr_lcrit)
+#define HWMON_C_CRIT _BITUL(hwmon_curr_crit)
+#define HWMON_C_AVERAGE _BITUL(hwmon_curr_average)
+#define HWMON_C_LOWEST _BITUL(hwmon_curr_lowest)
+#define HWMON_C_HIGHEST _BITUL(hwmon_curr_highest)
+#define HWMON_C_RESET_HISTORY _BITUL(hwmon_curr_reset_history)
+#define HWMON_C_LABEL _BITUL(hwmon_curr_label)
+#define HWMON_C_ALARM _BITUL(hwmon_curr_alarm)
+#define HWMON_C_MIN_ALARM _BITUL(hwmon_curr_min_alarm)
+#define HWMON_C_MAX_ALARM _BITUL(hwmon_curr_max_alarm)
+#define HWMON_C_LCRIT_ALARM _BITUL(hwmon_curr_lcrit_alarm)
+#define HWMON_C_CRIT_ALARM _BITUL(hwmon_curr_crit_alarm)
+#define HWMON_C_RATED_MIN _BITUL(hwmon_curr_rated_min)
+#define HWMON_C_RATED_MAX _BITUL(hwmon_curr_rated_max)
+#define HWMON_C_BEEP _BITUL(hwmon_curr_beep)
+
+enum hwmon_power_attributes {
+ hwmon_power_enable = 0,
+ hwmon_power_average = 1,
+ hwmon_power_average_interval = 2,
+ hwmon_power_average_interval_max = 3,
+ hwmon_power_average_interval_min = 4,
+ hwmon_power_average_highest = 5,
+ hwmon_power_average_lowest = 6,
+ hwmon_power_average_max = 7,
+ hwmon_power_average_min = 8,
+ hwmon_power_input = 9,
+ hwmon_power_input_highest = 10,
+ hwmon_power_input_lowest = 11,
+ hwmon_power_reset_history = 12,
+ hwmon_power_accuracy = 13,
+ hwmon_power_cap = 14,
+ hwmon_power_cap_hyst = 15,
+ hwmon_power_cap_max = 16,
+ hwmon_power_cap_min = 17,
+ hwmon_power_min = 18,
+ hwmon_power_max = 19,
+ hwmon_power_crit = 20,
+ hwmon_power_lcrit = 21,
+ hwmon_power_label = 22,
+ hwmon_power_alarm = 23,
+ hwmon_power_cap_alarm = 24,
+ hwmon_power_min_alarm = 25,
+ hwmon_power_max_alarm = 26,
+ hwmon_power_lcrit_alarm = 27,
+ hwmon_power_crit_alarm = 28,
+ hwmon_power_rated_min = 29,
+ hwmon_power_rated_max = 30,
+};
+
+#define HWMON_P_ENABLE _BITUL(hwmon_power_enable)
+#define HWMON_P_AVERAGE _BITUL(hwmon_power_average)
+#define HWMON_P_AVERAGE_INTERVAL _BITUL(hwmon_power_average_interval)
+#define HWMON_P_AVERAGE_INTERVAL_MAX _BITUL(hwmon_power_average_interval_max)
+#define HWMON_P_AVERAGE_INTERVAL_MIN _BITUL(hwmon_power_average_interval_min)
+#define HWMON_P_AVERAGE_HIGHEST _BITUL(hwmon_power_average_highest)
+#define HWMON_P_AVERAGE_LOWEST _BITUL(hwmon_power_average_lowest)
+#define HWMON_P_AVERAGE_MAX _BITUL(hwmon_power_average_max)
+#define HWMON_P_AVERAGE_MIN _BITUL(hwmon_power_average_min)
+#define HWMON_P_INPUT _BITUL(hwmon_power_input)
+#define HWMON_P_INPUT_HIGHEST _BITUL(hwmon_power_input_highest)
+#define HWMON_P_INPUT_LOWEST _BITUL(hwmon_power_input_lowest)
+#define HWMON_P_RESET_HISTORY _BITUL(hwmon_power_reset_history)
+#define HWMON_P_ACCURACY _BITUL(hwmon_power_accuracy)
+#define HWMON_P_CAP _BITUL(hwmon_power_cap)
+#define HWMON_P_CAP_HYST _BITUL(hwmon_power_cap_hyst)
+#define HWMON_P_CAP_MAX _BITUL(hwmon_power_cap_max)
+#define HWMON_P_CAP_MIN _BITUL(hwmon_power_cap_min)
+#define HWMON_P_MIN _BITUL(hwmon_power_min)
+#define HWMON_P_MAX _BITUL(hwmon_power_max)
+#define HWMON_P_LCRIT _BITUL(hwmon_power_lcrit)
+#define HWMON_P_CRIT _BITUL(hwmon_power_crit)
+#define HWMON_P_LABEL _BITUL(hwmon_power_label)
+#define HWMON_P_ALARM _BITUL(hwmon_power_alarm)
+#define HWMON_P_CAP_ALARM _BITUL(hwmon_power_cap_alarm)
+#define HWMON_P_MIN_ALARM _BITUL(hwmon_power_min_alarm)
+#define HWMON_P_MAX_ALARM _BITUL(hwmon_power_max_alarm)
+#define HWMON_P_LCRIT_ALARM _BITUL(hwmon_power_lcrit_alarm)
+#define HWMON_P_CRIT_ALARM _BITUL(hwmon_power_crit_alarm)
+#define HWMON_P_RATED_MIN _BITUL(hwmon_power_rated_min)
+#define HWMON_P_RATED_MAX _BITUL(hwmon_power_rated_max)
+
+enum hwmon_energy_attributes {
+ hwmon_energy_enable = 0,
+ hwmon_energy_input = 1,
+ hwmon_energy_label = 2,
+};
+
+#define HWMON_E_ENABLE _BITUL(hwmon_energy_enable)
+#define HWMON_E_INPUT _BITUL(hwmon_energy_input)
+#define HWMON_E_LABEL _BITUL(hwmon_energy_label)
+
+enum hwmon_humidity_attributes {
+ hwmon_humidity_enable = 0,
+ hwmon_humidity_input = 1,
+ hwmon_humidity_label = 2,
+ hwmon_humidity_min = 3,
+ hwmon_humidity_min_hyst = 4,
+ hwmon_humidity_max = 5,
+ hwmon_humidity_max_hyst = 6,
+ hwmon_humidity_alarm = 7,
+ hwmon_humidity_fault = 8,
+ hwmon_humidity_rated_min = 9,
+ hwmon_humidity_rated_max = 10,
+ hwmon_humidity_min_alarm = 11,
+ hwmon_humidity_max_alarm = 12,
+};
+
+#define HWMON_H_ENABLE _BITUL(hwmon_humidity_enable)
+#define HWMON_H_INPUT _BITUL(hwmon_humidity_input)
+#define HWMON_H_LABEL _BITUL(hwmon_humidity_label)
+#define HWMON_H_MIN _BITUL(hwmon_humidity_min)
+#define HWMON_H_MIN_HYST _BITUL(hwmon_humidity_min_hyst)
+#define HWMON_H_MAX _BITUL(hwmon_humidity_max)
+#define HWMON_H_MAX_HYST _BITUL(hwmon_humidity_max_hyst)
+#define HWMON_H_ALARM _BITUL(hwmon_humidity_alarm)
+#define HWMON_H_FAULT _BITUL(hwmon_humidity_fault)
+#define HWMON_H_RATED_MIN _BITUL(hwmon_humidity_rated_min)
+#define HWMON_H_RATED_MAX _BITUL(hwmon_humidity_rated_max)
+#define HWMON_H_MIN_ALARM _BITUL(hwmon_humidity_min_alarm)
+#define HWMON_H_MAX_ALARM _BITUL(hwmon_humidity_max_alarm)
+
+enum hwmon_fan_attributes {
+ hwmon_fan_enable = 0,
+ hwmon_fan_input = 1,
+ hwmon_fan_label = 2,
+ hwmon_fan_min = 3,
+ hwmon_fan_max = 4,
+ hwmon_fan_div = 5,
+ hwmon_fan_pulses = 6,
+ hwmon_fan_target = 7,
+ hwmon_fan_alarm = 8,
+ hwmon_fan_min_alarm = 9,
+ hwmon_fan_max_alarm = 10,
+ hwmon_fan_fault = 11,
+ hwmon_fan_beep = 12,
+};
+
+#define HWMON_F_ENABLE _BITUL(hwmon_fan_enable)
+#define HWMON_F_INPUT _BITUL(hwmon_fan_input)
+#define HWMON_F_LABEL _BITUL(hwmon_fan_label)
+#define HWMON_F_MIN _BITUL(hwmon_fan_min)
+#define HWMON_F_MAX _BITUL(hwmon_fan_max)
+#define HWMON_F_DIV _BITUL(hwmon_fan_div)
+#define HWMON_F_PULSES _BITUL(hwmon_fan_pulses)
+#define HWMON_F_TARGET _BITUL(hwmon_fan_target)
+#define HWMON_F_ALARM _BITUL(hwmon_fan_alarm)
+#define HWMON_F_MIN_ALARM _BITUL(hwmon_fan_min_alarm)
+#define HWMON_F_MAX_ALARM _BITUL(hwmon_fan_max_alarm)
+#define HWMON_F_FAULT _BITUL(hwmon_fan_fault)
+#define HWMON_F_BEEP _BITUL(hwmon_fan_beep)
+
+enum hwmon_pwm_attributes {
+ hwmon_pwm_input = 0,
+ hwmon_pwm_enable = 1,
+ hwmon_pwm_mode = 2,
+ hwmon_pwm_freq = 3,
+ hwmon_pwm_auto_channels_temp = 4,
+};
+
+#define HWMON_PWM_INPUT _BITUL(hwmon_pwm_input)
+#define HWMON_PWM_ENABLE _BITUL(hwmon_pwm_enable)
+#define HWMON_PWM_MODE _BITUL(hwmon_pwm_mode)
+#define HWMON_PWM_FREQ _BITUL(hwmon_pwm_freq)
+#define HWMON_PWM_AUTO_CHANNELS_TEMP _BITUL(hwmon_pwm_auto_channels_temp)
+
+enum hwmon_intrusion_attributes {
+ hwmon_intrusion_alarm = 0,
+ hwmon_intrusion_beep = 1,
+};
+
+#define HWMON_INTRUSION_ALARM _BITUL(hwmon_intrusion_alarm)
+#define HWMON_INTRUSION_BEEP _BITUL(hwmon_intrusion_beep)
+
+#endif /* _UAPI_LINUX_HWMON_H */
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH 2/5] hwmon: Expose attribute validation and string helpers
2026-10-09 15:38 [PATCH 0/5] hwmon: Introduce uhwmon for userspace implementations Jihong Min
2026-10-09 15:38 ` [PATCH 1/5] hwmon: Export sensor and attribute definitions to userspace Jihong Min
@ 2026-10-09 15:38 ` Jihong Min
2026-10-09 15:38 ` [PATCH 3/5] hwmon: Add a generic interface for userspace implementations Jihong Min
` (2 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Jihong Min @ 2026-10-09 15:38 UTC (permalink / raw)
To: Guenter Roeck, linux-hwmon
Cc: Jonathan Corbet, Shuah Khan, Randy Dunlap, linux-api, linux-doc,
linux-kernel
Expose the core's string classification and sysfs template validation
through the kernel hwmon API. Let drivers query these properties without
maintaining separate attribute tables.
Reuse the helpers when creating attributes.
Assisted-by: Codex:gpt-6-astra
Signed-off-by: Jihong Min <hurryman2212@gmail.com>
---
drivers/hwmon/hwmon.c | 30 +++++++++++++++++++++++++-----
include/linux/hwmon.h | 3 +++
2 files changed, 28 insertions(+), 5 deletions(-)
diff --git a/drivers/hwmon/hwmon.c b/drivers/hwmon/hwmon.c
index 10d2df3efdfa..bed16cfd4f1c 100644
--- a/drivers/hwmon/hwmon.c
+++ b/drivers/hwmon/hwmon.c
@@ -502,7 +502,14 @@ static ssize_t hwmon_attr_store(struct device *dev,
return count;
}
-static bool is_string_attr(enum hwmon_sensor_types type, u32 attr)
+/**
+ * hwmon_is_string_attr - test whether an attribute returns a string
+ * @type: sensor type
+ * @attr: attribute identifier
+ *
+ * Return: true for string attributes, false otherwise.
+ */
+bool hwmon_is_string_attr(enum hwmon_sensor_types type, u32 attr)
{
return (type == hwmon_temp && attr == hwmon_temp_label) ||
(type == hwmon_in && attr == hwmon_in_label) ||
@@ -513,6 +520,7 @@ static bool is_string_attr(enum hwmon_sensor_types type, u32 attr)
(type == hwmon_humidity && attr == hwmon_humidity_label) ||
(type == hwmon_fan && attr == hwmon_fan_label);
}
+EXPORT_SYMBOL_GPL(hwmon_is_string_attr);
static const struct attribute *hwmon_genattr(const void *drvdata,
enum hwmon_sensor_types type,
@@ -526,7 +534,7 @@ static const struct attribute *hwmon_genattr(const void *drvdata,
struct attribute *a;
umode_t mode;
const char *name;
- bool is_string = is_string_attr(type, attr);
+ bool is_string = hwmon_is_string_attr(type, attr);
mode = hwmon_is_visible(ops, drvdata, type, attr, index);
if (!mode)
@@ -776,6 +784,20 @@ static const int __templates_size[] = {
[hwmon_intrusion] = ARRAY_SIZE(hwmon_intrusion_attr_templates),
};
+/**
+ * hwmon_is_valid_attr - test whether an attribute has a sysfs template
+ * @type: sensor type
+ * @attr: attribute identifier
+ *
+ * Return: true for supported sysfs attributes, false otherwise.
+ */
+bool hwmon_is_valid_attr(enum hwmon_sensor_types type, u32 attr)
+{
+ return (unsigned int)type < ARRAY_SIZE(__templates) &&
+ attr < __templates_size[type] && __templates[type][attr];
+}
+EXPORT_SYMBOL_GPL(hwmon_is_valid_attr);
+
int hwmon_notify_event(struct device *dev, enum hwmon_sensor_types type,
u32 attr, int channel)
{
@@ -843,14 +865,12 @@ static int hwmon_genattrs(const void *drvdata,
const struct hwmon_channel_info *info)
{
const char * const *templates;
- int template_size;
int i, aindex = 0;
if (info->type >= ARRAY_SIZE(__templates))
return -EINVAL;
templates = __templates[info->type];
- template_size = __templates_size[info->type];
for (i = 0; info->config[i]; i++) {
u32 attr_mask = info->config[i];
@@ -861,7 +881,7 @@ static int hwmon_genattrs(const void *drvdata,
attr = __ffs(attr_mask);
attr_mask &= ~BIT(attr);
- if (attr >= template_size || !templates[attr])
+ if (!hwmon_is_valid_attr(info->type, attr))
continue; /* attribute is invisible */
a = hwmon_genattr(drvdata, info->type, attr, i,
templates[attr], ops);
diff --git a/include/linux/hwmon.h b/include/linux/hwmon.h
index d48e0aa82bc5..b026aa317630 100644
--- a/include/linux/hwmon.h
+++ b/include/linux/hwmon.h
@@ -138,6 +138,9 @@ devm_hwmon_device_register_with_info(struct device *dev,
void hwmon_device_unregister(struct device *dev);
+bool hwmon_is_valid_attr(enum hwmon_sensor_types type, u32 attr);
+bool hwmon_is_string_attr(enum hwmon_sensor_types type, u32 attr);
+
int hwmon_notify_event(struct device *dev, enum hwmon_sensor_types type,
u32 attr, int channel);
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH 3/5] hwmon: Add a generic interface for userspace implementations
2026-10-09 15:38 [PATCH 0/5] hwmon: Introduce uhwmon for userspace implementations Jihong Min
2026-10-09 15:38 ` [PATCH 1/5] hwmon: Export sensor and attribute definitions to userspace Jihong Min
2026-10-09 15:38 ` [PATCH 2/5] hwmon: Expose attribute validation and string helpers Jihong Min
@ 2026-10-09 15:38 ` Jihong Min
2026-10-09 15:38 ` [PATCH 4/5] hwmon: Reject truncated attribute names Jihong Min
2026-10-09 15:38 ` [PATCH 5/5] docs: hwmon: Document the uhwmon userspace interface Jihong Min
4 siblings, 0 replies; 6+ messages in thread
From: Jihong Min @ 2026-10-09 15:38 UTC (permalink / raw)
To: Guenter Roeck, linux-hwmon
Cc: Jonathan Corbet, Shuah Khan, Randy Dunlap, linux-api, linux-doc,
linux-kernel
Add /dev/uhwmon so userspace drivers can expose standard hwmon
attributes. Use the shared hwmon identifiers directly and forward
numeric values or labels through read and write requests.
Reuse hwmon sysfs formatting and notifications. Preserve devices across
daemon disconnects and support reconnection or explicit removal.
Assisted-by: Codex:gpt-6-astra
Signed-off-by: Jihong Min <hurryman2212@gmail.com>
---
.../userspace-api/ioctl/ioctl-number.rst | 1 +
MAINTAINERS | 1 +
drivers/hwmon/Kconfig | 9 +
drivers/hwmon/Makefile | 1 +
drivers/hwmon/uhwmon.c | 641 ++++++++++++++++++
include/uapi/linux/uhwmon.h | 64 ++
6 files changed, 717 insertions(+)
create mode 100644 drivers/hwmon/uhwmon.c
create mode 100644 include/uapi/linux/uhwmon.h
diff --git a/Documentation/userspace-api/ioctl/ioctl-number.rst b/Documentation/userspace-api/ioctl/ioctl-number.rst
index 2fc53093752d..76126b74ebb7 100644
--- a/Documentation/userspace-api/ioctl/ioctl-number.rst
+++ b/Documentation/userspace-api/ioctl/ioctl-number.rst
@@ -384,6 +384,7 @@ Code Seq# Include File Comments
0xB8 01-02 uapi/misc/mrvl_cn10k_dpi.h Marvell CN10K DPI driver
0xB8 all uapi/linux/mshv.h Microsoft Hyper-V /dev/mshv driver
<mailto:linux-hyperv@vger.kernel.org>
+0xB9 00-02 uapi/linux/uhwmon.h Userspace hardware monitoring
0xBA 00-0F uapi/linux/liveupdate.h Pasha Tatashin
<mailto:pasha.tatashin@soleen.com>
0xC0 00-0F linux/usb/iowarrior.h
diff --git a/MAINTAINERS b/MAINTAINERS
index f3f0292832bb..fb9d9233a8d6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -11493,6 +11493,7 @@ F: drivers/hwmon/
F: include/linux/hwmon*.h
F: include/trace/events/hwmon*.h
F: include/uapi/linux/hwmon.h
+F: include/uapi/linux/uhwmon.h
K: (devm_)?hwmon_device_(un)?register(|_with_groups|_with_info)
HARDWARE RANDOM NUMBER GENERATOR CORE
diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index fecff8610ea8..dbff7bff035f 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -2537,6 +2537,15 @@ config SENSORS_TSC1641
This driver can also be built as a module. If so, the module
will be called tsc1641.
+config SENSORS_UHWMON
+ tristate "Userspace hardware monitoring devices"
+ help
+ Provide a standard interface for userspace hwmon drivers through
+ /dev/uhwmon.
+
+ This driver can also be built as a module. If so, the module
+ will be called uhwmon.
+
config SENSORS_VEXPRESS
tristate "Versatile Express"
depends on VEXPRESS_CONFIG
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index 1229b6b3996d..27baa01d8d0f 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -242,6 +242,7 @@ obj-$(CONFIG_SENSORS_TMP421) += tmp421.o
obj-$(CONFIG_SENSORS_TMP464) += tmp464.o
obj-$(CONFIG_SENSORS_TMP513) += tmp513.o
obj-$(CONFIG_SENSORS_TSC1641) += tsc1641.o
+obj-$(CONFIG_SENSORS_UHWMON) += uhwmon.o
obj-$(CONFIG_SENSORS_VEXPRESS) += vexpress-hwmon.o
obj-$(CONFIG_SENSORS_VIA_CPUTEMP)+= via-cputemp.o
obj-$(CONFIG_SENSORS_VIA686A) += via686a.o
diff --git a/drivers/hwmon/uhwmon.c b/drivers/hwmon/uhwmon.c
new file mode 100644
index 000000000000..1855c87f06d9
--- /dev/null
+++ b/drivers/hwmon/uhwmon.c
@@ -0,0 +1,641 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Generic hwmon interface for userspace implementations.
+ *
+ * Copyright (C) 2026 Jihong Min <hurryman2212@gmail.com>
+ */
+
+#include <linux/fs.h>
+#include <linux/hwmon.h>
+#include <linux/list.h>
+#include <linux/miscdevice.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/overflow.h>
+#include <linux/poll.h>
+#include <linux/slab.h>
+#include <linux/uaccess.h>
+#include <linux/uhwmon.h>
+
+struct uhwmon_attr {
+ struct uhwmon_attribute spec;
+ char *text;
+};
+
+struct uhwmon_pending {
+ struct uhwmon_request request;
+ s64 *value;
+ int result;
+ bool done;
+};
+
+struct uhwmon_device {
+ struct list_head node;
+ struct device *hwmon;
+ char *name;
+ struct uhwmon_attr *attrs;
+ struct hwmon_chip_info chip;
+ struct hwmon_channel_info channels[hwmon_max];
+ const struct hwmon_channel_info *info[hwmon_max + 1];
+ char *text;
+ u32 num_attrs;
+ /* Protects connection and request state. Never held while waiting. */
+ struct mutex lock;
+ bool online;
+ struct uhwmon_pending *pending;
+ u64 next_id;
+ wait_queue_head_t wait;
+ bool readable;
+ bool destroyed;
+};
+
+static DEFINE_MUTEX(uhwmon_control_lock);
+static LIST_HEAD(uhwmon_devices);
+
+static int uhwmon_find_attr(const struct uhwmon_device *data,
+ enum hwmon_sensor_types type, u32 attr, int channel)
+{
+ unsigned int i;
+
+ for (i = 0; i < data->num_attrs; i++) {
+ const struct uhwmon_attribute *spec = &data->attrs[i].spec;
+
+ if (spec->type == type && spec->attr == attr &&
+ spec->channel == channel)
+ return i;
+ }
+ return -ENOENT;
+}
+
+static int uhwmon_request(struct uhwmon_device *data, unsigned int index,
+ u32 op, s64 *value)
+{
+ struct uhwmon_pending pending = {
+ .request = {
+ .attr = index,
+ .op = op,
+ .value = op == UHWMON_WRITE ? *value : 0,
+ },
+ .value = value,
+ };
+ int ret;
+
+ mutex_lock(&data->lock);
+ if (!data->online) {
+ ret = op == UHWMON_READ ? -ENODATA : -ENODEV;
+ goto out;
+ }
+ /* hwmon serializes callbacks, including consumption of read_string(). */
+ pending.request.id = ++data->next_id;
+ data->pending = &pending;
+ data->readable = true;
+ mutex_unlock(&data->lock);
+ wake_up_all(&data->wait);
+
+ ret = wait_event_interruptible(data->wait, READ_ONCE(pending.done));
+ mutex_lock(&data->lock);
+ if (!pending.done && !data->readable) {
+ /* Once delivered, only fatal signals may abort the wait. */
+ mutex_unlock(&data->lock);
+ ret = wait_event_killable(data->wait, READ_ONCE(pending.done));
+ mutex_lock(&data->lock);
+ }
+ if (pending.done)
+ ret = pending.result;
+ data->pending = NULL;
+ data->readable = false;
+out:
+ mutex_unlock(&data->lock);
+ return ret;
+}
+
+static umode_t uhwmon_is_visible(const void *drvdata,
+ enum hwmon_sensor_types type, u32 attr,
+ int channel)
+{
+ const struct uhwmon_device *data = drvdata;
+ int index = uhwmon_find_attr(data, type, attr, channel);
+
+ return index < 0 ? 0 : data->attrs[index].spec.mode;
+}
+
+static int uhwmon_read_value(struct device *dev, enum hwmon_sensor_types type,
+ u32 attr, int channel, long *val)
+{
+ struct uhwmon_device *data = dev_get_drvdata(dev);
+ int index = uhwmon_find_attr(data, type, attr, channel);
+ s64 value;
+ int ret;
+
+ if (index < 0)
+ return index;
+ value = data->attrs[index].spec.value;
+ if (!(data->attrs[index].spec.flags & UHWMON_ATTR_CONSTANT)) {
+ ret = uhwmon_request(data, index, UHWMON_READ, &value);
+ if (ret)
+ return ret;
+ }
+ if (type == hwmon_energy64) {
+ *(s64 *)val = value;
+ return 0;
+ }
+ if (value < LONG_MIN || value > LONG_MAX)
+ return -ERANGE;
+ *val = value;
+ return 0;
+}
+
+static int uhwmon_read_string(struct device *dev, enum hwmon_sensor_types type,
+ u32 attr, int channel, const char **str)
+{
+ struct uhwmon_device *data = dev_get_drvdata(dev);
+ int index = uhwmon_find_attr(data, type, attr, channel);
+ int ret;
+
+ if (index < 0)
+ return index;
+ if (data->attrs[index].spec.flags & UHWMON_ATTR_CONSTANT) {
+ *str = data->attrs[index].text;
+ return 0;
+ }
+ ret = uhwmon_request(data, index, UHWMON_READ, NULL);
+ if (!ret)
+ *str = data->text;
+ return ret;
+}
+
+static int uhwmon_write_value(struct device *dev, enum hwmon_sensor_types type,
+ u32 attr, int channel, long val)
+{
+ struct uhwmon_device *data = dev_get_drvdata(dev);
+ int index = uhwmon_find_attr(data, type, attr, channel);
+ s64 value = val;
+
+ if (index < 0)
+ return index;
+ return uhwmon_request(data, index, UHWMON_WRITE, &value);
+}
+
+static const struct hwmon_ops uhwmon_ops = {
+ .is_visible = uhwmon_is_visible,
+ .read = uhwmon_read_value,
+ .read_string = uhwmon_read_string,
+ .write = uhwmon_write_value,
+};
+
+static void uhwmon_free(struct uhwmon_device *data)
+{
+ unsigned int i;
+
+ for (i = 0; i < data->num_attrs; i++)
+ kfree(data->attrs[i].text);
+ for (i = 0; i < hwmon_max; i++)
+ kfree(data->channels[i].config);
+ kvfree(data->attrs);
+ kfree(data->text);
+ kfree(data->name);
+ kfree(data);
+}
+
+static bool uhwmon_same_schema(struct uhwmon_device *a, struct uhwmon_device *b)
+{
+ unsigned int i;
+
+ if (a->num_attrs != b->num_attrs)
+ return false;
+ for (i = 0; i < a->num_attrs; i++) {
+ struct uhwmon_attr *x = &a->attrs[i], *y = &b->attrs[i];
+
+ if (memcmp(&x->spec, &y->spec, sizeof(x->spec)) ||
+ (x->text && strcmp(x->text, y->text)))
+ return false;
+ }
+ return true;
+}
+
+static void uhwmon_disconnect(struct uhwmon_device *data)
+{
+ mutex_lock(&data->lock);
+ data->online = false;
+ data->readable = false;
+ if (data->pending && !data->pending->done) {
+ data->pending->result =
+ data->pending->request.op == UHWMON_READ ? -ENODATA :
+ -ENODEV;
+ data->pending->done = true;
+ }
+ mutex_unlock(&data->lock);
+ wake_up_all(&data->wait);
+}
+
+static void uhwmon_unregister(struct uhwmon_device *data)
+{
+ uhwmon_disconnect(data);
+ hwmon_device_unregister(data->hwmon);
+ data->destroyed = true;
+}
+
+static struct miscdevice uhwmon_misc;
+
+static int uhwmon_create(struct file *file, void __user *arg)
+{
+ struct uhwmon_device *data, *existing;
+ struct uhwmon_attribute *spec = NULL;
+ struct uhwmon_create create;
+ u32 counts[hwmon_max] = {}, text_size = 0;
+ unsigned int i, j, n = 0;
+ int ret, total = 0;
+
+ if (file->private_data)
+ return -EBUSY;
+ if (copy_from_user(&create, arg, sizeof(create)))
+ return -EFAULT;
+ if (!create.num_attrs || create.reserved)
+ return -EINVAL;
+ if (create.num_attrs >= INT_MAX)
+ return -EOVERFLOW;
+ data = kzalloc_obj(*data);
+ if (!data)
+ return -ENOMEM;
+ mutex_init(&data->lock);
+ init_waitqueue_head(&data->wait);
+ data->name = strndup_user(u64_to_user_ptr(create.name), NAME_MAX + 1);
+ if (IS_ERR(data->name)) {
+ ret = PTR_ERR(data->name);
+ data->name = NULL;
+ goto out;
+ }
+ ret = -EINVAL;
+ if (!*data->name)
+ goto out;
+ for (i = 0; data->name[i]; i++)
+ if (hwmon_is_bad_char(data->name[i]))
+ goto out;
+ data->attrs =
+ kvcalloc(create.num_attrs, sizeof(*data->attrs), GFP_KERNEL);
+ if (!data->attrs) {
+ ret = -ENOMEM;
+ goto out;
+ }
+ data->num_attrs = create.num_attrs;
+ spec = memdup_array_user(u64_to_user_ptr(create.attrs),
+ create.num_attrs, sizeof(*spec));
+ if (IS_ERR(spec)) {
+ ret = PTR_ERR(spec);
+ spec = NULL;
+ goto out;
+ }
+ for (i = 0; i < data->num_attrs; i++) {
+ struct uhwmon_attr *attr = &data->attrs[i];
+ u32 type;
+ bool string;
+
+ ret = -EINVAL;
+ if (spec[i].attr >= 32 ||
+ !hwmon_is_valid_attr(spec[i].type, spec[i].attr) ||
+ !spec[i].mode || spec[i].mode & ~0644 ||
+ spec[i].flags & ~UHWMON_ATTR_CONSTANT)
+ goto out;
+ if (spec[i].type == hwmon_chip && spec[i].channel)
+ goto out;
+ /*
+ * N is a zero-based channel index: channels 0..N need at least
+ * N + 1 attribute slots, plus a NULL terminator. The core uses
+ * int counts, so N + 2 <= INT_MAX. The full count is checked below.
+ */
+ if (spec[i].channel > INT_MAX - 2) {
+ ret = -EOVERFLOW;
+ goto out;
+ }
+ string = hwmon_is_string_attr(spec[i].type, spec[i].attr);
+ if (string) {
+ if (!spec[i].size || spec[i].size > PAGE_SIZE ||
+ spec[i].mode & 0222 || spec[i].value)
+ goto out;
+ } else if (spec[i].size || spec[i].text) {
+ goto out;
+ }
+ if (spec[i].flags & UHWMON_ATTR_CONSTANT) {
+ if (spec[i].mode & 0222)
+ goto out;
+ if (string) {
+ attr->text = strndup_user(
+ u64_to_user_ptr(spec[i].text),
+ spec[i].size);
+ if (IS_ERR(attr->text)) {
+ ret = PTR_ERR(attr->text);
+ attr->text = NULL;
+ goto out;
+ }
+ } else if (spec[i].type != hwmon_energy64 &&
+ (spec[i].value < LONG_MIN ||
+ spec[i].value > LONG_MAX)) {
+ ret = -ERANGE;
+ goto out;
+ }
+ } else {
+ if (spec[i].value || spec[i].text)
+ goto out;
+ if (string)
+ text_size = max(text_size, spec[i].size);
+ }
+ type = spec[i].type == hwmon_energy64 ? hwmon_energy :
+ spec[i].type;
+ for (j = 0; j < i; j++) {
+ u32 other_type = spec[j].type == hwmon_energy64 ?
+ hwmon_energy :
+ spec[j].type;
+
+ if (type == other_type &&
+ spec[i].attr == spec[j].attr &&
+ spec[i].channel == spec[j].channel)
+ goto out;
+ }
+ attr->spec = spec[i];
+ attr->spec.text = 0;
+ counts[spec[i].type] =
+ max(counts[spec[i].type], spec[i].channel + 1);
+ }
+ list_for_each_entry(existing, &uhwmon_devices, node) {
+ if (strcmp(existing->name, data->name))
+ continue;
+ ret = -EBUSY;
+ if (existing->online)
+ goto out;
+ ret = -EINVAL;
+ if (!uhwmon_same_schema(existing, data))
+ goto out;
+ mutex_lock(&existing->lock);
+ existing->online = true;
+ mutex_unlock(&existing->lock);
+ /* Publish initialized state to concurrent read/write/poll calls. */
+ smp_store_release(&file->private_data, existing);
+ ret = 0;
+ goto out;
+ }
+ for (i = 0; i < hwmon_max; i++) {
+ u32 *config;
+
+ if (!counts[i])
+ continue;
+ config = kcalloc(counts[i] + 1, sizeof(*config), GFP_KERNEL);
+ if (!config) {
+ ret = -ENOMEM;
+ goto out;
+ }
+ data->channels[i].type = i;
+ data->channels[i].config = config;
+ data->info[n++] = &data->channels[i];
+ for (j = 0; j < data->num_attrs; j++)
+ if (spec[j].type == i)
+ config[spec[j].channel] |= BIT(spec[j].attr);
+ for (j = 0; j < counts[i]; j++) {
+ /* Zero terminates the array; is_visible() hides gaps. */
+ if (!config[j])
+ config[j] = BIT(0);
+ if (check_add_overflow(total, hweight32(config[j]),
+ &total) ||
+ total == INT_MAX) {
+ ret = -EOVERFLOW;
+ goto out;
+ }
+ }
+ }
+ if (text_size) {
+ data->text = kmalloc(text_size, GFP_KERNEL);
+ if (!data->text) {
+ ret = -ENOMEM;
+ goto out;
+ }
+ }
+ data->chip.ops = &uhwmon_ops;
+ data->chip.info = data->info;
+ data->hwmon = hwmon_device_register_with_info(
+ uhwmon_misc.this_device, data->name, data, &data->chip, NULL);
+ if (IS_ERR(data->hwmon)) {
+ ret = PTR_ERR(data->hwmon);
+ goto out;
+ }
+ list_add(&data->node, &uhwmon_devices);
+ mutex_lock(&data->lock);
+ data->online = true;
+ mutex_unlock(&data->lock);
+ /* Publish initialized state to concurrent read/write/poll calls. */
+ smp_store_release(&file->private_data, data);
+ kfree(spec);
+ return 0;
+out:
+ kfree(spec);
+ uhwmon_free(data);
+ return ret;
+}
+
+static ssize_t uhwmon_read(struct file *file, char __user *buf, size_t count,
+ loff_t *offset)
+{
+ /* Paired with CREATE's publication of the initialized device. */
+ struct uhwmon_device *data = smp_load_acquire(&file->private_data);
+ int ret;
+
+ if (!data)
+ return -ENODEV;
+ if (count < sizeof(struct uhwmon_request))
+ return -EINVAL;
+ for (;;) {
+ if (mutex_lock_interruptible(&data->lock))
+ return -ERESTARTSYS;
+ if (!data->online) {
+ ret = -ENODEV;
+ goto out;
+ }
+ if (data->readable)
+ break;
+ mutex_unlock(&data->lock);
+ if (file->f_flags & O_NONBLOCK)
+ return -EAGAIN;
+ ret = wait_event_interruptible(
+ data->wait,
+ !READ_ONCE(data->online) || READ_ONCE(data->readable));
+ if (ret)
+ return ret;
+ }
+ ret = -EFAULT;
+ if (copy_to_user(buf, &data->pending->request,
+ sizeof(data->pending->request)))
+ goto out;
+ data->readable = false;
+ ret = sizeof(data->pending->request);
+out:
+ mutex_unlock(&data->lock);
+ return ret;
+}
+
+static ssize_t uhwmon_write(struct file *file, const char __user *buf,
+ size_t count, loff_t *offset)
+{
+ /* Paired with CREATE's publication of the initialized device. */
+ struct uhwmon_device *data = smp_load_acquire(&file->private_data);
+ struct uhwmon_pending *pending;
+ struct uhwmon_reply reply;
+ size_t size, length;
+ ssize_t ret;
+
+ if (!data)
+ return -ENODEV;
+ if (count < sizeof(reply))
+ return -EINVAL;
+ if (copy_from_user(&reply, buf, sizeof(reply)))
+ return -EFAULT;
+ if (reply.reserved || reply.status > 0 || reply.status <= -ERESTARTSYS)
+ return -EINVAL;
+ mutex_lock(&data->lock);
+ pending = data->pending;
+ ret = -ENODEV;
+ if (!data->online)
+ goto out;
+ ret = -ESTALE;
+ if (!pending || pending->request.id != reply.id)
+ goto out;
+ ret = -EALREADY;
+ if (pending->done)
+ goto out;
+ ret = -EINVAL;
+ if (data->readable)
+ goto out;
+ size = data->attrs[pending->request.attr].spec.size;
+ if (!reply.status && pending->request.op == UHWMON_READ) {
+ if (size) {
+ if (count < sizeof(reply) + size)
+ goto out;
+ ret = -EFAULT;
+ if (copy_from_user(data->text, buf + sizeof(reply),
+ size))
+ goto out;
+ length = strnlen(data->text, size);
+ ret = -EINVAL;
+ if (length == size)
+ goto out;
+ } else {
+ *pending->value = reply.value;
+ }
+ } else {
+ size = 0;
+ }
+ pending->result = reply.status;
+ pending->done = true;
+ ret = sizeof(reply) + size;
+out:
+ mutex_unlock(&data->lock);
+ wake_up_all(&data->wait);
+ return ret;
+}
+
+static __poll_t uhwmon_poll(struct file *file, poll_table *wait)
+{
+ /* Paired with CREATE's publication of the initialized device. */
+ struct uhwmon_device *data = smp_load_acquire(&file->private_data);
+ __poll_t events = 0;
+
+ if (!data)
+ return EPOLLERR;
+ poll_wait(file, &data->wait, wait);
+ guard(mutex)
+ (&data->lock);
+ if (!data->online)
+ return EPOLLHUP | EPOLLERR;
+ if (data->readable)
+ events |= EPOLLIN | EPOLLRDNORM;
+ return events;
+}
+
+static long uhwmon_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
+{
+ struct uhwmon_device *data;
+ const struct uhwmon_attribute *attr;
+ u32 index;
+
+ guard(mutex)
+ (&uhwmon_control_lock);
+ data = file->private_data;
+ if (cmd == UHWMON_CREATE)
+ return uhwmon_create(file, (void __user *)arg);
+ if (cmd != UHWMON_DESTROY && cmd != UHWMON_NOTIFY)
+ return -ENOTTY;
+ if (!data || data->destroyed)
+ return -ENODEV;
+ if (cmd == UHWMON_DESTROY) {
+ uhwmon_unregister(data);
+ list_del(&data->node);
+ return 0;
+ }
+ if (copy_from_user(&index, (void __user *)arg, sizeof(index)))
+ return -EFAULT;
+ if (index >= data->num_attrs)
+ return -EINVAL;
+ attr = &data->attrs[index].spec;
+ return hwmon_notify_event(data->hwmon, attr->type, attr->attr,
+ attr->channel);
+}
+
+static int uhwmon_open(struct inode *inode, struct file *file)
+{
+ file->private_data = NULL;
+ return stream_open(inode, file);
+}
+
+static int uhwmon_release(struct inode *inode, struct file *file)
+{
+ struct uhwmon_device *data;
+
+ guard(mutex)
+ (&uhwmon_control_lock);
+ data = file->private_data;
+ if (data) {
+ uhwmon_disconnect(data);
+ if (data->destroyed)
+ uhwmon_free(data);
+ }
+ return 0;
+}
+
+static const struct file_operations uhwmon_fops = {
+ .owner = THIS_MODULE,
+ .open = uhwmon_open,
+ .release = uhwmon_release,
+ .read = uhwmon_read,
+ .write = uhwmon_write,
+ .poll = uhwmon_poll,
+ .unlocked_ioctl = uhwmon_ioctl,
+ .compat_ioctl = compat_ptr_ioctl,
+};
+
+static struct miscdevice uhwmon_misc = {
+ .minor = MISC_DYNAMIC_MINOR,
+ .name = "uhwmon",
+ .fops = &uhwmon_fops,
+ .mode = 0600,
+};
+
+static int __init uhwmon_init(void)
+{
+ return misc_register(&uhwmon_misc);
+}
+
+static void __exit uhwmon_exit(void)
+{
+ struct uhwmon_device *data, *next;
+
+ misc_deregister(&uhwmon_misc);
+ list_for_each_entry_safe(data, next, &uhwmon_devices, node) {
+ uhwmon_unregister(data);
+ list_del(&data->node);
+ uhwmon_free(data);
+ }
+}
+
+module_init(uhwmon_init);
+module_exit(uhwmon_exit);
+
+MODULE_AUTHOR("Jihong Min <hurryman2212@gmail.com>");
+MODULE_DESCRIPTION("Generic hwmon interface for userspace implementations");
+MODULE_LICENSE("GPL");
diff --git a/include/uapi/linux/uhwmon.h b/include/uapi/linux/uhwmon.h
new file mode 100644
index 000000000000..8bc499646f49
--- /dev/null
+++ b/include/uapi/linux/uhwmon.h
@@ -0,0 +1,64 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+/*
+ * Copyright (C) 2026 Jihong Min <hurryman2212@gmail.com>
+ */
+
+#ifndef _UAPI_LINUX_UHWMON_H
+#define _UAPI_LINUX_UHWMON_H
+
+#include <linux/hwmon.h>
+#include <linux/ioctl.h>
+#include <linux/types.h>
+
+#define UHWMON_ATTR_CONSTANT (1U << 0)
+
+/*
+ * type and attr are hwmon enum identifiers, not HWMON_* bitmasks. channel is
+ * zero-based. Numeric attributes use size = 0; labels use a NUL-inclusive size
+ * in 1..PAGE_SIZE and omit the sysfs newline. Only constants set value or text.
+ */
+struct uhwmon_attribute {
+ __u32 type;
+ __u32 attr;
+ __u32 channel;
+ __u32 mode;
+ __u32 flags;
+ __u32 size;
+ __aligned_s64 value;
+ __aligned_u64 text;
+};
+
+/* name points to a NUL-terminated chip name; reserved must be zero. */
+struct uhwmon_create {
+ __aligned_u64 name;
+ __aligned_u64 attrs;
+ __u32 num_attrs;
+ __u32 reserved;
+};
+
+#define UHWMON_READ 1
+#define UHWMON_WRITE 2
+
+/* attr indexes the registration array; value carries a numeric write. */
+struct uhwmon_request {
+ __aligned_u64 id;
+ __u32 attr;
+ __u32 op;
+ __aligned_s64 value;
+};
+
+/* Only successful label reads append size bytes of NUL-terminated text. */
+struct uhwmon_reply {
+ __aligned_u64 id;
+ __s32 status;
+ __u32 reserved;
+ __aligned_s64 value;
+ char text[];
+};
+
+/* CREATE also reconnects an identical, currently unowned device. */
+#define UHWMON_CREATE _IOW(0xb9, 0, struct uhwmon_create)
+#define UHWMON_NOTIFY _IOW(0xb9, 1, __u32)
+#define UHWMON_DESTROY _IO(0xb9, 2)
+
+#endif /* _UAPI_LINUX_UHWMON_H */
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH 4/5] hwmon: Reject truncated attribute names
2026-10-09 15:38 [PATCH 0/5] hwmon: Introduce uhwmon for userspace implementations Jihong Min
` (2 preceding siblings ...)
2026-10-09 15:38 ` [PATCH 3/5] hwmon: Add a generic interface for userspace implementations Jihong Min
@ 2026-10-09 15:38 ` Jihong Min
2026-10-09 15:38 ` [PATCH 5/5] docs: hwmon: Document the uhwmon userspace interface Jihong Min
4 siblings, 0 replies; 6+ messages in thread
From: Jihong Min @ 2026-10-09 15:38 UTC (permalink / raw)
To: Guenter Roeck, linux-hwmon
Cc: Jonathan Corbet, Shuah Khan, Randy Dunlap, linux-api, linux-doc,
linux-kernel
Large channel numbers can produce truncated or conflicting attribute
names. Return -EOVERFLOW if a generated name does not fit its buffer.
Assisted-by: Codex:gpt-6-astra
Signed-off-by: Jihong Min <hurryman2212@gmail.com>
---
drivers/hwmon/hwmon.c | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/drivers/hwmon/hwmon.c b/drivers/hwmon/hwmon.c
index bed16cfd4f1c..288f356980e4 100644
--- a/drivers/hwmon/hwmon.c
+++ b/drivers/hwmon/hwmon.c
@@ -553,8 +553,12 @@ static const struct attribute *hwmon_genattr(const void *drvdata,
if (type == hwmon_chip) {
name = template;
} else {
- scnprintf(hattr->name, sizeof(hattr->name), template,
- index + hwmon_attr_base(type));
+ if (snprintf(hattr->name, sizeof(hattr->name), template,
+ index + hwmon_attr_base(type)) >=
+ sizeof(hattr->name)) {
+ kfree(hattr);
+ return ERR_PTR(-EOVERFLOW);
+ }
name = hattr->name;
}
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH 5/5] docs: hwmon: Document the uhwmon userspace interface
2026-10-09 15:38 [PATCH 0/5] hwmon: Introduce uhwmon for userspace implementations Jihong Min
` (3 preceding siblings ...)
2026-10-09 15:38 ` [PATCH 4/5] hwmon: Reject truncated attribute names Jihong Min
@ 2026-10-09 15:38 ` Jihong Min
4 siblings, 0 replies; 6+ messages in thread
From: Jihong Min @ 2026-10-09 15:38 UTC (permalink / raw)
To: Guenter Roeck, linux-hwmon
Cc: Jonathan Corbet, Shuah Khan, Randy Dunlap, linux-api, linux-doc,
linux-kernel
Document the uhwmon UAPI for developers implementing userspace hwmon
drivers. Cover device registration, request handling, notifications
and device lifetime.
Assisted-by: Codex:gpt-6-astra
Signed-off-by: Jihong Min <hurryman2212@gmail.com>
---
Documentation/hwmon/index.rst | 1 +
Documentation/hwmon/uhwmon.rst | 141 +++++++++++++++++++++++++++++++++
2 files changed, 142 insertions(+)
create mode 100644 Documentation/hwmon/uhwmon.rst
diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst
index 9955a525436a..88c6388e7c5c 100644
--- a/Documentation/hwmon/index.rst
+++ b/Documentation/hwmon/index.rst
@@ -12,6 +12,7 @@ Hardware Monitoring
submitting-patches
sysfs-interface
userspace-tools
+ uhwmon
Hardware Monitoring Kernel Drivers
==================================
diff --git a/Documentation/hwmon/uhwmon.rst b/Documentation/hwmon/uhwmon.rst
new file mode 100644
index 000000000000..155f1a965d16
--- /dev/null
+++ b/Documentation/hwmon/uhwmon.rst
@@ -0,0 +1,141 @@
+.. SPDX-License-Identifier: GPL-2.0-only
+
+Userspace hardware monitoring
+=============================
+
+Copyright (C) 2026 Jihong Min <hurryman2212@gmail.com>
+
+``uhwmon`` connects userspace drivers to ``hwmon_ops`` through the control
+device ``/dev/uhwmon``. The hwmon core creates and formats sysfs attributes.
+The daemon supplies values in the units defined by :doc:`sysfs-interface`.
+
+Enable ``CONFIG_HWMON`` and ``CONFIG_SENSORS_UHWMON`` and load ``uhwmon``.
+Include ``<linux/uhwmon.h>``.
+
+Register a device
+-----------------
+
+Open ``/dev/uhwmon`` with ``O_RDWR`` for each device. Zero-initialize all
+UAPI structures.
+
+Call ``UHWMON_CREATE`` with the NUL-terminated chip name, attribute array and
+its count in ``struct uhwmon_create``. Each ``struct uhwmon_attribute`` contains:
+
+* ``type``, ``attr``: hwmon enum IDs, not ``HWMON_*`` bitmasks.
+* ``channel``: zero-based index; zero for chip attributes.
+* ``mode``: nonzero permissions using only bits from 0644.
+* ``flags``: zero, or ``UHWMON_ATTR_CONSTANT`` for read-only constants.
+* ``value``: a constant number, otherwise zero.
+* ``text``: pointer to a constant label, otherwise zero.
+* ``size``: NUL-inclusive label capacity from 1 to ``PAGE_SIZE``; zero
+ for numbers.
+
+Labels are read-only, NUL-terminated and omit the trailing newline.
+
+Handle requests
+---------------
+
+Read a ``struct uhwmon_request`` and write a ``struct uhwmon_reply`` on the
+owning file. Request and reply headers are 24 bytes. Only successful label
+replies append ``size`` bytes of text. Buffers smaller than the message are
+rejected with ``EINVAL``.
+
+``struct uhwmon_request``:
+
+* ``id``: request identifier; copy it into the reply.
+* ``attr``: index in the registered attribute array.
+* ``op``: ``UHWMON_READ`` or ``UHWMON_WRITE``.
+* ``value``: number to apply for a write; zero for a read.
+
+``struct uhwmon_reply``:
+
+* ``id``: the request's identifier.
+* ``status``: zero on success or a negative errno; use ``-ENODATA`` for
+ unavailable or stale readings. Return ``-EINTR`` only before applying a write.
+* ``reserved``: zero.
+* ``value``: successful numeric read result; ignored otherwise.
+* ``text``: NUL-terminated result of a successful label read; absent otherwise.
+
+Numbers use signed 64-bit fields and must fit the kernel's ``long``,
+except for energy64 reads.
+Discard replies rejected with ``ESTALE`` (canceled request).
+
+This example registers ``temp1_input`` and ``temp1_enable`` and reports
+42000 millidegrees Celsius while enabled:
+
+.. code-block:: c
+
+ struct uhwmon_attribute attrs[] = {
+ { .type = hwmon_temp, .attr = hwmon_temp_input, .mode = 0444 },
+ { .type = hwmon_temp, .attr = hwmon_temp_enable, .mode = 0644 },
+ };
+ struct uhwmon_create create = {
+ .name = (uintptr_t)"example",
+ .attrs = (uintptr_t)attrs,
+ .num_attrs = 2,
+ };
+ struct uhwmon_request req;
+ struct uhwmon_reply reply = {};
+ int enabled = 1;
+
+ if (ioctl(fd, UHWMON_CREATE, &create) < 0)
+ return;
+
+ for (;;) {
+ if (read(fd, &req, sizeof(req)) < 0) {
+ if (errno == EINTR)
+ continue;
+ break;
+ }
+ reply.id = req.id;
+ reply.status = -EINVAL;
+ if (req.attr == 0 && req.op == UHWMON_READ) {
+ reply.value = 42000;
+ reply.status = enabled ? 0 : -ENODATA;
+ } else if (req.attr == 1 && req.op == UHWMON_READ) {
+ reply.value = enabled;
+ reply.status = 0;
+ } else if (req.attr == 1 && req.op == UHWMON_WRITE &&
+ (req.value == 0 || req.value == 1)) {
+ enabled = req.value;
+ reply.status = 0;
+ }
+ if (write(fd, &reply, sizeof(reply)) < 0 && errno != ESTALE)
+ break; // Fatal error.
+ }
+
+Disconnect and reconnect
+------------------------
+
+Sysfs requests can be interrupted or restarted before delivery to the daemon.
+After delivery, they wait for a reply or disconnection; only fatal signals
+can interrupt this wait. Completed replies take precedence over signals.
+
+Releasing the last control file reference disconnects the daemon: dynamic
+reads return ``ENODATA``, writes return ``ENODEV``, and constants remain readable.
+Closing an fd does not cancel I/O in another thread; duplicated fds retain
+ownership. Interrupt blocked control reads or use ``UHWMON_DESTROY``.
+
+The device persists after disconnection. To reconnect, open a new control fd
+and register the same name and attribute array, including constant values.
+
+``UHWMON_DESTROY`` removes the device. Close and reopen the control file
+before registering another device.
+
+Notify changes
+--------------
+
+The kernel cannot detect changes to values held by the daemon. After changing
+a value, call ``UHWMON_NOTIFY`` with its 32-bit attribute index. This wakes
+sysfs pollers waiting for ``POLLPRI`` and sends a ``KOBJ_CHANGE`` uevent with
+``NAME=<attribute>``. Applications read the attribute again from offset 0 to
+get the new value. The notification carries no value and does not replace a
+read reply.
+
+For example, after changing ``enabled``, notify listeners of ``temp1_enable``:
+
+.. code-block:: c
+
+ __u32 index = 1; /* attrs[1]: temp1_enable */
+ if (ioctl(fd, UHWMON_NOTIFY, &index) < 0)
+ return;
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-10-09 15:39 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-09 15:38 [PATCH 0/5] hwmon: Introduce uhwmon for userspace implementations Jihong Min
2026-10-09 15:38 ` [PATCH 1/5] hwmon: Export sensor and attribute definitions to userspace Jihong Min
2026-10-09 15:38 ` [PATCH 2/5] hwmon: Expose attribute validation and string helpers Jihong Min
2026-10-09 15:38 ` [PATCH 3/5] hwmon: Add a generic interface for userspace implementations Jihong Min
2026-10-09 15:38 ` [PATCH 4/5] hwmon: Reject truncated attribute names Jihong Min
2026-10-09 15:38 ` [PATCH 5/5] docs: hwmon: Document the uhwmon userspace interface Jihong Min
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®