LDLS: every MDIO read was RFLAGS; ACPI: an Embedded Controller

On the TUF A15 Linux's r8169 found its PHY and read its ID as 0x02460246.
mdio-bus keeps the register it has just read in r11 across
preempt_enable(), which on x86 is `decl %gs:__preempt_count; je` to a
call that preserves every register. LDLS gave __preempt_count zeroes, so
the count reached zero and the call was made -- to a logging stub,
whose syscall left RFLAGS (0x246) in r11. __preempt_count now starts at
PREEMPT_ENABLED (x86 keeps NEED_RESCHED inverted), and preempt_schedule
and its notrace twin are a bare ret.

The same laptop reported its battery full and on AC, unplugged. The EC
owns both, and with no EmbeddedControl handler _REG never told the
firmware the EC was usable, so _BST and _PSR answered their fallbacks.
The EC is found by PNP0C09 or the ECDT and served by the ACPI 12.3
byte protocol, installed before _INI. SCI queries are not handled yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
Nexoniarz 2026-10-25 15:11:36 +01:00
commit d437e42b49
3 changed files with 179 additions and 1 deletions

View file

@ -264,7 +264,7 @@ resolve(void *ctx, const char *name, unsigned long long *out)
// bargain as the logging stubs, and the same caveat -- see the memcpy note in
// docs/architecture/ldls.md.
#define PERCPU_SLOTS 8
#define PERCPU_SLOTS 16
#define PERCPU_SIZE (16 * 1024)
static char percpu_mem[PERCPU_SLOTS][PERCPU_SIZE];
@ -294,6 +294,14 @@ percpu_for(void *ctx, const char *name)
*(unsigned long long *)percpu_mem[i] =
(unsigned long long)(void *)(percpu_mem[i] + 64);
puts_(" per-CPU "); puts_(name); puts_(" -- a task of zeroes\n");
} else if (streq(name, "__preempt_count")) {
// x86 keeps PREEMPT_NEED_RESCHED inverted in this word, so that
// `decl %gs:__preempt_count; je resched` is one instruction. Zero
// means "count zero *and* a reschedule wanted", and every
// preempt_enable() then called preempt_schedule -- see the note on
// __SCT__preempt_schedule in ldls/kpi/base.c for what that cost.
*(unsigned int *)percpu_mem[i] = 0x80000000u;
puts_(" per-CPU "); puts_(name); puts_(" -- PREEMPT_ENABLED\n");
} else {
puts_(" per-CPU "); puts_(name); puts_(" -- 16K of zeroes\n");
}

View file

