Skip to content

os: fans and power profile inside the image, on the boards we know - #550

Merged
bong-water-water-bong merged 3 commits into
mainfrom
1bit/os-fans
Oct 24, 2026
Merged

bong-water-water-bong merged 3 commits into
mainfrom
1bit/os-fans

Conversation

@bong-water-water-bong

@bong-water-water-bong bong-water-water-bong commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

The appliance had no fan or power control at all (grep -rn 'fan|power_mode' os/ was empty), so on a
board with an embedded controller it ran on the firmware's fan curve, which is conservative, and reported no
fan readings or temperatures of its own. This brings the development box's setup into the image for the
boards it knows
, and writes nothing anywhere else.

os/mini/board.sh (installed as /usr/sbin/1bit-board, BusyBox sh, no Python, no systemd):

  • the EC fan curve (mode=curve plus the ramp-up/ramp-down trip points, which the EC applies on its own sensor
    with hysteresis);
  • a predictive boost loop, 0.5 s poll: package power ≥ 35 W or GPU busy ≥ 40 % or Tctl ≥ 70 °C or a
    3 °C rise per poll → the CPU fans at level 4; back to the curve after 20 s quiet; level 5 only above 82 °C,
    held to 76 °C;
  • the power profile: the EC power mode first (a mode write resets the SMU limits), then the SMU limits through
    ryzenadj, then the fabric/SoC clock pins — every value overridable from /etc/1bit/power, SKIP_POWER=1 to
    leave power alone;
  • driver only: it never touches debug registers.

Board match is DMI based (BeyondMax Series / AXB35-02). On any other board every command exits without
writing a byte, status says fans: not controlled (this board is not one we know; the firmware curve stays),
and the boot banner says the same.

Deterministic by construction. os/mini/mkimage.sh builds ec_su_axb35 for the image kernel from the
source this repository ships (os/licenses/ec_su_axb35, GPL-2.0, verbatim upstream f62c2c22, built in a temp
copy so the tree stays clean) and builds ryzenadj from the pinned source in os/licenses/ryzenadj
(LGPL-3.0, tag v0.19.0, commit ea71739b) rather than depending on what the build machine has. Both source
trees and licences are installed into the image under /usr/share/licenses/1bit-os/, so every binary travels
with its corresponding source. A failed build fails the image build loudly; EC_MODULE=skip ships without the
driver.

Two Secure Boot traps, both handled:

  1. Module signing. An out-of-tree module must be signed by a key the kernel trusts, and it has to be the
    MOK: the "Secure Boot Kernel" certificate carries no module-signing OID (verified on the development box —
    amdxdna.ko and ec_su_axb35.ko are both signed by strixhalo Secure Boot MOK, and only MOK.der has
    1.3.6.1.4.1.2312.16.1.2). mkimage.sh now signs the module with MOK.priv/MOK.der through the kernel's
    scripts/sign-file (MODULE_SIGN_KEY/MODULE_SIGN_CERT override) and warns loudly if it cannot.
  2. The SMU cap and lockdown. ryzenadj writes the SMU mailbox through /dev/mem, which lockdown
    (integrity) refuses. On the development box that never bites because the kernel does not enter lockdown from
    Secure Boot: /sys/kernel/security/lockdown reads [none] and CONFIG_LOCK_DOWN_IN_EFI_SECURE_BOOT is not
    set in the shipped kernel config — the same kernel the image carries. If a machine ever does refuse the write
    (lockdown forced, no /dev/mem, or no tool), 1bit-board power now falls back to the EC's own balanced
    profile (85 W sustained, 120 W burst)
    instead of leaving the board in performance with no cap, and both
    the banner and power-status say which cap is in force (cap=90/90/105W … vs fallback=balanced …).

os/mini/init.sh: on a known board, load the EC driver, apply the curve and the power profile, start the
boost loop, and report [fans] …, [power] … and temps: Tctl …C EC …C in the banner and in the 1bit.check
self-test. Unknown board → one line, nothing written.

