* [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; 7+ 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] 7+ 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-10 14:50 ` Markus Elfring
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, 1 reply; 7+ 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] 7+ messages in thread