@ -33,6 +33,9 @@
#include <uacpi/event.h>
#include <uacpi/sleep.h>
#include <uacpi/registers.h>
#include <uacpi/opregion.h>
#include <uacpi/tables.h>
#include <uacpi/acpi.h>
static int ready;
@ -42,6 +45,7 @@ static int s5_typ_a = -1, s5_typ_b = -1;
static uacpi_interrupt_ret power_button(uacpi_handle ctx);
static void cstate_find(void);
static void ec_init(void);
void
acpi_aml_init(void)
@ -62,6 +66,10 @@ acpi_aml_init(void)
return;
}
// The Embedded Controller, before _INI: methods that run there may already
// read it, and on laptops it is where the battery and the adapter live.
ec_init();
// \_PIC(1): tell the firmware the I/O APIC is in use, so that _PRT answers
// with I/O APIC inputs rather than PIC lines. Before _INI runs, which on
// some boards reads the answer.
@ -102,6 +110,151 @@ acpi_aml_init(void)
cstate_find();
}
// --- the Embedded Controller ----------------------------------------------------
//
// A laptop's EC is a microcontroller behind two I/O ports that owns the
// battery gauge, the charger, the lid and the fans; AML reaches it through an
// OperationRegion in the EmbeddedControl space, and only the OS can serve
// that space (ACPI 12). Without a handler every EC field read fails -- and
// firmware guards against that: _REG(EmbeddedControl, 1) sets a flag (ASUS
// calls it ECAV), and until it is set _BST answers "full" and _PSR "on AC".
// That is what the TUF A15 reported, unplugged.
//
// The protocol (ACPI 12.3): wait for the input buffer to empty, write a
// command to the command port, then the address -- and for a read wait for
// the output buffer and take the byte from the data port. Ports come from
// the EC device's _CRS, data first; or from the ECDT when the DSDT has no
// PNP0C09 early enough. SCI events (_Qxx) are not taken yet: nothing here
// needs to be told the adapter changed, because everything reads on demand.
#define EC_OBF 0x01
#define EC_IBF 0x02
#define EC_RD 0x80
#define EC_WR 0x81
static uint16 ec_data, ec_cmd;
static uacpi_namespace_node *ec_node;
static struct spinlock eclock;
static int
ec_wait(int want_obf)
{
for (int i = 0; i < 100000; i++) { // ~100 ms; an EC answers in tens of µs
uint8 st = inb(ec_cmd);
if (want_obf ? (st & EC_OBF) : !(st & EC_IBF))
return 0;
microdelay(1);
}
return -1;
}
static int
ec_byte(int write, uint8 addr, uint8 *v)
{
int r = -1;
acquire(&eclock);
if (ec_wait(0) < 0) goto out;
outb(ec_cmd, write ? EC_WR : EC_RD);
if (ec_wait(0) < 0) goto out;
outb(ec_data, addr);
if (write) {
if (ec_wait(0) < 0) goto out;
outb(ec_data, *v);
if (ec_wait(0) < 0) goto out;
} else {
if (ec_wait(1) < 0) goto out;
*v = inb(ec_data);
}
r = 0;
out:
release(&eclock);
return r;
}
static uacpi_status
ec_handler(uacpi_region_op op, uacpi_handle opdata)
{
if (op == UACPI_REGION_OP_ATTACH || op == UACPI_REGION_OP_DETACH)
return UACPI_STATUS_OK;
if (op != UACPI_REGION_OP_READ && op != UACPI_REGION_OP_WRITE)
return UACPI_STATUS_INVALID_ARGUMENT;
uacpi_region_rw_data *d = opdata;
int write = op == UACPI_REGION_OP_WRITE;
if (!write)
d->value = 0;
// Wider fields are consecutive bytes, low first.
for (int i = 0; i < d->byte_width; i++) {
uint8 b = write ? (uint8)(d->value >> (8 * i)) : 0;
if (d->address + i > 0xFF || ec_byte(write, (uint8)(d->address + i), &b) < 0)
return UACPI_STATUS_HARDWARE_TIMEOUT;
if (!write)
d->value |= (uacpi_u64)b << (8 * i);
}
return UACPI_STATUS_OK;
}
struct ec_ports { int n; uint16 p[2]; };
static uacpi_iteration_decision
ec_port(void *user, uacpi_resource *r)
{
struct ec_ports *e = user;
if (e->n < 2 && r->type == UACPI_RESOURCE_TYPE_IO)
e->p[e->n++] = r->io.minimum;
else if (e->n < 2 && r->type == UACPI_RESOURCE_TYPE_FIXED_IO)
e->p[e->n++] = r->fixed_io.address;
return UACPI_ITERATION_DECISION_CONTINUE;
}
static uacpi_iteration_decision
found_ec(void *user, uacpi_namespace_node *node, uacpi_u32 depth)
{
(void)user; (void)depth;
struct ec_ports e = { 0, { 0, 0 } };
uacpi_for_each_device_resource(node, "_CRS", ec_port, &e);
if (e.n == 2) {
ec_node = node;
ec_data = e.p[0];
ec_cmd = e.p[1];
return UACPI_ITERATION_DECISION_BREAK;
}
return UACPI_ITERATION_DECISION_CONTINUE;
}
static void
ec_init(void)
{
uacpi_table t;
initlock(&eclock, "ec");
uacpi_find_devices("PNP0C09", found_ec, 0);
if (!ec_node &&
uacpi_table_find_by_signature("ECDT", &t) == UACPI_STATUS_OK) {
struct acpi_ecdt *e = t.ptr;
if (e->ec_control.address && e->ec_data.address) {
ec_cmd = (uint16)e->ec_control.address;
ec_data = (uint16)e->ec_data.address;
ec_node = uacpi_namespace_root();
}
uacpi_table_unref(&t);
}
if (!ec_node)
return;
// Installing runs _REG(3, 1) for the regions under it.
uacpi_status st = uacpi_install_address_space_handler(
ec_node, UACPI_ADDRESS_SPACE_EMBEDDED_CONTROLLER, ec_handler, 0);
if (uacpi_unlikely_error(st)) {
kinfo("acpi: EC at 0x%x/0x%x, handler refused: %s\n", ec_data, ec_cmd,
uacpi_status_to_string(st));
return;
}
uint8 probe = 0;
kinfo("acpi: EC at data 0x%x, command 0x%x%s\n", ec_data, ec_cmd,
ec_byte(0, 0, &probe) < 0 ? " -- not answering" : "");
}
// --- power -------------------------------------------------------------------
// Run in the worker thread, not the SCI handler: syncing the filesystems

View file

@ -835,6 +835,17 @@ static int kpi_system_state = 3; // SYSTEM_RUNNING
// A list_head: empty means pointing at itself, not zero.
static void *kpi_leds_list[2] = { kpi_leds_list, kpi_leds_list };
// --- calls that must not touch a register ---------------------------------------------
//
// preempt_schedule is reached through a thunk that saves every register, so
// its callers keep live values in caller-saved ones across the call. A
// logging stub makes a syscall, and `syscall` writes RFLAGS into r11: mdio-
// bus kept the PHY register it had just read in r11, and every MDIO read on
// the RTL8168H came back 0x0246 -- the PHY ID read as 0x02460246 and no
// driver matched it. Nothing to schedule here anyway; a bare ret.
__asm__(".text\n.globl kpi_ret\nkpi_ret:\n\tret\n");
extern void kpi_ret(void);
// --- the table ------------------------------------------------------------------------------------
extern int kpi_queue_delayed_work_on(int cpu, void *q, void *dwork, unsigned long delay);
@ -937,6 +948,12 @@ const struct kpi_sym kpi_base_table[] = {
{ "request_firmware", (void *)kpi_request_firmware },
{ "release_firmware", (void *)kpi_nop },
// must preserve every register
{ "__SCT__preempt_schedule", (void *)kpi_ret },
{ "__SCT__preempt_schedule_notrace", (void *)kpi_ret },
{ "preempt_schedule_thunk", (void *)kpi_ret },
{ "preempt_schedule_notrace_thunk", (void *)kpi_ret },
// must not be zero
{ "__list_add_valid_or_report", (void *)kpi_true },
{ "__list_del_entry_valid_or_report", (void *)kpi_true },