os/mini/release-check.sh: a fans row —

PASS  fans : controlled (fan1 4122 rpm, fan2 4091 rpm at level 4)
SKIP  fans : this board has no EC we drive: the firmware fan curve stays        (also SKIP in a VM)
FAIL  fans : the driver loaded but a fan reads 0 rpm at level 4 (…): the curve and the boost cannot cool this board

The FAIL case is deliberate: it is exactly how a fan failed on our own hardware.

/etc/1bit/power ships as a commented template whose defaults are the development box's profile
(performance mode, 90/90/105 W, level-5 trip at 82 °C unchanged — the appliance is the same board and pad class),
with the fan trip points marked as the first thing to revisit on a hotter unit.

os/LICENSES.md: the board driver (GPL-2.0) and the SMU tool (LGPL-3.0, pinned) with corresponding-source
entries.

Verified on the development box (same board, same kernel): known, curve (identical trip points), status
with live rpms, boost raising the fans on load and releasing to the curve on TERM, power exercised end to
end against a fake EC — including the fallback path (ryzenadj forced to fail → EC mode becomes balanced,
banner says so) — so the real profile was never touched; the /etc/1bit/power parser (comments, spaces,
SKIP_POWER=1, FALLBACK_MODE); tools/copyright.py --check passes; sh -n clean on all four scripts.

Draft on purpose — image boot on hardware pending: the module build, the ryzenadj build and the VM boot test
run once the build lane opens (EC_MODULE=skip builds an image without the driver). No behaviour change on
machines without this board.


Verified by building the image and booting it in QEMU (24 Oct, build lane)

os/mini/mkimage.sh on this board, under a build hold (gpu-hold -l "build pi osfans" -t 30, nice -n 19 taskset -c 0-3), against ~/wt/release-rc/work/build:

step result
board driver ec_su_axb35.ko 33,846 bytes, in /lib/modules/<kver>/updates/, signed by MOK.der — modinfo -F signer says strixhalo Secure Boot MOK
SMU tool ryzenadj v0.19.0 built from os/licenses/ryzenadj/src (68,184 bytes, 6 libs linked) → /opt/1bit/ryzenadj
initramfs contents usr/sbin/1bit-board, etc/1bit/power, the signed .ko, opt/1bit/ryzenadj, and both licence trees with their full source (…/1bit-os/ec_su_axb35/src 31 files, …/1bit-os/ryzenadj/src 47 files)
image 1bit-os.img 2.3 GB, 1bit-os.efi 97 MB, initramfs.img 86 MB

os/mini/vmtest-release.sh (QEMU, KVM, OVMF, the real release check over SSH):

RESULT: PASS (16 checks, no failures)        # image with the driver
SKIP  fans : this board has no EC we drive: the firmware fan curve stays
RESULT: PASS (16 checks, no failures)        # EC_MODULE=skip image
SKIP  fans : this board has no EC we drive: the firmware fan curve stays

i.e. the SKIP path is exercised exactly as intended (no EC in a VM), the EC_MODULE=skip build still boots
and passes, and nothing else regressed (chat, bench, sealed, licences all PASS; chat-hrx, chat-npu,
identity SKIP for the documented VM reasons).

Three things the build lane taught this PR, all fixed here: the image kernel is clang-built, so the module
must build with LLVM=1 and the matching /usr/lib/llvm-<major>/bin on PATH (gcc chokes on
-mretpoline-external-thunk, and LLVM=1 alone dies on ld.lld: not found); the MOK private key is
0600 root, so signing goes through sudo -n when needed and always from a scratch copy (a root-owned
module inside the image tree broke the later chmod/cpio); $out/logs has to exist before the build logs
open.

Still pending on hardware: a boot on the real board with 1bit-release-check (the fans row should PASS
with live rpms there) — that is the owner's step, after the leaderboard is done.

@github-actions

