Gitii/ms-a1-fan-controller
GitHub: Gitii/ms-a1-fan-controller
为 Minisforum MS-A1 提供的 Linux hwmon 内核驱动,通过逆向厂商专有 EC 协议在 Secure Boot 下实现只读的风扇转速与温度传感器数据访问。
Stars: 1 | Forks: 0
# Minisforum MS-A1 EC 风扇 / 温度驱动
已签名的 Linux 内核模块 (`msa1_ec`),通过标准的 `hwmon` sysfs ABI 暴露 Minisforum MS-A1 专有的嵌入式控制器。在 UEFI Secure Boot 和内核 lockdown 下运行,无需 `iopl()`,无需 `/dev/port`,无需用户态 I/O 技巧。
## 为什么需要这个驱动
MS-A1 的 BIOS **并未**通过 ACPI 暴露其风扇/温度 EC —— 没有 `ECDT` 表,没有 `PNP0C09` 设备,且 `/sys/kernel/debug/ec/ec0/io` 永远不会出现。只能通过在非标准端口 `0x6C / 0x68` 上直接进行 I/O,并使用厂商命令 `0xDD`(温度)和 `0xD5`(风扇)来访问该 EC。在 Secure Boot 下,内核运行在 `integrity` lockdown 模式中,这会阻断所有访问这些端口的用户态路径(`iopl`、`/dev/port`、`/dev/mem`)。与 EC 通信的唯一方法是从内核内部进行——因此有了本驱动。在 Secure Boot 下加载它需要使用 MOK (Machine Owner Key) 签名,并一次性注册该密钥。
## 您将获得
```
/sys/class/hwmon/hwmonN/ (where the msa1_ec device lands)
name = msa1_ec
temp1_input CPU DTS temperature (millidegrees C)
temp1_label "CPU"
temp2_input Motherboard temperature
temp2_label "Board"
fan1_input Fan 1 RPM
fan1_label "Fan 1"
fan2_input Fan 2 RPM
fan2_label "Fan 2"
fan3_input Fan 3 RPM
fan3_label "Fan 3"
/sys/kernel/debug/msa1_ec/
dump_d5 Full 256-byte EC dump via cmd 0xD5 (discovery aid)
dump_dd Full 256-byte EC dump via cmd 0xDD
```
所有内容均为只读。在此硬件上**无法**进行风扇/PWM 写入 —— EC 被固件锁定,不向任何 OS 暴露写入路径。有关完整的逆向工程分析,请参阅 [docs/FINDINGS.md](docs/FINDINGS.md)。
## 快速开始
```
# 0. 一次性先决条件
sudo apt install -y dkms build-essential mokutil openssl \
linux-headers-$(uname -r) lm-sensors
# 1. 一次性:生成 + 注册 MOK 签名密钥
sudo make setup-mok
sudo reboot # complete enrollment in the blue MOK Manager screen
# 2. 重启后,验证注册情况
mokutil --list-enrolled | grep -A1 'msa1-ec'
# 3. 通过 DKMS 构建、签名、安装并加载
sudo make dkms-install
# 4. 确认
sensors | sed -n '/msa1_ec/,/^$/p'
make status
```
卸载:
```
sudo modprobe -r msa1_ec
sudo make dkms-uninstall
sudo mokutil --delete /var/lib/shim-signed/mok/msa1.der # optional
```
## 构建与签名对照表
| 命令 | 作用 |
|----------------------|-------------------------------------------------------------------|
| `make` | 针对当前运行的内核进行 kbuild(不安装、不签名) |
| `sudo make sign` | 使用 `/var/lib/shim-signed/mok/msa1.{priv,der}` 对构建的 `.ko` 进行签名 |
| `sudo make install` | 将签名的 `.ko` 放入 `/lib/modules/$(uname -r)/extra/` + depmod |
| `sudo make load` | 重新加载模块,跟踪 dmesg |
| `sudo make reload` | install + load |
| `sudo make setup-mok`| 生成 MOK 密钥对 + 请求注册 |
| `sudo make dkms-install` | 完整的 DKMS 工作流(抵御内核更新) |
| `sudo make dkms-uninstall` | 移除 DKMS 包 + 框架配置 |
| `make status` | 单页健康状态摘要 |
| `make user` | 构建用户态 CLI 读取器 (`ms_a1_ec_read`) |
| `make dsdt` | 提取 + 反编译 DSDT/SSDTs 并 grep 搜索 fan/WMI/SMI 符号 |
| `make clean` | kbuild 清理 |
## 布局
```
.
├── src/
│ ├── msa1_ec.c kernel module (~470 LoC, single file)
│ ├── Kbuild obj-m := msa1_ec.o
│ └── Makefile kbuild wrapper for out-of-tree builds
├── userspace/
│ └── ms_a1_ec_read.c dependency-free CLI reader (reads /sys/class/hwmon)
├── packaging/
│ ├── setup-mok.sh one-time MOK keypair generation + enrollment
│ └── install.sh one-shot DKMS install with sign config
├── docs/
│ └── FINDINGS.md full reverse-engineering writeup (why writes are impossible)
├── dkms.conf DKMS module manifest
├── Makefile top-level convenience targets
├── devbox.json devbox environment (gcc, make, openssl, lm_sensors)
└── LICENSE GPL-2.0
```
## 模块参数
```
sudo modprobe msa1_ec big_endian=1 # try if fan RPMs look wrong (~16k bogus)
sudo modprobe msa1_ec debug=1 # log every EC transaction (very noisy)
sudo modprobe msa1_ec force=1 # skip DMI check - DANGEROUS, see below
sudo modprobe msa1_ec allow_scan=1 # expose the scan_cmds discovery debugfs file
```
| 参数 | 默认值 | 效果 |
|---------------|---------|----------------------------------------------------------------------------------------|
| `big_endian` | 0 (LE) | RPM 字节序(低位偏移存 LSB 与 低位偏移存 MSB)。如果 RPM 看起来不对,请切换此项。 |
| `debug` | 0 | 详细的每笔事务 `dev_info` 日志记录。 |
| `force` | 0 | 绕过 DMI 限制。**会在任何系统上写入 0x6C/0x68** - 在非 MS-A1 上请保持关闭。 |
| `allow_scan` | 0 | 暴露 `/sys/kernel/debug/msa1_ec/scan_cmds`。请参阅下文的 *发现辅助工具*。 |
## 发现辅助工具
该模块提供了三个 debugfs 文件,用于逆向工程 EC 的命令集(主要用于构建风扇控制 / Phase 6):
| 文件 | 总是存在? | 作用 |
|--------------|-----------------|------------------------------------------------------------------------------|
| `dump_d5` | 是 | 使用命令 `0xD5`(已知读取风扇)进行 256 字节偏移扫描。显示风扇命令会对哪些偏移量做出响应。 |
| `dump_dd` | 是 | 使用命令 `0xDD`(已知读取温度)进行 256 字节偏移扫描。 |
| `scan_cmds` | 仅当 `allow_scan=1` 时 | 在少数已知及探索性偏移量上扫描约 45 个候选命令字节。识别 EC 对哪些命令字节做出 ACK。 |
```
# 持续转储:
sudo cat /sys/kernel/debug/msa1_ec/dump_d5
sudo cat /sys/kernel/debug/msa1_ec/dump_dd
# Command-byte 扫描(需要主动选择启用,请参阅下方的安全说明):
sudo modprobe -r msa1_ec
sudo modprobe msa1_ec allow_scan=1
sudo cat /sys/kernel/debug/msa1_ec/scan_cmds
```
### `scan_cmds` 安全性
扫描器仅执行**读取**操作(cmd, offset, 等待 OBF, 读取数据)—— 它永远不会
发出语义未知的写入操作。它还会**强制跳过**这些
已知的会执行写入/状态更改的 ACPI EC 操作码:
| 跳过项 | 原因 |
|---------|-----|
| `0x81` | ACPI WR_EC — 将值字节写入 EC RAM |
| `0x82` | ACPI Burst Enable — 更改 EC 模式 |
| `0x83` | ACPI Burst Disable — 更改 EC 模式 |
即便如此,如果未知的固件以不同方式解释我们的
`offset` 字节,对其进行“读取”探测也可能产生副作用。因此,扫描器
**默认关闭**。将其输出视为一次性诊断结果,然后卸载模块,并在不带 `allow_scan=1` 的情况下重新加载以进行正常操作。
## 验证
执行 `modprobe msa1_ec` 后:
```
# 1. 模块已加载,签名已被接受(内核必须未被 taint)
cat /proc/sys/kernel/tainted # -> 0
lsmod | grep msa1_ec
dmesg | grep msa1_ec # -> "EC probe OK, CPU = X.YYY °C", "loaded"
# 2. hwmon 设备存在
ls /sys/class/hwmon/ | while read h; do
[ "$(cat /sys/class/hwmon/$h/name 2>/dev/null)" = "msa1_ec" ] && echo $h
done
# 3. 实时读数
sensors | sed -n '/msa1_ec/,/^$/p'
# 4. RPM 跟踪负载(在第二个终端中运行):
sudo apt install -y stress
stress --cpu 8 --timeout 60 &
watch -n1 'sensors | grep -A6 msa1_ec'
# 5. 原始 EC 转储 - 对于发现写入寄存器非常有用
sudo cat /sys/kernel/debug/msa1_ec/dump_d5
sudo cat /sys/kernel/debug/msa1_ec/dump_dd
```
如果 `tainted` 不为 0,或者您在 dmesg 中看到 `Loading of unsigned module is rejected`,说明签名链已断开 —— 请参阅 [故障排除](#troubleshooting)。
## EC 协议参考
从 [AIDA64 论坛帖子][aida-thread] 逆向工程而来,由
JaxJiang 于 2025-10-15 发布。
```
Transport (x86 port I/O)
────────────────────────
CMD port = 0x6C (write command byte)
DATA port = 0x68 (write offset / read result)
status @ 0x6C, bit 0 OBF: data available
status @ 0x6C, bit 1 IBF: EC busy
Sequence (1-byte read)
──────────────────────
wait IBF=0
outb(cmd, 0x6C) cmd = 0xDD or 0xD5
wait IBF=0
outb(offset, 0x68)
wait OBF=1
val = inb(0x68)
Temperatures via cmd 0xDD (signed °C)
─────────────────────────────────────
0x20 -> CPU DTS temperature
0x21 -> motherboard temperature
Fan RPM via cmd 0xD5 (2 bytes per fan)
──────────────────────────────────────
0x14 / 0x15 -> fan3 (low / high byte)
0x16 / 0x17 -> fan2
0x18 / 0x19 -> fan1
```
## 为什么用户态 I/O 不起作用
那些显而易见的方法 —— `iopl(3)` + `inb`/`outb`,或者 `/dev/port` —— 都被
内核随 Secure Boot 提供的 **integrity lockdown** 模式阻止了:
- `iopl(3)` / `ioperm()` -> 被阻止
- `/dev/port` 读写 -> 被阻止
- `/dev/mem`, `/dev/kmem` -> 被阻止
- `ec_sys` debugfs 后备方案本应该有效,但 MS-A1 根本没有暴露
ACPI EC,因此永远不会创建 `/sys/kernel/debug/ec/ec0/io`
注册 MOK 密钥**不会**解除 lockdown —— 这是一个常见的
误解。MOK 只是让内核信任您签名的模块。
唯一可行的路径是从内核空间执行端口 I/O,这
正是 `msa1_ec` 所做的。
## 故障排除
### `modprobe: ERROR: could not insert 'msa1_ec': Required key not available`
内核拒绝了模块签名。
- 检查 MOK 是否已注册:`mokutil --list-enrolled | grep -A1 msa1-ec`
- 重新签名:`sudo make sign && sudo make install && sudo make load`
### `not running on a Minisforum MS-A1; not loading`
DMI 不匹配。请使用以下命令验证:
```
cat /sys/class/dmi/id/sys_vendor # expect: Micro Computer (HK) Tech Limited
cat /sys/class/dmi/id/product_name # expect: MS-A1
```
如果您确定这是一台具有不同 DMI 字符串的 MS-A1,请在
`src/msa1_ec.c` -> `msa1_dmi_table` 中添加匹配项并重新编译。
### 风扇报告 ~16000 RPM 或无意义的值
字节序可能错误。请切换字节顺序:
```
sudo modprobe -r msa1_ec
sudo modprobe msa1_ec big_endian=1
```
### `implausible CPU temp -128 m°C — refusing to register`
EC 实际上并没有响应。可能的原因:
- 在非 MS-A1 硬件上加载了 `force=1`
- BIOS 更改了 EC 协议(检查 BIOS 版本)
- EC 处于异常状态 - 尝试 `sudo make unload && sudo make load`
### 内核更新后模块消失
如果没有 DKMS,`.ko` 将绑定到某一个内核版本。
请使用 DKMS 安装路径(`sudo make dkms-install`),这样每次内核升级时 DKMS 都会重新构建
+ 重新签名模块。
## 为什么没有风扇控制
我们尝试过 —— 进行了穷尽测试。包含 308 次探测的命令字节扫描、完整的 DSDT/SSDT
反编译、WMI 命名空间检查以及 SMI/SMM 分析都证实
MS-A1 的风扇控制器已被**固件锁定**:EC 仅暴露了两个读取
命令,固件没有 ACPI 风扇方法,AMD OverDrive WMI 命名空间
是个占位符,唯一的写入路径是通过专有且未公开文档的 SMI
处理程序。没有任何 OS —— 无论是 Linux 还是 Windows —— 能够控制这些风扇。
完整的调查记录在 **[docs/FINDINGS.md](docs/FINDINGS.md)** 中。
如需间接控制(通过 `amd-pstate` 调整 CPU 功率)或硬件级
控制(外部微控制器),请参阅
[替代方案部分](docs/FINDINGS.md#5-what-is-possible)。
## 安全性
- DMI 限制可防止在其他系统上意外加载。除非您准备好
对那些系统的键盘控制器造成干扰(EC 端口与 i8042 窗口重叠),否则不要
禁用它(`force=1`)。
- `scan_cmds`(通过 `allow_scan=1` 启用)会探测未知的 EC 命令字节。
它仅执行读取并强制跳过已知的写入类 ACPI 命令,但请将其视为一种
诊断工具,并在正常使用时重新加载且不带 `allow_scan`。
## 许可证
GPL-2.0。请参阅每个源文件中的 SPDX 头文件。
标签:Linux内核驱动, Secure Boot, 云资产清单, 传感器, 客户端加密, 嵌入式控制器, 硬件监控, 逆向工程