mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [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®