Copy link
Copy Markdown

Preparing review...

@bong-water-water-bong
bong-water-water-bong marked this pull request as draft October 24, 2026 07:49
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Review (orchestrator): DMI-gated, driver-only, no byte written on an unknown board, GPL/LGPL sources shipped with the image, fans row with the dead-fan FAIL case; host checks against a fake EC are the right call while the lane is busy. Decisions: ship the SMU tool pinned (version + source commit recorded in os/, not 'if the builder has it') so the 90 W cap is deterministic; default power file on the known board = the dev box profile. Converted to draft until the module build + VMTEST (QEMU reaches the SKIP path, EC_MODULE=skip image still boots) run at 'lane shared'; the hardware boot + release check is the owner's step after 'leaderboard done'.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

PR Reviewer Guide 🔍

(Review updated until commit f49e7fe)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Possible Issue

The predictive boost loop in boost() function may not correctly handle the case where last temperature is not updated when load is 0. This can lead to incorrect boost behavior when the temperature does not change but other conditions (like power or GPU busy) are met.

now=$(date +%s); load=0
if [ "$w" -ge "$BOOST_W" ] || [ "$busy" -ge "$BOOST_BUSY" ] || [ "$tc" -ge "$BOOST_HOT_C" ] \
   || [ $(( tc - last )) -ge "$BOOST_SLOPE" ]; then load=1; fi
Possible Issue

The power() function uses a hardcoded sleep of 2 seconds after writing the EC power mode. This may not be sufficient for all hardware or may cause unnecessary delays. A more robust method for ensuring the EC has applied the mode should be considered.

sleep 2
Possible Issue

The build process for ec_su_axb35.ko may fail silently if the LLVM=1 flag is not properly set or if the required LLVM toolchain is not found. This could lead to an unsigned module being installed, which may not load correctly on systems with Secure Boot enabled.

        [ -x "$d/ld.lld" ] && [ -x "$d/clang" ] && { PATH="$d:$PATH"; export PATH; kmake="LLVM=1"; break; }
    done
    [ -n "$kmake" ] || echo "   no LLVM toolchain (ld.lld) on this builder: the kernel is clang-built, the module will not build"
fi

⚠️ Review coverage: The following files were not included in this review because of the token budget:

  • os/licenses/ryzenadj/src/lib/api.c
  • os/licenses/ec_su_axb35/src/src/ec_su_axb35.c
  • os/licenses/ryzenadj/src/main.c
  • os/licenses/ryzenadj/src/lib/win32/OlsApi.h
  • os/licenses/ryzenadj/src/argparse.c
  • os/licenses/ryzenadj/src/lib/ryzenadj.h
  • os/licenses/ec_su_axb35/src/hwmon/ec-su_axb35-hwmon.c
  • os/licenses/ryzenadj/src/lib/linux/osdep_linux_smu_kernel_module.c
  • os/licenses/ryzenadj/src/lib/cpuid.c
  • os/licenses/ryzenadj/src/argparse.h
  • os/licenses/ryzenadj/src/lib/linux/osdep_linux.c
  • os/licenses/ryzenadj/src/lib/nb_smu_ops.h
  • os/licenses/ryzenadj/src/lib/win32/OlsDef.h
  • os/licenses/ryzenadj/src/lib/ryzenadj_priv.h
  • os/licenses/ryzenadj/src/lib/linux/osdep_linux_mem.h
  • os/licenses/ryzenadj/src/lib/linux/osdep_linux_smu_kernel_module.h
  • os/licenses/ryzenadj/src/win32/readjustService.ps1
  • os/licenses/ryzenadj/src/win32/installServiceTask.bat
  • os/licenses/ryzenadj/src/win32/uninstallServiceTask.bat
  • os/licenses/ec_su_axb35/src/scripts/su_axb35_monitor
  • os/licenses/ec_su_axb35/src/LICENSE
  • os/licenses/ec_su_axb35/LICENSE
  • os/licenses/ryzenadj/src/README.md
  • os/licenses/ryzenadj/src/LICENSE
  • os/licenses/ryzenadj/LICENSE
  • os/licenses/ryzenadj/src/.github/workflows/release.yml
  • os/licenses/ec_su_axb35/src/.clang-format
  • os/LICENSES.md
  • os/licenses/ryzenadj/src/CMakeLists.txt
  • os/licenses/ryzenadj/src/.github/workflows/build.yml
  • os/mini/power.conf.example
  • os/licenses/ec_su_axb35/src/README.md
  • os/licenses/ryzenadj/src/win32/RyzenAdjServiceTask.xml.template
  • os/licenses/ec_su_axb35/src/Makefile
  • os/licenses/ryzenadj/SOURCE
  • os/licenses/ec_su_axb35/SOURCE
  • os/licenses/ec_su_axb35/src/contrib/netdata/README.md
  • os/licenses/ec_su_axb35/src/hwmon/Makefile
  • os/licenses/ec_su_axb35/src/hwmon/README.md
  • os/licenses/ec_su_axb35/src/python-gui/ec-fan-control.desktop
  • os/licenses/ec_su_axb35/src/Kbuild

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit 96bb144

