Repository navigation
fix(fan): fail-safe holds fans at max RPM instead of restoring auto - #23
Open
catalinb13 wants to merge 1 commit into
Open
catalinb13 wants to merge 1 commit into
catalinb13 wants to merge 1 commit into
Conversation
The thermal safety guard was fail-open: on a trip it handed the fans back to Apple's automatic control and rewrote config.fan.profile to "auto", destroying the user's curve. Field incident (M1 Max): a corrupt SMC sentinel reading (Ts02 279.56C) tripped the guard, and the impossible value also blocked latch release, pinning the machine on the system default curve until the profile was manually re-applied. - FanSafetyDecision.forceAuto -> holdAtMaxRPM: the daemon now pins every fan at its max RPM under its own control, never touches the persisted profile, and resumes the configured curve automatically when the latch releases. - Plausibility filter: readings outside [-5, 150]C (NaN, sentinels like 279.56C/-999) never trip the guard and never block release; being blind to *all* credible readings while manual trips it. - Hot-spot allowance extended to TC*/Tg* die sensors (junction spec ~110C); they routinely exceed 100C under normal sustained load and were false-tripping the guard (TC22 100.67C). - Latch releases after sustained clean ticks; safety hold state is surfaced in fan status (SAFETY HOLD line) and logged on engage/ release edges. - Charging/adapter apply no longer throws every tick on systems without the keys when the feature is disabled, which polluted daemon status lastError. Bumps version to 0.2.4. Guard tests rewritten for the new semantics (garbage sentinel, TC22 trip, blind-by-corruption, release).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The fan safety guard was fail-open. On a trip it handed the fans back to Apple's automatic control and rewrote
config.fan.profile = "auto"on disk, permanently destroying the user's curve until it was manually re-applied. This is the reported "smctl sometimes just does not work and resets to Apple default" behavior.Field incident (M1 Max Mac Studio, October 2026), from the unified log:
TC22 100.67Cis a normal Apple Silicon die temperature under sustained load — a false trip.Ts02 279.56Cis a corrupt SMC sentinel reading (the sensor reports ~66C) — physically impossible. It not only tripped the guard, it also blocked latch release, pinning the machine on Apple's default curve indefinitely.Changes
Fail-closed safety hold (
holdAtMaxRPM)FanSafetyDecision.forceAuto→holdAtMaxRPM. A trip now pins every fan at its maximum RPM under daemon control through the normal target key (F0Tg, the same path the curve engine already uses), latches out new manual commands, never touches the persisted profile, and automatically resumes the configured curve when the latch releases. An overheating Mac needs full cooling, not the system default curve — handing control back at the worst possible moment was backwards.Plausibility filtering
Readings outside the
[-40, 150] °Cwindow (NaN,-999,279.56Csentinels) are implausible: they never trip the guard and never block latch release, in both directions. Being blind to all credible readings while fans are manual trips the hold immediately (fail-closed, not "assume fine").Hot-spot allowance for die sensors
The
Tp*allowance is extended toTC*/Tg*families (junction spec ~110C, allow to ceiling+10, hard max 110C).Ts*skin/proximity sensors keep the base ceiling. Release hysteresis is per-sensor relative to each sensor's effective ceiling.Observability
smctl fan statusshows aSAFETY HOLD: <reason> (fans pinned at maximum; profile resumes on release)line while engaged.FansStatusDTOcarries an optionalsafetyHoldreason.smctl daemon statuslast erroron Mac Studio/desktop Macs.restoreHardwareDefaultsBestEfforton SIGTERM (dead-man) is intentionally kept: an exiting daemon still leaves no fan policy behind.Tests
FanSafetyGuardTestsrewritten for the new semantics plus regressions for the exact field incidents:testGarbageSentinelReadingDoesNotTrip,testSentinelCannotPinTheLatchForever,testCoreAndHotspotSensorsAtFullLoadDoNotTrip(TC22/TC09/Tg0D @105C),testAllReadingsImplausibleEngagesFailSafeImmediately,testPlausibilityPredicate.testFanGuardHoldsMaxRPMWhenSensorsUnreadableasserts the hold writes max-RPM toF0Tg, keepsF0Mdmanual, does not write auto, and leavesconfig.fan.profileunchanged.docs/design.md§5.3) updated to the new semantics. Version bumped to 0.2.4.Validation on hardware
Installed over v0.2.2 on the affected M1 Max Mac Studio: daemon restarted via
launchctl kickstart -k, user's customcoolerprofile re-applied and holding; fans track the curve,last error: -, no guard events, and/etc/smctl/config.tomlmtime confirms nothing rewrites the profile anymore.