summaryrefslogtreecommitdiffstats
path: root/drivers/power/supply/surface-rt-ec.c
diff options
context:
space:
mode:
authorJinke Han <jinkehan@didiglobal.com>2026-09-08 15:37:42 +0800
committerIngo Molnar <mingo@kernel.org>2026-09-17 09:48:42 +0200
commita5f7a5bb3b7f28ba7e4fa246775b29a0e5537255 (patch)
tree73fef824f9a846cf935869d0dbc5070ef6e2f741 /drivers/power/supply/surface-rt-ec.c
downloadlinux-stable-a5f7a5bb3b7f28ba7e4fa246775b29a0e5537255.tar.gz
linux-stable-a5f7a5bb3b7f28ba7e4fa246775b29a0e5537255.zip
x86/kprobes: Fix crash when probing CS CALL instructionsgrafted
When using eBPF to probe CS CALL instructions within a function, a crash can be triggered. The eBPF tool probes offset 257 of the __hrtimer_run_queues() function: <__hrtimer_run_queues+249>: nopl 0x0(%rax,%rax,1) <__hrtimer_run_queues+254>: mov %r14,%rdi <__hrtimer_run_queues+257>: cs call <__x86_indirect_thunk_r12> <__hrtimer_run_queues+263>: mov %eax,%r12d <__hrtimer_run_queues+266>: xchg %ax,%ax <__hrtimer_run_queues+268>: mov %r13,%rdi Which triggers this crash: BUG: unable to handle page fault for address: 00000000000f41c9 #PF: supervisor write access in kernel mode #PF: error_code(0x0002) - not-present page PGD 0 P4D 0 Oops: 0002 [#1] SMP NOPTI CPU: 1 PID: 0 Comm: swapper/1 Kdump: loaded Tainted: P RIP: 0010:__hrtimer_run_queues+0x106/0x230 Note that __hrtimer_run_queues+0x106 is __hrtimer_run_queues+262, which is at the 6th byte of the above CS CALL instruction. Since the CS CALL instruction occupies 6 bytes, the exception occurred in the middle of that call instruction. The root cause is that when using eBPF tools to probe in the middle of a function, a kprobe with INT3 is used as the underlying implementation. During single-step emulation of the original CALL instruction, int3_emulate_call() assumes that the probed CALL instruction is 5 bytes long. However, the actual CS-prefixed CALL instruction occupies 6 bytes, so it constructs an incorrect exception return address. When the CPU returns from the kprobe handler, the next instruction to be executed is at the address of the last byte of that CS CALL instruction. Coincidentally, starting from that address, the CPU fetches and decodes a completely different instruction, which ultimately triggers a kernel crash. Fix the issue by using the actual instruction length obtained from the instruction decoder when constructing the exception return address, rather than relying on the hardcoded CALL_INSN_SIZE macro. [ mingo: Refined the changelog ] Fixes: 6256e668b7af ("x86/kprobes: Use int3 instead of debug trap for single-step") Suggested-by: Masami Hiramatsu (Google) <mhiramat@kernel.org> Signed-off-by: Jinke Han <jinkehan@didiglobal.com> Signed-off-by: Ingo Molnar <mingo@kernel.org> Reviewed-by: Masami Hiramatsu (Google) <mhiramat@kernel.org> Acked-by: Yafang Shao <laoar.shao@gmail.com> Acked-by: Borislav Petkov <bp@alien8.de> Cc: Peter Zijlstra <peterz@infradead.org> Link: https://patch.msgid.link/20260908073742.GA10517@didi-ThinkCentre-M920t-N000
Diffstat (limited to 'drivers/power/supply/surface-rt-ec.c')
-rw-r--r--drivers/power/supply/surface-rt-ec.c389
1 files changed, 389 insertions, 0 deletions
diff --git a/drivers/power/supply/surface-rt-ec.c b/drivers/power/supply/surface-rt-ec.c
new file mode 100644
index 000000000..da57b0a1a
--- /dev/null
+++ b/drivers/power/supply/surface-rt-ec.c
@@ -0,0 +1,389 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+#include <linux/devm-helpers.h>
+#include <linux/delay.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/module.h>
+#include <linux/property.h>
+#include <linux/power_supply.h>
+#include <linux/types.h>
+
+/* Register Addresses (B=byte; W=word; S=string) */
+#define REGB_STATUS 0x02
+#define REGW_VOLTAGE_NOW 0x20
+#define REGW_CURRENT_NOW 0x24
+#define REGW_CAPACITY 0x28
+#define REGW_CHARGE_NOW 0x2a
+#define REGW_CHARGE_FULL 0x2c
+#define REGW_CYCLE_COUNT 0x3a
+#define REGW_CHARGE_FULL_DESIGN 0x3c
+#define REGW_VOLTAGE_MAX_DESIGN 0x3e
+#define REGW_SERIAL_NUMBER 0x44
+#define REGS_MANUFACTURER 0x46
+#define REGS_MODEL_NAME 0x52
+#define REGS_TECHNOLOGY 0x5a
+#define REGB_ONLINE 0x67
+
+struct srt_ec_device {
+ struct i2c_client *client;
+
+ struct power_supply *bat;
+ struct power_supply *psy;
+
+ struct gpio_desc *enable_gpiod;
+ struct delayed_work poll_work;
+
+ unsigned int technology;
+ unsigned int capacity;
+
+ const char *serial;
+ char manufacturer[13];
+ char model_name[10];
+};
+
+static const enum power_supply_property srt_bat_power_supply_props[] = {
+ POWER_SUPPLY_PROP_CAPACITY,
+ POWER_SUPPLY_PROP_CHARGE_NOW,
+ POWER_SUPPLY_PROP_CHARGE_FULL,
+ POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN,
+ POWER_SUPPLY_PROP_CURRENT_NOW,
+ POWER_SUPPLY_PROP_CYCLE_COUNT,
+ POWER_SUPPLY_PROP_MANUFACTURER,
+ POWER_SUPPLY_PROP_MODEL_NAME,
+ POWER_SUPPLY_PROP_ONLINE,
+ POWER_SUPPLY_PROP_PRESENT,
+ POWER_SUPPLY_PROP_SERIAL_NUMBER,
+ POWER_SUPPLY_PROP_STATUS,
+ POWER_SUPPLY_PROP_TECHNOLOGY,
+ POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN,
+ POWER_SUPPLY_PROP_VOLTAGE_NOW,
+};
+
+static const enum power_supply_property srt_psy_power_supply_props[] = {
+ POWER_SUPPLY_PROP_ONLINE,
+ POWER_SUPPLY_PROP_PRESENT,
+};
+
+static int srt_bat_get_value(struct i2c_client *client, int reg, int *val)
+{
+ int ret;
+
+ switch (reg) {
+ case REGW_CHARGE_NOW:
+ case REGW_CHARGE_FULL_DESIGN:
+ case REGW_CHARGE_FULL:
+ case REGW_VOLTAGE_MAX_DESIGN:
+ case REGW_VOLTAGE_NOW:
+ ret = i2c_smbus_read_word_data(client, reg);
+ if (ret < 0)
+ return ret;
+
+ *val = ret * 1000;
+ break;
+
+ case REGW_CURRENT_NOW:
+ ret = i2c_smbus_read_word_data(client, reg);
+ if (ret < 0)
+ return ret;
+
+ *val = (s16)ret * 1000;
+ break;
+
+ case REGW_CAPACITY:
+ case REGW_CYCLE_COUNT:
+ ret = i2c_smbus_read_word_data(client, reg);
+ if (ret < 0)
+ return ret;
+
+ *val = ret;
+ break;
+
+ case REGB_STATUS:
+ ret = i2c_smbus_read_byte_data(client, reg);
+ if (ret < 0)
+ return ret;
+
+ if (ret & BIT(0))
+ *val = POWER_SUPPLY_STATUS_CHARGING;
+ else
+ *val = POWER_SUPPLY_STATUS_DISCHARGING;
+ break;
+
+ case REGB_ONLINE:
+ ret = i2c_smbus_read_byte_data(client, reg);
+ if (ret < 0)
+ return ret;
+
+ *val = (ret & BIT(1)) >> 1;
+ break;
+
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int srt_bat_power_supply_get_property(struct power_supply *psy,
+ enum power_supply_property psp,
+ union power_supply_propval *val)
+{
+ struct srt_ec_device *srt = power_supply_get_drvdata(psy);
+ struct i2c_client *client = srt->client;
+ int ret = 0;
+
+ switch (psp) {
+ case POWER_SUPPLY_PROP_MANUFACTURER:
+ val->strval = srt->manufacturer;
+ break;
+ case POWER_SUPPLY_PROP_MODEL_NAME:
+ val->strval = srt->model_name;
+ break;
+ case POWER_SUPPLY_PROP_SERIAL_NUMBER:
+ val->strval = srt->serial;
+ break;
+ case POWER_SUPPLY_PROP_CAPACITY:
+ ret = srt_bat_get_value(client, REGW_CAPACITY, &val->intval);
+ break;
+ case POWER_SUPPLY_PROP_CHARGE_NOW:
+ ret = srt_bat_get_value(client, REGW_CHARGE_NOW, &val->intval);
+ break;
+ case POWER_SUPPLY_PROP_CHARGE_FULL:
+ ret = srt_bat_get_value(client, REGW_CHARGE_FULL, &val->intval);
+ break;
+ case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN:
+ ret = srt_bat_get_value(client, REGW_CHARGE_FULL_DESIGN,
+ &val->intval);
+ break;
+ case POWER_SUPPLY_PROP_CURRENT_NOW:
+ ret = srt_bat_get_value(client, REGW_CURRENT_NOW, &val->intval);
+ break;
+ case POWER_SUPPLY_PROP_CYCLE_COUNT:
+ ret = srt_bat_get_value(client, REGW_CYCLE_COUNT, &val->intval);
+ break;
+ case POWER_SUPPLY_PROP_PRESENT:
+ val->intval = 1;
+ break;
+ case POWER_SUPPLY_PROP_ONLINE:
+ ret = srt_bat_get_value(client, REGB_ONLINE, &val->intval);
+ break;
+ case POWER_SUPPLY_PROP_STATUS:
+ if (srt->capacity < 100)
+ ret = srt_bat_get_value(client, REGB_STATUS, &val->intval);
+ else
+ val->intval = POWER_SUPPLY_STATUS_FULL;
+ break;
+ case POWER_SUPPLY_PROP_TECHNOLOGY:
+ val->intval = srt->technology;
+ break;
+ case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN:
+ ret = srt_bat_get_value(client, REGW_VOLTAGE_MAX_DESIGN,
+ &val->intval);
+ break;
+ case POWER_SUPPLY_PROP_VOLTAGE_NOW:
+ ret = srt_bat_get_value(client, REGW_VOLTAGE_NOW, &val->intval);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return ret;
+}
+
+static int srt_psy_power_supply_get_property(struct power_supply *psy,
+ enum power_supply_property psp,
+ union power_supply_propval *val)
+{
+ struct srt_ec_device *srt = power_supply_get_drvdata(psy);
+ struct i2c_client *client = srt->client;
+ int ret;
+
+ switch (psp) {
+ case POWER_SUPPLY_PROP_ONLINE:
+ case POWER_SUPPLY_PROP_PRESENT:
+ ret = i2c_smbus_read_byte_data(client, REGB_ONLINE);
+ if (ret < 0)
+ return ret;
+
+ val->intval = ret & BIT(0);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static void srt_bat_poll_work(struct work_struct *work)
+{
+ struct srt_ec_device *srt =
+ container_of(work, struct srt_ec_device, poll_work.work);
+ int ret, capacity;
+
+ ret = srt_bat_get_value(srt->client, REGW_CAPACITY, &capacity);
+ if (!ret && capacity != srt->capacity) {
+ srt->capacity = capacity;
+ power_supply_changed(srt->bat);
+ }
+
+ /* continuously send uevent notification */
+ schedule_delayed_work(&srt->poll_work, 30 * HZ);
+}
+
+static irqreturn_t srt_psy_detect_irq(int irq, void *dev_id)
+{
+ struct srt_ec_device *srt = dev_id;
+
+ power_supply_changed(srt->psy);
+
+ return IRQ_HANDLED;
+}
+
+static const struct power_supply_desc srt_bat_power_supply_desc = {
+ .name = "surface-rt-battery",
+ .type = POWER_SUPPLY_TYPE_BATTERY,
+ .properties = srt_bat_power_supply_props,
+ .num_properties = ARRAY_SIZE(srt_bat_power_supply_props),
+ .get_property = srt_bat_power_supply_get_property,
+ .external_power_changed = power_supply_changed,
+};
+
+static const struct power_supply_desc srt_psy_power_supply_desc = {
+ .name = "surface-rt-ac-adapter",
+ .type = POWER_SUPPLY_TYPE_MAINS,
+ .properties = srt_psy_power_supply_props,
+ .num_properties = ARRAY_SIZE(srt_psy_power_supply_props),
+ .get_property = srt_psy_power_supply_get_property,
+};
+
+static char *battery_supplied_to[] = { "surface-rt-battery" };
+
+static int srt_ec_probe(struct i2c_client *client)
+{
+ struct power_supply_config bat_cfg = {};
+ struct power_supply_config psy_cfg = {};
+ struct device *dev = &client->dev;
+ struct srt_ec_device *srt;
+ char str_buf[4];
+ int ret;
+
+ srt = devm_kzalloc(dev, sizeof(*srt), GFP_KERNEL);
+ if (!srt)
+ return -ENOMEM;
+
+ i2c_set_clientdata(client, srt);
+ srt->client = client;
+
+ srt->enable_gpiod = devm_gpiod_get(dev, "enable", GPIOD_OUT_HIGH);
+ if (IS_ERR(srt->enable_gpiod))
+ return dev_err_probe(dev, PTR_ERR(srt->enable_gpiod),
+ "failed to get enable gpio\n");
+
+ /* wait till EC is ready */
+ usleep_range(1000, 1500);
+
+ ret = i2c_smbus_read_word_data(client, REGW_SERIAL_NUMBER);
+ if (ret < 0)
+ return ret;
+
+ srt->serial = devm_kasprintf(dev, GFP_KERNEL, "%04x", ret);
+ if (!srt->serial)
+ return -ENOMEM;
+
+ ret = i2c_smbus_read_i2c_block_data(client, REGS_MANUFACTURER,
+ sizeof(srt->manufacturer) - 1,
+ srt->manufacturer);
+ if (ret < 0)
+ return ret;
+
+ ret = i2c_smbus_read_i2c_block_data(client, REGS_MODEL_NAME,
+ sizeof(srt->model_name) - 1,
+ srt->model_name);
+ if (ret < 0)
+ return ret;
+
+ ret = i2c_smbus_read_i2c_block_data(client, REGS_TECHNOLOGY,
+ sizeof(str_buf), str_buf);
+ if (ret < 0)
+ return ret;
+
+ if (!strncmp(str_buf, "LION", 4))
+ srt->technology = POWER_SUPPLY_TECHNOLOGY_LION;
+ else
+ srt->technology = POWER_SUPPLY_TECHNOLOGY_UNKNOWN;
+
+ bat_cfg.drv_data = srt;
+ bat_cfg.fwnode = dev_fwnode(dev);
+
+ srt->bat = devm_power_supply_register(dev, &srt_bat_power_supply_desc,
+ &bat_cfg);
+ if (IS_ERR(srt->bat))
+ return dev_err_probe(dev, PTR_ERR(srt->bat),
+ "failed to register battery power supply\n");
+
+ psy_cfg.drv_data = srt;
+ psy_cfg.fwnode = dev_fwnode(dev);
+ psy_cfg.supplied_to = battery_supplied_to;
+ psy_cfg.num_supplicants = ARRAY_SIZE(battery_supplied_to);
+
+ srt->psy = devm_power_supply_register(dev, &srt_psy_power_supply_desc,
+ &psy_cfg);
+ if (IS_ERR(srt->psy))
+ return dev_err_probe(dev, PTR_ERR(srt->psy),
+ "failed to register AC power supply\n");
+
+ ret = devm_request_threaded_irq(dev, client->irq, NULL, srt_psy_detect_irq,
+ IRQF_ONESHOT, client->name, srt);
+ if (ret < 0)
+ return ret;
+
+ ret = devm_delayed_work_autocancel(dev, &srt->poll_work, srt_bat_poll_work);
+ if (ret < 0)
+ return ret;
+
+ schedule_delayed_work(&srt->poll_work, HZ);
+
+ return 0;
+}
+
+static int srt_ec_suspend(struct device *dev)
+{
+ struct srt_ec_device *srt = dev_get_drvdata(dev);
+
+ cancel_delayed_work_sync(&srt->poll_work);
+
+ return 0;
+}
+
+static int srt_ec_resume(struct device *dev)
+{
+ struct srt_ec_device *srt = dev_get_drvdata(dev);
+
+ schedule_delayed_work(&srt->poll_work, HZ);
+
+ return 0;
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(srt_ec_pm_ops, srt_ec_suspend, srt_ec_resume);
+
+static const struct of_device_id srt_ec_of_match[] = {
+ { .compatible = "microsoft,surface-rt-ec" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, srt_ec_of_match);
+
+static struct i2c_driver srt_ec_driver = {
+ .driver = {
+ .name = "surface-rt-ec",
+ .of_match_table = srt_ec_of_match,
+ .pm = &srt_ec_pm_ops,
+ },
+ .probe = srt_ec_probe,
+};
+module_i2c_driver(srt_ec_driver);
+
+MODULE_AUTHOR("Jonas Schwöbel <jonasschwoebel@yahoo.de>");
+MODULE_DESCRIPTION("Surface RT Embedded Controller driver");
+MODULE_LICENSE("GPL");