@bong-water-water-bong
bong-water-water-bong marked this pull request as ready for review October 24, 2026 12:03
@github-actions

Copy link
Copy Markdown

Preparing review...

bong-water-water-bong added 3 commits October 24, 2026 09:03
The appliance had no fan or power control at all: on this board class the firmware's own fan curve is
conservative, and without a driver for the board's embedded controller the image shows no fan readings
and cannot set the EC power mode or the SMU limits. This brings all of it to the boards we know and
writes nothing at all anywhere else.

- os/mini/board.sh (installed as /usr/sbin/1bit-board): the EC fan curve, a predictive boost loop
  (0.5 s poll on package power, GPU busy, Tctl and its rise; level 4 on load, back to the curve after
  20 s quiet, level 5 only above 82 C) and the power profile (EC power mode first, then the SMU limits
  through ryzenadj when the image carries it, then the fabric/SoC clock pins), with overrides read from
  /etc/1bit/power. Driver only: no debug-register access, ever.
- os/mini/mkimage.sh: builds ec_su_axb35 for the image kernel from the source this repository ships
  (os/licenses/ec_su_axb35, GPL-2.0, verbatim upstream commit f62c2c22), installs the module plus its
  source and licence into the image, installs board.sh and the /etc/1bit/power template, and ships
  ryzenadj (LGPL-3.0, licence included) when the build machine has it.
- os/mini/init.sh: on a board we know, load the EC driver, apply the curve and the power profile and
  start the boost loop; report fans, power profile and the two temperatures in the boot banner and in
  the 1bit.check self-test. On any other board, one line saying the firmware curve stays.
- os/mini/release-check.sh: a fans row - PASS with the fan rpms at level 4, SKIP when this board has no
  EC we drive, FAIL when the driver loaded but a fan still reads 0 rpm at level 4 for 5 s (the fault
  mode we have seen on real hardware).
- os/LICENSES.md: the board driver (GPL-2.0, source shipped) and the SMU tool (LGPL-3.0).

Image boot on hardware pending: the module build and the VM boot test run once the build lane opens
(EC_MODULE=skip builds an image without the driver). No behaviour change on machines without this board.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
…ile when the cap cannot be written

Three things the review asked for, plus the two Secure Boot traps that would have bitten the image:

- **ryzenadj is pinned, not conditional.** `os/licenses/ryzenadj` carries LGPL-3.0, a SOURCE naming tag
  v0.19.0 (commit ea71739b) with its build recipe, and that tag's source verbatim. `mkimage.sh` now builds
  it from that source (cmake; needs cmake, a C compiler and libpci-dev) instead of copying whatever the
  build machine happens to have, installs `/opt/1bit/ryzenadj`, and puts the source in the image next to
  the binary. The 90 W cap is deterministic.

