Skip to content

fix: Apple Silicon integer SMC decoding (si8/si16, endianness) + daemon crash on unauthorized XPC write - #4

Merged
harryisfish merged 2 commits into
mainfrom
fix/apple-silicon-integer-decode
Jun 10, 2026
Merged

fix: Apple Silicon integer SMC decoding (si8/si16, endianness) + daemon crash on unauthorized XPC write#4
harryisfish merged 2 commits into
mainfrom
fix/apple-silicon-integer-decode

Conversation

@harryisfish

Copy link
Copy Markdown
Contributor

Summary

Two fixes, both found and verified on real hardware (M2 MacBook Air, macOS 15.7.5):

1. Integer SMC keys decoded wrong on Apple Silicon (4453647)

  • ui16/ui32 used big-endian (Intel convention); Apple Silicon SMC is little-endian. B0AV read 53039 instead of 12239 mV (IOKit ground truth: 12235 mV). Byte order is now a decode parameter defaulting to the build architecture.
  • si8/si16/si32 were unsupported and fell through to raw bytes. Not cosmetic: the daemon's plugged-in detection reads AC-W (si8) via doubleValue, which always returned nil and silently defaulted to plugged-in. Verified live: AC-W now tracks adapter attach/detach (-1 ↔ wattage), B0AC matches IOKit Amperage.
  • B0AC/B0AV report mA/mV; values are now scaled to the displayed A/V units.
  • PDTR is DC-in total power — legitimately ~0 on battery (verified: on AC it exactly equals VD0R x ID0R). Added PSTR (system total power) to sensors and power status as the portable-friendly draw signal.

2. Unauthorized XPC write crashed the daemon (38dc9de)

userIsAdmin converted the caller's primary gid with a checked Int32() init. System accounts use "negative" ids (nobody's gid -2 = 4294967294 as gid_t), so the conversion trapped — turning the authorization gate into a local DoS. Reproduced on-machine: sudo -u nobody smctl battery maintain 50 crashed smctld; after the fix it gets a clean "Write requests require root or an admin user." with the daemon PID unchanged. Fixed with Int32(bitPattern:) + regression tests.

Test plan

  • 11 new unit tests (decoder byte orders with real captured bytes; userIsAdmin with system-account ids) — CI runs them (local CLT has no XCTest)
  • Real-hardware verification on M2 MacBook Air against ioreg/AppleSmartBattery: voltage/current match within sampling noise
  • End-to-end battery maintain 70-80: charge inhibit at 80% (B0AC 1535 -> 0 mA), resume at limit 90 (-> +1547 mA), policy survives daemon restart
  • Hardware data point for the coverage list: M2 MacBook Air (Mac14,2) detects tahoe-charging/tahoe-adapter on macOS 15.7.5; charge control works. Note: with CHTE inhibit, pmset/ioreg still report "charging" — actual state is visible in B0AC.

… add si8/si16/si32 + PSTR

Verified on M2 MacBook Air against AppleSmartBattery (IOKit) ground truth:

- ui16/ui32 were decoded big-endian (Intel convention); Apple Silicon SMC
  stores integers little-endian. B0AV read 53039 instead of 12239 mV
  (IOKit: 12235 mV). Byte order is now a decode parameter defaulting to
  the build architecture.
- si8/si16/si32 were unsupported and fell through to raw bytes. This was
  not just cosmetic: the daemon's plugged-in detection reads AC-W (si8)
  via doubleValue, which always returned nil and silently defaulted to
  plugged-in. AC-W now decodes (-1 = no adapter; rendered as
  "not connected"); B0AC decodes to -320 mA matching IOKit Amperage.
- B0AC/B0AV report mA/mV; scale to the displayed A/V units.
- PDTR is DC-in total power and legitimately reads ~0 on battery; add
  PSTR (system total power) to sensors and power status as the
  portable-friendly draw signal (reads ~3-6 W on battery, consistent
  with V*I from the battery rails).
userIsAdmin converted the caller's primary gid with a checked Int32()
initializer. System accounts use "negative" ids — nobody's gid -2 is
4294967294 as gid_t — so the conversion trapped and took down the root
daemon (EXC_BREAKPOINT in authorizeWrite), turning the authorization
gate into a local DoS: any user could crash-loop smctld instead of
being rejected.

Use Int32(bitPattern:) and add a regression test. Reproduced and
verified on-machine: before, `sudo -u nobody smctl battery maintain 50`
crashed the daemon (crash report smctld-2026-06-10-133814.ips); after,
it gets "Write requests require root or an admin user." and the daemon
PID is unchanged.
@harryisfish
harryisfish merged commit 4ae5c07 into main Jun 10, 2026
1 check passed
@leaperone-bot
leaperone-bot deleted the fix/apple-silicon-integer-decode branch June 10, 2026 20:20
harryisfish added a commit that referenced this pull request Jun 10, 2026
Squashed PR #5 after rebasing onto PR #4.\n\nValidated:\n- swift test --disable-sandbox\n- git diff --check\n- git merge-tree --write-tree --merge-base $(git merge-base HEAD origin/main) HEAD origin/main\n- GitHub CI Build & Test (macOS)
harryisfish added a commit that referenced this pull request Jun 10, 2026
Squashed replacement for closed stacked PR #6 after PR #4 and PR #5 landed.\n\nAdditional fixes included before merge:\n- persist battery.force_discharge in writeConfig\n- prevent disabled charge limiting from keeping force-discharge armed\n- document new CLI entries in English and Chinese README files\n\nValidated:\n- swift test --disable-sandbox\n- git diff --check\n- git merge-tree --write-tree --merge-base $(git merge-base HEAD origin/main) HEAD origin/main\n- GitHub CI Build & Test (macOS)
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