hw/misc/applesmc.c | 346 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 338 insertions(+), 8 deletions(-)
v1: https://lore.kernel.org/qemu-devel/20260507040153.14565-1-matthew@pq.io/
Changes in v2 (all in patch 2/2, applesmc_isa_realize() #KEY block):
* Add braces around the QLIST_FOREACH() count loop body
(qemu coding style: loops always need braces, even single-line).
Reported-by: Peter Maydell <peter.maydell@linaro.org>
* Replace the manual 4-byte big-endian byte-shift packing of
`count` into `numkey_buf` with a single stl_be_p() call.
Reported-by: Peter Maydell <peter.maydell@linaro.org>
Patch 1/2 is unchanged from v1.
Peter also noted that the static-buffer "must remain valid forever"
contract that applesmc_add_key() requires is awkward — fair, but
fixing it (e.g. switching the key table to a glib hashtable that
copies values) is independent from this series and not reported as
performance-noticeable in profiles. Happy to follow up with a
hashtable conversion as a separate series if the maintainers
think it's worth doing.
Original v1 cover letter follows below.
---
The QEMU applesmc device implements just enough of the Apple SMC PMIO
protocol to satisfy the OSK boot check on older macOS versions. On
modern macOS guests (x86 10.14+, all of the 15.x series) the real
AppleSMC kext enumerates the SMC key space at boot via
APPLESMC_GET_KEY_BY_INDEX_CMD (0x12). The current device only
acknowledges APPLESMC_READ_CMD (0x10) at the command port; every
other command falls through to the default arm of the switch and
sets ST_1E_BAD_CMD.
The macOS driver interprets the resulting 0x82 reply as "spurious
data" and enters a retry loop that floods the kernel log with
kSMCSpuriousData (0x81) / kSMCKeyNotFound errors at roughly 1800
events per second, pegging kernel_task at ~70% CPU and WindowServer
at ~509% CPU. This reproduces reliably on any recent macOS 15 guest
booted with -device isa-applesmc,osk=<valid-OSK>.
This two-patch series fixes the protocol-level bug and rounds out
the SMC key table to a complete iMac20,1 profile.
Patch 1: protocol-level fix
- Accept WRITE_CMD, GET_KEY_BY_INDEX_CMD, GET_KEY_TYPE_CMD at the
command port (in addition to READ_CMD).
- Implement the indexed-iteration walker (returns real key names
from s->data_def, or APPLESMC_ST_1E_BAD_INDEX 0xb8 once the
index is past the end so the guest stops iterating).
- Implement GET_KEY_TYPE returning a 6-byte type/size/attr
response matching VirtualSMC's kern_pmio.cpp behaviour.
- Accept and log WRITE_CMD silently.
- Replace the unknown-key NOEXIST (0x84) reply with a zeroed
payload of the requested length, logged at LOG_UNIMP.
- Route the BAD_CMD path through qemu_log_mask(LOG_GUEST_ERROR).
- Fix MSSD initialiser typo ("\0x3" -> "\x03"). The original
literal was three bytes ('\0', 'x', '3') truncated to one
('\0') by the size argument, so MSSD has been silently
returning 0 since the device was introduced; the corrected
value matches what a real iMac20,1 SMC reports.
Patch 2: populate the key table
- Add 94 keys covering the categories macOS queries on a Sequoia
15.7.5 guest: 28 temperature sensors (sp78), 4 fan keys (fpe2),
12 power-rail keys, 6 DIMM keys, 11 SMC-internal bookkeeping,
13 motion-sensor / wireless, 3 write targets (HE0N/MSDW/NTOK),
2 power-management gates (HE2N/WDTC), 8 platform-identity /
probe keys, plus the Apple-canonical #KEY total-count.
- Sensor values match a real iMac20,1 idle probe published at
https://linux-hardware.org/?probe=999fc708a4&log=sensors:
CPU 40-51 C, GPU 36-42 C, fan at 1200 RPM (= F0Mn idle), etc.
Measured impact (macOS 15.7.5 guest, iMac20,1 profile):
Metric | Before | After
-----------------|---------:|------:
SMC errors / 5s | 9,225 | 2
kernel_task CPU | 70 % | ~2 %
WindowServer CPU | 509 % | ~6 %
A note for review on the zero-valued keys in patch 2: the 26 keys
covering DIMM / SMC bookkeeping / motion-sensor / wireless rails are
registered with present-with-zero values rather than omitted. macOS
distinguishes "absent" (NOEXIST reply, retry-poll) from "broken"
(present, value 0, accepted-and-ignored). Registering these keys
present-with-zero stops the retry-poll behaviour without asserting
any specific value. If the maintainer prefers a tighter scope for
this series I am happy to drop any subset and follow up; the
present-with-zero approach was driven by which keys macOS observed
querying during boot.
Backwards compatibility: legacy macOS guests (10.11-10.13) which do
not iterate the key space via GET_KEY_BY_INDEX boot unchanged. The
original six keys (REV/OSK0/OSK1/NATJ/MSSP/MSSD) are still present
and respond with the same values, modulo the MSSD typo fix in patch
1 which corrects MSSD to the value a real iMac20,1 SMC reports.
Tested against current master (post v11.0.0). Builds clean on
gcc-13 / clang-17 with --enable-werror. Sequoia 15.7.5 guest boots
to login screen with the SMC retry storm absent from the kernel
log; smoke test recipe in TESTING.md alongside the series.
Matthew Jackson (2):
hw/misc/applesmc: fix GET_KEY_BY_INDEX to return real keys, accept
WRITE/TYPE commands
hw/misc/applesmc: populate Apple SMC key table
hw/misc/applesmc.c | 346 +++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 338 insertions(+), 8 deletions(-)
--
2.50.1 (Apple Git-155)
v1: https://lore.kernel.org/qemu-devel/20260507040153.14565-1-matthew@pq.io/
v2: https://lore.kernel.org/qemu-devel/20260507152030.48753-1-matthew@pq.io/
This is a rebase/respin of v2, which had no outstanding review
comments after Peter's two style nits were addressed. Resending on
current master with checkpatch cleared, plus a new patch 3/3 that
gives the file a MAINTAINERS entry.
Changes in v3:
* New patch 3/3: add a MAINTAINERS entry for hw/misc/applesmc.c.
The file currently has no entry, so get_maintainer.pl resolves
only the open list and these patches had no designated reviewer.
This is easy to drop if a maintainer would rather route it
elsewhere; patches 1-2 do not depend on it.
* Rebased on current master. hw/misc/applesmc.c is byte-identical
upstream to the v2 base (blob fd96f5f), so the rebase is a clean
no-op with no functional change to patches 1-2 from v2.
* checkpatch cleanups in patches 1-2, no functional change:
- patch 1/2: reformat the GET_KEY_TYPE 4th-byte block comment to
the leading-/* and trailing-*/-on-their-own-line house style.
- patch 2/2: shorten the F0Ac fan-key comment to stay under the
80-column limit.
Both were pre-existing checkpatch warnings; neither was raised in
the v1/v2 review. The series is now checkpatch-clean.
Changes in v2 (carried into v3, all in patch 2/2, #KEY block):
* Add braces around the QLIST_FOREACH() count loop body
(qemu coding style: loops always need braces, even single-line).
Reported-by: Peter Maydell <peter.maydell@linaro.org>
* Replace the manual 4-byte big-endian byte-shift packing of
`count` into `numkey_buf` with a single stl_be_p() call.
Reported-by: Peter Maydell <peter.maydell@linaro.org>
Patch 1/2 is otherwise unchanged since v1.
On Peter's v2 remark that applesmc_add_key()'s "pointer must remain
valid forever" contract is awkward: agreed it's not ideal, but the
fix (e.g. converting the key table to a glib hashtable that copies
values) is orthogonal to this protocol bug fix and isn't reported as
noticeable in profiles. Happy to send that conversion as a separate
follow-up series if the maintainers think it's worth doing.
Original v1 cover letter follows below.
---
The QEMU applesmc device implements just enough of the Apple SMC PMIO
protocol to satisfy the OSK boot check on older macOS versions. On
modern macOS guests (x86 10.14+, all of the 15.x series) the real
AppleSMC kext enumerates the SMC key space at boot via
APPLESMC_GET_KEY_BY_INDEX_CMD (0x12). The current device only
acknowledges APPLESMC_READ_CMD (0x10) at the command port; every
other command falls through to the default arm of the switch and
sets ST_1E_BAD_CMD.
The macOS driver interprets the resulting 0x82 reply as "spurious
data" and enters a retry loop that floods the kernel log with
kSMCSpuriousData (0x81) / kSMCKeyNotFound errors at roughly 1800
events per second, pegging kernel_task at ~70% CPU and WindowServer
at ~509% CPU. This reproduces reliably on any recent macOS 15 guest
booted with -device isa-applesmc,osk=<valid-OSK>.
This series fixes the protocol-level bug and rounds out the SMC key
table to a complete iMac20,1 profile.
Patch 1: protocol-level fix
- Accept WRITE_CMD, GET_KEY_BY_INDEX_CMD, GET_KEY_TYPE_CMD at the
command port (in addition to READ_CMD).
- Implement the indexed-iteration walker (returns real key names
from s->data_def, or APPLESMC_ST_1E_BAD_INDEX 0xb8 once the
index is past the end so the guest stops iterating).
- Implement GET_KEY_TYPE returning a 6-byte type/size/attr
response matching VirtualSMC's kern_pmio.cpp behaviour.
- Accept and log WRITE_CMD silently.
- Replace the unknown-key NOEXIST (0x84) reply with a zeroed
payload of the requested length, logged at LOG_UNIMP.
- Route the BAD_CMD path through qemu_log_mask(LOG_GUEST_ERROR).
- Fix MSSD initialiser typo ("\0x3" -> "\x03"). The original
literal was three bytes ('\0', 'x', '3') truncated to one
('\0') by the size argument, so MSSD has been silently
returning 0 since the device was introduced; the corrected
value matches what a real iMac20,1 SMC reports.
Patch 2: populate the key table
- Add 94 keys covering the categories macOS queries on a Sequoia
15.7.5 guest: 28 temperature sensors (sp78), 4 fan keys (fpe2),
12 power-rail keys, 6 DIMM keys, 11 SMC-internal bookkeeping,
13 motion-sensor / wireless, 3 write targets (HE0N/MSDW/NTOK),
2 power-management gates (HE2N/WDTC), 8 platform-identity /
probe keys, plus the Apple-canonical #KEY total-count.
- Sensor values match a real iMac20,1 idle probe published at
https://linux-hardware.org/?probe=999fc708a4&log=sensors:
CPU 40-51 C, GPU 36-42 C, fan at 1200 RPM (= F0Mn idle), etc.
Patch 3: MAINTAINERS entry for the device (see "Changes in v3").
Measured impact (macOS 15.7.5 guest, iMac20,1 profile):
Metric | Before | After
-----------------|---------:|------:
SMC errors / 5s | 9,225 | 2
kernel_task CPU | 70 % | ~2 %
WindowServer CPU | 509 % | ~6 %
A note for review on the zero-valued keys in patch 2: the 26 keys
covering DIMM / SMC bookkeeping / motion-sensor / wireless rails are
registered with present-with-zero values rather than omitted. macOS
distinguishes "absent" (NOEXIST reply, retry-poll) from "broken"
(present, value 0, accepted-and-ignored). Registering these keys
present-with-zero stops the retry-poll behaviour without asserting
any specific value. If the maintainer prefers a tighter scope for
this series I am happy to drop any subset and follow up; the
present-with-zero approach was driven by which keys macOS observed
querying during boot.
Backwards compatibility: legacy macOS guests (10.11-10.13) which do
not iterate the key space via GET_KEY_BY_INDEX boot unchanged. The
original six keys (REV/OSK0/OSK1/NATJ/MSSP/MSSD) are still present
and respond with the same values, modulo the MSSD typo fix in patch
1 which corrects MSSD to the value a real iMac20,1 SMC reports.
Tested against current master. Builds clean on gcc-13 / clang-17
with --enable-werror. Sequoia 15.7.5 guest boots to login screen
with the SMC retry storm absent from the kernel log.
Matthew Jackson (3):
hw/misc/applesmc: fix GET_KEY_BY_INDEX to return real keys, accept
WRITE/TYPE commands
hw/misc/applesmc: populate Apple SMC key table
MAINTAINERS: adopt hw/misc/applesmc.c
MAINTAINERS | 5 +
hw/misc/applesmc.c | 348 +++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 345 insertions(+), 8 deletions(-)
--
2.50.1 (Apple Git-155)
The applesmc device implements just enough of the Apple SMC PMIO
protocol to satisfy the OSK boot check on older macOS versions.
On modern macOS guests (x86 10.14+, all 15.x), the AppleSMC kext
enumerates the SMC key space at boot via APPLESMC_GET_KEY_BY_INDEX_CMD
(0x12). The current device only acknowledges APPLESMC_READ_CMD
(0x10) at the command port; every other command falls through to
the default arm of the switch and sets ST_1E_BAD_CMD.
The macOS driver interprets the resulting 0x82 reply as
"spurious data" and enters a retry loop that floods the kernel
log with kSMCSpuriousData (0x81) / kSMCKeyNotFound errors at
roughly 1800 events per second, pegging kernel_task at ~70%
CPU and WindowServer at ~509% CPU. This reproduces reliably
on any recent macOS 15 guest booted with -device
isa-applesmc,osk=<valid-OSK>.
This patch:
* Accepts APPLESMC_WRITE_CMD, APPLESMC_GET_KEY_BY_INDEX_CMD,
and APPLESMC_GET_KEY_TYPE_CMD at the command port (in
addition to the existing READ_CMD path).
* Implements GET_KEY_BY_INDEX by walking s->data_def and
returning the 4-byte ASCII key name at the requested index;
returns APPLESMC_ST_1E_BAD_INDEX (0xb8) once the index is
past the end of the list so the guest stops iterating.
* Implements GET_KEY_TYPE by looking up the key in s->data_def
and returning a 6-byte response (type[4] + size[1] + attr[1])
matching VirtualSMC's kern_pmio.cpp behaviour.
* Implements WRITE by accepting the key name, length, and
payload and logging at LOG_UNIMP. macOS writes SMC keys
during normal power management; silent acceptance avoids
BAD_CMD on every write.
* Replaces the unknown-key NOEXIST (0x84) reply on READ with
a zeroed payload of the requested length, logged at
LOG_UNIMP. Early-boot probes hit hundreds of undocumented
keys per second; NOEXIST triggers retry storms while a
zeroed payload satisfies the probe semantics without
asserting a particular value.
* Routes the BAD_CMD path through qemu_log_mask
(LOG_GUEST_ERROR) instead of the smc_debug printf.
* Fixes a typo in the MSSD key initialiser ("\0x3" -> "\x03").
The original literal was three bytes ('\\0', 'x', '3')
truncated to one ('\\0') by the size argument, so MSSD has
been silently returning 0 since introduction; the corrected
value matches what a real iMac20,1 SMC reports.
Reported-by: macOS guests booted with -device isa-applesmc since 10.14.
Signed-off-by: Matthew Jackson <matthew@pq.io>
---
hw/misc/applesmc.c | 179 +++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 172 insertions(+), 7 deletions(-)
diff --git a/hw/misc/applesmc.c b/hw/misc/applesmc.c
index fd96f5f245..263b2251b0 100644
--- a/hw/misc/applesmc.c
+++ b/hw/misc/applesmc.c
@@ -35,6 +35,7 @@
#include "hw/core/qdev-properties.h"
#include "ui/console.h"
#include "qemu/error-report.h"
+#include "qemu/log.h"
#include "qemu/module.h"
#include "qemu/timer.h"
#include "qom/object.h"
@@ -126,7 +127,14 @@ static void applesmc_io_cmd_write(void *opaque, hwaddr addr, uint64_t val,
smc_debug("CMD received: 0x%02x\n", (uint8_t)val);
switch (val) {
case APPLESMC_READ_CMD:
- /* did last command run through OK? */
+ case APPLESMC_WRITE_CMD:
+ case APPLESMC_GET_KEY_BY_INDEX_CMD:
+ case APPLESMC_GET_KEY_TYPE_CMD:
+ /*
+ * Accept all standard SMC commands. Pre-existing code only handled
+ * READ_CMD; macOS boots hang if WRITE/TYPE/GET_KEY_BY_INDEX commands
+ * return BAD_CMD during early AppleSMC driver init.
+ */
if (status == APPLESMC_ST_CMD_DONE || status == APPLESMC_ST_NEW_CMD) {
s->cmd = val;
s->status = APPLESMC_ST_NEW_CMD | APPLESMC_ST_ACK;
@@ -137,7 +145,8 @@ static void applesmc_io_cmd_write(void *opaque, hwaddr addr, uint64_t val,
}
break;
default:
- smc_debug("UNEXPECTED CMD 0x%02x\n", (uint8_t)val);
+ qemu_log_mask(LOG_GUEST_ERROR,
+ "applesmc: unexpected CMD 0x%02x\n", (uint8_t)val);
s->status = APPLESMC_ST_NEW_CMD;
s->status_1e = APPLESMC_ST_1E_BAD_CMD;
}
@@ -179,17 +188,172 @@ static void applesmc_io_data_write(void *opaque, hwaddr addr, uint64_t val,
s->data_len = d->len;
s->data_pos = 0;
s->status = APPLESMC_ST_ACK | APPLESMC_ST_DATA_READY;
- s->status_1e = APPLESMC_ST_CMD_DONE; /* clear on valid key */
+ s->status_1e = APPLESMC_ST_CMD_DONE;
} else {
- smc_debug("READ_CMD: key '%c%c%c%c' not found!\n",
- s->key[0], s->key[1], s->key[2], s->key[3]);
+ /*
+ * Return zeros for unknown keys instead of NOEXIST. Early
+ * macOS boot probes many undocumented keys; responding
+ * NOEXIST triggers retry storms. A zeroed payload satisfies
+ * the probe without asserting a particular value.
+ */
+ qemu_log_mask(LOG_UNIMP,
+ "applesmc: READ unknown key '%c%c%c%c' len=%d\n",
+ s->key[0], s->key[1], s->key[2], s->key[3],
+ (uint8_t)val);
+ memset(s->data, 0, APPLESMC_MAX_DATA_LENGTH);
+ s->data_len = (uint8_t)val;
+ s->data_pos = 0;
+ s->status = APPLESMC_ST_ACK | APPLESMC_ST_DATA_READY;
+ s->status_1e = APPLESMC_ST_CMD_DONE;
+ }
+ }
+ s->read_pos++;
+ break;
+ case APPLESMC_WRITE_CMD:
+ /*
+ * Accept writes silently. macOS writes SMC keys during power
+ * management, fan control, etc. Log at LOG_UNIMP for visibility
+ * without treating the write as an error.
+ */
+ if ((s->status & 0x0f) == APPLESMC_ST_CMD_DONE) {
+ break;
+ }
+ if (s->read_pos < 4) {
+ s->key[s->read_pos] = val;
+ s->status = APPLESMC_ST_ACK;
+ } else if (s->read_pos == 4) {
+ s->data_len = (uint8_t)val;
+ s->data_pos = 0;
+ s->status = APPLESMC_ST_ACK;
+ } else {
+ if (s->data_pos < s->data_len) {
+ s->data[s->data_pos] = (uint8_t)val;
+ s->data_pos++;
+ if (s->data_pos == s->data_len) {
+ qemu_log_mask(LOG_UNIMP,
+ "applesmc: WRITE key '%c%c%c%c' len=%d\n",
+ s->key[0], s->key[1], s->key[2], s->key[3],
+ s->data_len);
+ s->status = APPLESMC_ST_CMD_DONE;
+ s->status_1e = APPLESMC_ST_CMD_DONE;
+ } else {
+ s->status = APPLESMC_ST_ACK;
+ }
+ }
+ }
+ s->read_pos++;
+ break;
+ case APPLESMC_GET_KEY_TYPE_CMD:
+ /*
+ * Return key type info. Protocol (matches VirtualSMC):
+ * - Receive 4 bytes of key name.
+ * - After the 4th byte, immediately set DATA_READY with response.
+ * - Response is 6 bytes: type[4] + size[1] + attr[1].
+ * Unlike READ_CMD there is no length byte between key name and
+ * response.
+ */
+ if ((s->status & 0x0f) == APPLESMC_ST_CMD_DONE) {
+ break;
+ }
+ if (s->read_pos < 3) {
+ s->key[s->read_pos] = val;
+ s->status = APPLESMC_ST_ACK;
+ } else if (s->read_pos == 3) {
+ /*
+ * 4th and final key byte. Unlike READ_CMD which has a 5th byte
+ * for data length, GET_KEY_TYPE responds immediately after the
+ * 4-byte key name (matching VirtualSMC kern_pmio.cpp behavior).
+ */
+ s->key[3] = val;
+ d = applesmc_find_key(s);
+ if (d != NULL) {
+ switch (d->len) {
+ case 1:
+ s->data[0] = 'u'; s->data[1] = 'i';
+ s->data[2] = '8'; s->data[3] = ' ';
+ break;
+ case 2:
+ s->data[0] = 'u'; s->data[1] = 'i';
+ s->data[2] = '1'; s->data[3] = '6';
+ break;
+ case 4:
+ s->data[0] = 'u'; s->data[1] = 'i';
+ s->data[2] = '3'; s->data[3] = '2';
+ break;
+ default:
+ s->data[0] = 'c'; s->data[1] = 'h';
+ s->data[2] = '8'; s->data[3] = '*';
+ break;
+ }
+ s->data[4] = d->len;
+ s->data[5] = 0xD0;
+ } else {
+ qemu_log_mask(LOG_UNIMP,
+ "applesmc: GET_KEY_TYPE unknown '%c%c%c%c'\n",
+ s->key[0], s->key[1], s->key[2], s->key[3]);
+ s->data[0] = 'u'; s->data[1] = 'i';
+ s->data[2] = '8'; s->data[3] = ' ';
+ s->data[4] = 1;
+ s->data[5] = 0xD0;
+ }
+ s->data_len = 6;
+ s->data_pos = 0;
+ s->status = APPLESMC_ST_ACK | APPLESMC_ST_DATA_READY;
+ s->status_1e = APPLESMC_ST_CMD_DONE;
+ }
+ s->read_pos++;
+ break;
+ case APPLESMC_GET_KEY_BY_INDEX_CMD:
+ /*
+ * Return key name by index. macOS sends a 4-byte big-endian index
+ * and expects the 4-byte ASCII key name at that position. The
+ * previous implementation returned 4 zero bytes, which macOS
+ * treated as kSMCSpuriousData (0x81) and retried indefinitely,
+ * flooding the kernel log at ~1800 errors/sec. Walk the keys list
+ * to return the actual key name, or APPLESMC_ST_1E_BAD_INDEX
+ * (0xb8) once the index is past the end of the list so the guest
+ * stops iterating.
+ */
+ if ((s->status & 0x0f) == APPLESMC_ST_CMD_DONE) {
+ break;
+ }
+ if (s->read_pos < 3) {
+ s->key[s->read_pos] = val;
+ s->status = APPLESMC_ST_ACK;
+ } else if (s->read_pos == 3) {
+ s->key[3] = val;
+ uint32_t idx = ((uint8_t)s->key[0] << 24)
+ | ((uint8_t)s->key[1] << 16)
+ | ((uint8_t)s->key[2] << 8)
+ | (uint8_t)s->key[3];
+ struct AppleSMCData *def;
+ uint32_t i = 0;
+ bool found = false;
+ QLIST_FOREACH(def, &s->data_def, node) {
+ if (i == idx) {
+ memcpy(s->data, def->key, 4);
+ s->data_len = 4;
+ s->data_pos = 0;
+ found = true;
+ break;
+ }
+ i++;
+ }
+ if (!found) {
+ s->data_len = 0;
+ s->status_1e = APPLESMC_ST_1E_BAD_INDEX;
s->status = APPLESMC_ST_CMD_DONE;
- s->status_1e = APPLESMC_ST_1E_NOEXIST;
+ s->read_pos++;
+ break;
}
+ s->status = APPLESMC_ST_ACK | APPLESMC_ST_DATA_READY;
+ s->status_1e = APPLESMC_ST_CMD_DONE;
}
s->read_pos++;
break;
default:
+ qemu_log_mask(LOG_GUEST_ERROR,
+ "applesmc: unhandled data for cmd 0x%02x\n", s->cmd);
s->status = APPLESMC_ST_CMD_DONE;
s->status_1e = APPLESMC_ST_1E_STILL_BAD_CMD;
}
@@ -330,12 +494,13 @@ static void applesmc_isa_realize(DeviceState *dev, Error **errp)
}
QLIST_INIT(&s->data_def);
+
applesmc_add_key(s, "REV ", 6, "\x01\x13\x0f\x00\x00\x03");
applesmc_add_key(s, "OSK0", 32, s->osk);
applesmc_add_key(s, "OSK1", 32, s->osk + 32);
applesmc_add_key(s, "NATJ", 1, "\0");
applesmc_add_key(s, "MSSP", 1, "\0");
- applesmc_add_key(s, "MSSD", 1, "\0x3");
+ applesmc_add_key(s, "MSSD", 1, "\x03");
}
static void applesmc_unrealize(DeviceState *dev)
--
2.50.1 (Apple Git-155)
With APPLESMC_GET_KEY_BY_INDEX_CMD now functional (previous
patch), the modern macOS AppleSMC kext walks the device's key
table at boot to discover what's available. The current key
set (REV/OSK0/OSK1/NATJ/MSSP/MSSD) is sparse enough that
several macOS subsystems retry-poll keys they expect to find,
contributing to the same kSMCSpuriousData traffic the previous
patch addresses.
This patch fills the key table out to a complete iMac20,1
profile (94 additional keys plus the canonical #KEY count).
Sensor values match a real iMac20,1 idle probe published at:
https://linux-hardware.org/?probe=999fc708a4&log=sensors
Categories:
* 28 temperature sensors in sp78 format with idle readings
from the probe: CPU 40 C (TC0P) / 45 C (TC0F) / 51 C
(TCXc), GPU 36 C (TG0P) / 42 C (TG0F/TG1F/TGDD), HDD
41 C (TH0P/TH1C/TH1F), LCD 28-29 C (TL0V/TL1V), memory
34-36 C (TM0V/TM0P), ambient 24 C (TA0V), PSU 33 C
(Tp00/Tp2F), generic sensors 33 C (Ts*S, TS0V), VRM
50 C (TVMD/TVmS/TVSL/TVSR; not in the probe, conservative
estimate). Battery sensors (TB0T/TB1T/TB2T) are
present-with-zero because iMacs have no battery; macOS's
"absent vs broken" distinction relies on the key being
registered even with a zero reading.
* 4 fan keys (FNum/F0Ac/F0Mn/F0Mx) describing iMac20,1's
single chassis fan: 1 fan, current 1200 RPM (= F0Mn,
matching the probe-reported idle), min 1200, max 3600.
fpe2 encoding (raw = RPM * 4, big-endian).
* 12 power-rail keys (PC0R/PCPC/PCPG/PCPT/PfCP/PfCT/PfGT/
PfHT/PfM0/PfST/PSTR/PHDC) for telemetry; present-with-
zero satisfies macOS without claiming a specific value.
* 6 DIMM keys (DM0P/DM0S/DM1P/DM1S/MD1R/MD1W); same
rationale.
* 11 SMC-internal bookkeeping keys (CLKH/DICT/RPlt/SBFL/
VRTC/WKTP plus 5 lower-case Apple-private bookkeeping
keys cePn/cmDU/maNN/mxT1/zEPD); same rationale.
* 13 motion-sensor / wireless keys (MSAc/MSAf/MSAg/MSAi/
MSGA/MSHP/MSPA/MTLV/QCLV/QENA/WIr0/WIw0/WIz0). Present-
with-zero is the correct desktop-class answer.
* 3 write targets that must also be readable: HE0N (iGPU
power; AGPM driver writes 1 on start), MSDW, NTOK.
* 2 power-management gates required by AGPM and the
watchdog probe: HE2N (dGPU power enable), WDTC.
* 8 platform-identity / probe keys (MPRO/MPRD/LGPB/BSLN/
EPCI/BEMB/$Adr/RGEN/DPLM/MSSW/OSWD): VirtualSMC-style
identity plus the early boot-time SMC probe keys.
* 2 GPU temperature sensors (TGDD/TG0P) and #KEY (the
Apple-canonical total-keys count, computed by walking
the data_def list at realize time).
* 1 VirtualSMC-compatible $Num key (some drivers prefer it
over #KEY).
After this patch, with the protocol fix from the previous
patch in place, a Sequoia 15.7.5 guest boots without the SMC
retry storm:
Metric | Before | After
-----------------|---------:|------:
SMC errors / 5s | 9,225 | 2
kernel_task CPU | 70 % | ~2 %
WindowServer CPU | 509 % | ~6 %
Legacy macOS guests (10.11-10.13) that do not iterate the
key space see no behavioural change beyond the corrected
MSSD value (now 0x03 as on real iMac20,1 hardware) and the
fact that READ on a previously-unknown key returns zeros
instead of NOEXIST.
Signed-off-by: Matthew Jackson <matthew@pq.io>
---
hw/misc/applesmc.c | 169 ++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 168 insertions(+), 1 deletion(-)
diff --git a/hw/misc/applesmc.c b/hw/misc/applesmc.c
index 263b2251b0..38062a07b5 100644
--- a/hw/misc/applesmc.c
+++ b/hw/misc/applesmc.c
@@ -32,7 +32,7 @@
#include "qemu/osdep.h"
#include "hw/isa/isa.h"
-#include "hw/core/qdev-properties.h"
+#include "hw/qdev-properties.h"
#include "ui/console.h"
#include "qemu/error-report.h"
#include "qemu/log.h"
@@ -495,12 +495,179 @@ static void applesmc_isa_realize(DeviceState *dev, Error **errp)
QLIST_INIT(&s->data_def);
+ /* System identification */
applesmc_add_key(s, "REV ", 6, "\x01\x13\x0f\x00\x00\x03");
applesmc_add_key(s, "OSK0", 32, s->osk);
applesmc_add_key(s, "OSK1", 32, s->osk + 32);
applesmc_add_key(s, "NATJ", 1, "\0");
applesmc_add_key(s, "MSSP", 1, "\0");
applesmc_add_key(s, "MSSD", 1, "\x03");
+
+ /*
+ * GPU power-management keys. Without HE2N the AGPM driver reads
+ * NOEXIST, errors out (0x82) and hangs the system on dynamic
+ * wallpaper changes. The driver itself sets the value to 1 on
+ * start, so the boot default of 0 is correct.
+ */
+ applesmc_add_key(s, "HE2N", 1, "\x00"); /* dGPU power enable */
+
+ /*
+ * Watchdog timer control. Queried by macOS during early boot; an
+ * unanswered query produces a cluster of SMCWDT errors per boot.
+ */
+ applesmc_add_key(s, "WDTC", 1, "\x00");
+
+ /*
+ * GPU temperature sensors. SP78 format: 16-bit big-endian with the
+ * top 8 bits encoding integer degrees C and the bottom 8 bits the
+ * fraction. Returning zeroed sensors caused macOS to flag these as
+ * broken and retry-poll. Values approximate a warm idle iMac20,1.
+ */
+ applesmc_add_key(s, "TGDD", 2, "\x2A\x00"); /* GPU diode: 42 C */
+ applesmc_add_key(s, "TG0P", 2, "\x24\x00"); /* GPU proximity: 36 C */
+
+ /*
+ * Fan control (iMac20,1 has one chassis fan). fpe2 format: raw
+ * value is RPM << 2, big-endian. Idle ~1500 RPM, range 1200-3600.
+ */
+ applesmc_add_key(s, "FNum", 1, "\x01");
+ applesmc_add_key(s, "F0Ac", 2, "\x12\xC0"); /* 1200 RPM (idle = F0Mn) */
+ applesmc_add_key(s, "F0Mn", 2, "\x12\xC0"); /* min: 1200 RPM */
+ applesmc_add_key(s, "F0Mx", 2, "\x38\x40"); /* max: 3600 RPM */
+
+ /* Platform-identity keys also provided by VirtualSMC. */
+ applesmc_add_key(s, "MPRO", 1, "\x01"); /* model property */
+ applesmc_add_key(s, "MPRD", 1, "\x00"); /* model product */
+ applesmc_add_key(s, "LGPB", 1, "\x00"); /* lid/GPU power */
+
+ /* CPU/power keys consumed by X86PlatformPlugin. */
+ applesmc_add_key(s, "BSLN", 1, "\x00"); /* baseline */
+ applesmc_add_key(s, "EPCI", 4, "\x00\x00\x00\x00"); /* EPC info */
+ applesmc_add_key(s, "BEMB", 1, "\x01"); /* board embedded */
+
+ /* Keys read by the boot-time SMC probe. */
+ applesmc_add_key(s, "OSWD", 2, "\x00\x00"); /* OS watchdog timer */
+ applesmc_add_key(s, "MSSW", 1, "\x00"); /* macOS software state */
+ applesmc_add_key(s, "RGEN", 1, "\x02"); /* SMC generation */
+ applesmc_add_key(s, "DPLM", 4, "\x00\x00\x00\x00"); /* display power */
+ applesmc_add_key(s, "$Adr", 4, "\x00\x00\x03\x00"); /* SMC base addr */
+
+ /*
+ * Temperature sensors covering the rails the macOS AppleSMC client
+ * polls on an iMac20,1-class host. Values are realistic idle
+ * readings in SP78 (big-endian) format; zeros trigger retry storms
+ * on broken-sensor paths.
+ */
+ applesmc_add_key(s, "TC0F", 2, "\x2D\x00"); /* CPU PECI filt: 45 C */
+ applesmc_add_key(s, "TC0P", 2, "\x28\x00"); /* CPU proximity: 40 C */
+ applesmc_add_key(s, "TCXc", 2, "\x33\x00"); /* CPU core max: 51 C */
+ applesmc_add_key(s, "TG0F", 2, "\x2A\x00"); /* GPU 0 filt: 42 C */
+ applesmc_add_key(s, "TG1F", 2, "\x2A\x00"); /* GPU 1 filt: 42 C */
+ applesmc_add_key(s, "TH0P", 2, "\x29\x00"); /* HDD proximity: 41 C */
+ applesmc_add_key(s, "TH1A", 2, "\x1A\x00"); /* HDD 1 ambient: 26 C */
+ applesmc_add_key(s, "TH1C", 2, "\x29\x00"); /* HDD 1 core: 41 C */
+ applesmc_add_key(s, "TH1F", 2, "\x29\x00"); /* HDD 1 filt: 41 C */
+ applesmc_add_key(s, "TL0V", 2, "\x1D\x00"); /* LCD 0: 29 C */
+ applesmc_add_key(s, "TL1V", 2, "\x1C\x00"); /* LCD 1: 28 C */
+ applesmc_add_key(s, "TM0P", 2, "\x24\x00"); /* memory prox: 36 C */
+ applesmc_add_key(s, "TM0V", 2, "\x22\x00"); /* memory VRM: 34 C */
+ applesmc_add_key(s, "Tp00", 2, "\x21\x00"); /* PSU: 33 C */
+ applesmc_add_key(s, "Tp2F", 2, "\x21\x00"); /* PSU 2: 33 C */
+ applesmc_add_key(s, "Ts0S", 2, "\x21\x00"); /* sensor 0: 33 C */
+ applesmc_add_key(s, "TS0V", 2, "\x21\x00"); /* sensor 0 V: 33 C */
+ applesmc_add_key(s, "Ts1S", 2, "\x21\x00"); /* sensor 1: 33 C */
+ applesmc_add_key(s, "Ts2S", 2, "\x21\x00"); /* sensor 2: 33 C */
+ applesmc_add_key(s, "TB0T", 2, "\x00\x00"); /* no battery */
+ applesmc_add_key(s, "TB1T", 2, "\x00\x00"); /* no battery */
+ applesmc_add_key(s, "TB2T", 2, "\x00\x00"); /* no battery */
+ applesmc_add_key(s, "TA0V", 2, "\x18\x00"); /* ambient 0: 24 C */
+ applesmc_add_key(s, "TVMD", 2, "\x32\x00"); /* VRM diode: 50 C */
+ applesmc_add_key(s, "TVmS", 2, "\x32\x00"); /* VRM sense: 50 C */
+ applesmc_add_key(s, "TVSL", 2, "\x32\x00"); /* VRM sense L: 50 C */
+ applesmc_add_key(s, "TVSR", 2, "\x32\x00"); /* VRM sense R: 50 C */
+
+ /* Power and platform rails (12 keys). */
+ applesmc_add_key(s, "PC0R", 2, "\x00\x00"); /* CPU 0 rail */
+ applesmc_add_key(s, "PCPC", 2, "\x00\x00"); /* CPU package core */
+ applesmc_add_key(s, "PCPG", 2, "\x00\x00"); /* CPU package GPU */
+ applesmc_add_key(s, "PCPT", 2, "\x00\x00"); /* CPU package total */
+ applesmc_add_key(s, "PfCP", 2, "\x00\x00"); /* platform CPU power */
+ applesmc_add_key(s, "PfCT", 2, "\x00\x00"); /* platform CPU temp */
+ applesmc_add_key(s, "PfGT", 2, "\x00\x00"); /* platform GPU temp */
+ applesmc_add_key(s, "PfHT", 2, "\x00\x00"); /* platform HDD temp */
+ applesmc_add_key(s, "PfM0", 2, "\x00\x00"); /* platform memory 0 */
+ applesmc_add_key(s, "PfST", 2, "\x00\x00"); /* platform system temp */
+ applesmc_add_key(s, "PSTR", 2, "\x00\x00"); /* power supply temp rail */
+ applesmc_add_key(s, "PHDC", 2, "\x00\x00"); /* platform HDC */
+
+ /* Memory / DIMM counters (6 keys). */
+ applesmc_add_key(s, "DM0P", 2, "\x00\x00"); /* DIMM 0 proximity */
+ applesmc_add_key(s, "DM0S", 2, "\x00\x00"); /* DIMM 0 sensor */
+ applesmc_add_key(s, "DM1P", 2, "\x00\x00"); /* DIMM 1 proximity */
+ applesmc_add_key(s, "DM1S", 2, "\x00\x00"); /* DIMM 1 sensor */
+ applesmc_add_key(s, "MD1R", 2, "\x00\x00"); /* memory DIMM 1 read */
+ applesmc_add_key(s, "MD1W", 2, "\x00\x00"); /* memory DIMM 1 write */
+
+ /* SMC internal bookkeeping (11 keys). */
+ applesmc_add_key(s, "CLKH", 1, "\x00"); /* clock halt */
+ applesmc_add_key(s, "DICT", 1, "\x00"); /* dictionary */
+ applesmc_add_key(s, "RPlt", 2, "\x00\x00"); /* platform revision */
+ applesmc_add_key(s, "SBFL", 1, "\x00"); /* secure boot flags */
+ applesmc_add_key(s, "VRTC", 2, "\x00\x00"); /* virtual RTC */
+ applesmc_add_key(s, "WKTP", 2, "\x00\x00"); /* wake type */
+ applesmc_add_key(s, "zEPD", 1, "\x00"); /* endpoint descriptor */
+ applesmc_add_key(s, "cePn", 1, "\x00"); /* CE panel */
+ applesmc_add_key(s, "cmDU", 1, "\x00"); /* cm display unit */
+ applesmc_add_key(s, "maNN", 1, "\x00"); /* manufacturer NN */
+ applesmc_add_key(s, "mxT1", 2, "\x00\x00"); /* max temp 1 */
+
+ /* Miscellaneous sensor/motion keys (13 keys). */
+ applesmc_add_key(s, "MSAc", 1, "\x00"); /* motion sensor active */
+ applesmc_add_key(s, "MSAf", 1, "\x00"); /* motion sensor filter */
+ applesmc_add_key(s, "MSAg", 1, "\x00"); /* motion sensor gain */
+ applesmc_add_key(s, "MSAi", 1, "\x00"); /* motion sensor interval */
+ applesmc_add_key(s, "MSGA", 1, "\x00"); /* MSG A */
+ applesmc_add_key(s, "MSHP", 1, "\x00"); /* MS HP */
+ applesmc_add_key(s, "MSPA", 1, "\x00"); /* MS PA */
+ applesmc_add_key(s, "MTLV", 1, "\x00"); /* MT level */
+ applesmc_add_key(s, "QCLV", 1, "\x00"); /* Q clevel */
+ applesmc_add_key(s, "QENA", 1, "\x00"); /* Q enable */
+ applesmc_add_key(s, "WIr0", 2, "\x00\x00"); /* WiFi rate 0 */
+ applesmc_add_key(s, "WIw0", 2, "\x00\x00"); /* WiFi write 0 */
+ applesmc_add_key(s, "WIz0", 2, "\x00\x00"); /* WiFi zone 0 */
+
+ /* Write targets that must also be readable. */
+ applesmc_add_key(s, "HE0N", 1, "\x00"); /* iGPU power; driver sets 1 */
+ applesmc_add_key(s, "MSDW", 1, "\x00"); /* MSD write */
+ applesmc_add_key(s, "NTOK", 1, "\x00"); /* notification token */
+
+ /*
+ * VirtualSMC-compatible total-keys key. Kept for compatibility with
+ * drivers that look for $Num; the canonical Apple #KEY below is what
+ * macOS actually iterates against.
+ */
+ applesmc_add_key(s, "$Num", 4, "\x00\x00\x00\x5e");
+
+ /*
+ * #KEY - Apple SMC convention for "total key count". macOS reads it
+ * at boot and iterates 0..count-1 via APPLESMC_GET_KEY_BY_INDEX_CMD.
+ * Without this key, macOS iterates unbounded and every out-of-range
+ * request floods the kernel log with kSMCSpuriousData errors
+ * (measured ~1800/sec on macOS 15). Count is computed by walking the
+ * list. The buffer is function-static so the pointer stays valid
+ * for the device's lifetime.
+ */
+ {
+ int count = 1; /* include #KEY itself */
+ struct AppleSMCData *def;
+ static char numkey_buf[4];
+
+ QLIST_FOREACH(def, &s->data_def, node) {
+ count++;
+ }
+ stl_be_p(numkey_buf, count);
+ applesmc_add_key(s, "#KEY", 4, numkey_buf);
+ }
}
static void applesmc_unrealize(DeviceState *dev)
--
2.50.1 (Apple Git-155)
hw/misc/applesmc.c has no MAINTAINERS entry, so patches to it are
not routed to anyone and get_maintainer.pl only resolves the open
list. Add an "Apple SMC" entry (Odd Fixes) so future changes have a
designated recipient.
Signed-off-by: Matthew Jackson <matthew@pq.io>
---
MAINTAINERS | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 7752917d8c..4155e1d93d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -2106,6 +2106,11 @@ F: hw/net/can/xlnx-*
F: include/hw/net/xlnx-*
F: tests/qtest/xlnx-can*-test*
+Apple SMC
+M: Matthew Jackson <matthew@pq.io>
+S: Odd Fixes
+F: hw/misc/applesmc.c
+
EDU
M: Jiri Slaby <jslaby@suse.cz>
S: Maintained
--
2.50.1 (Apple Git-155)
© 2016 - 2026 Red Hat, Inc.