- **The board driver is signed with the module-signing MOK.** An out-of-tree module has to be signed by a
  key the kernel trusts; it must be the MOK, because the "Secure Boot Kernel" cert carries no
  module-signing OID (verified: amdxdna.ko and ec_su_axb35.ko on the development box are both signed by
  "strixhalo Secure Boot MOK"). `mkimage.sh` signs with MOK.priv/MOK.der through the kernel's sign-file,
  overridable with MODULE_SIGN_KEY/MODULE_SIGN_CERT, and warns loudly when it cannot.

- **The SMU cap has a defined fallback.** If the limits cannot be written - lockdown refuses /dev/mem, a
  kernel without it, or no tool - `1bit-board power` falls back to the EC's own `balanced` profile
  (85 W sustained, 120 W burst) instead of leaving the board in `performance` with no cap, and the boot
  banner says which cap is in force (`power: … cap=90/90/105W …` or `… fallback=balanced …`).
  `FALLBACK_MODE` is in `/etc/1bit/power`.

- `/etc/1bit/power` now documents that its defaults are the development box's profile (performance,
  90/90/105 W, level-5 trip at 82 C unchanged) and marks the fan trip points as the first thing to revisit
  on a hotter unit.

- `tools/copyright.py` knows `*.example` (a '#'-comment file; the template ships as /etc/1bit/power).

Image boot on hardware and the VM test still pending (build lane).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
…t through the MOK

The first real image build found three things the PR could not have known without building:

- the image kernel is clang-built (`CONFIG_CC_IS_CLANG`), so `gcc` rejects its flags
  (`-mretpoline-external-thunk`); the module now builds with `LLVM=1` and the matching
  `/usr/lib/llvm-<major>/bin` on PATH, because `LLVM=1` also wants `ld.lld`, `llvm-ar` and friends -
  a bare `clang` is not enough (`ld.lld: not found`);
- the MOK private key is 0600 root, so an unprivileged build cannot read it: the module is signed
  through `sudo -n` when needed, and always from the scratch copy, never from the file already inside
  the image tree (a root-written module there broke the later chmod/cpio);
- `$out/logs` is created before the build logs are opened.

Also documents that MODULE_SIGN_KEY/MODULE_SIGN_CERT are deliberately not MOK_KEY/MOK_CERT: a module
needs the MOK that carries the module-signing OID, the EFI image needs the kernel-signing key.

Built and verified: image 2.3 GB (EFI 97 MB, initramfs 86 MB), `ec_su_axb35.ko` 33,846 bytes signed by
`MOK.der` ("strixhalo Secure Boot MOK"), ryzenadj v0.19.0 built from the pinned source (68,184 bytes,
6 libs), `1bit-board` and `/etc/1bit/power` in the initramfs, both licence trees with their source.
`vmtest-release.sh` in QEMU: RESULT PASS (16 checks, no failures) for the image with the driver and for
the `EC_MODULE=skip` image, with `SKIP fans : this board has no EC we drive` in both.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
@github-actions

Copy link
Copy Markdown

Preparing review...

@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit f49e7fe

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Review (orchestrator): VMTEST PASS (16 checks) for both the driver image and the EC_MODULE=skip image, fans row SKIP in the VM as designed, module signed with the MOK that carries the module-signing attribute, SMU tool pinned and built from vendored source with the EC balanced-mode fallback when lockdown or the tool is missing. Merging; the hardware boot + 1bit-release-check (fans row PASS with live rpm, [power] cap=90/90/105W in the banner) is the owner's step after the leaderboard.

@bong-water-water-bong
bong-water-water-bong merged commit 293489e into main Oct 24, 2026
7 of 8 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the 1bit/os-fans branch October 24, 2026 12:08
@github-actions

Copy link
Copy Markdown

Persistent review updated to latest commit f49e7fe

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants