From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from mail-pj1-f48.google.com (mail-pj1-f48.google.com [209.85.216.48]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C5A033C0619 for ; Mon, 7 Sep 2026 21:02:04 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.216.48 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788814926; cv=none; b=gWRagY3CP0qoydGV8QLYlldC0RQr4kGLfI85j03S41oGQQvQofEVHT1vCYsIO60BpjEy+8ZhxhUWygMzJT7kB1g3nOM9iEVN6bpkRVy/1AG4+xbI/bC63gma69QebEojm2oELO4W+qxySh6pgHLNU0FQadMw2z9UrqowMlDVkRE= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1788814926; c=relaxed/simple; bh=PmIyASc5ahDl6dbI4azKOk3q1WbU34wxtlrJ4X2sGNo=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=KjhZkrvwUDeN36Bv20Tp9npyqLqEiDj0Q7sdxbDyZUFQQweBFJVE1ifaaoQNg9UPsY/JnrzbKLbncXdvHHqUOsgQSXjekxO51ZIm1JWTEHwT3Qni+2UnTZ9+8N6NyxK8jd8L8QolUzo2y8KcDqVWSlRi5zROTJ9glHtcLb6UYzw= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=Mf5fypFi; arc=none smtp.client-ip=209.85.216.48 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="Mf5fypFi" Received: by mail-pj1-f48.google.com with SMTP id 98e67ed59e1d1-3964dfb5a69so5126307a91.1 for ; Mon, 07 Sep 2026 14:02:04 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1788814924; x=1789419724; darn=vger.kernel.org; h=content-transfer-encoding:content-type:mime-version:references :in-reply-to:message-id:date:subject:cc:to:from:from:to:cc:subject :date:message-id:reply-to:content-type; bh=BQIbaWfAc0DLmkRrbhqRm47PuouvMVIcEo2TREL0b7I=; b=Mf5fypFi054Gm33w/gzXZbkLgPVnbD6Q+iUFpUJw/5JDDOZlnDzMeJ9oDtv7fPsyT+ huROqulTV+NQEkz6xXhiuMPfExfPORrTL7jUjHY92HDXkyrGfJEvktEgIw/nZLiq6Kte 4WSbAGl6bQTUcgSXtw+ym2q/w/TuvzoGgYoAsyiT+u2l7e8wmqdK3VuWwR0j/oXKQtAG orqmTg36cbvuAki+vY+r/Zc0RMp+zyk9DAoJ0E3XWYY7Mi2RE8DC/6oAgmaoPf/WUhxU dAM9MMewMXvlWLyLzIiFAjjwaLTRQQdepHWZP8Pb9TdtD72JYTX5f/Ep2O0xwhyxD+lW yTSg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1788814924; x=1789419724; h=content-transfer-encoding:content-type:mime-version:references :in-reply-to:message-id:date:subject:cc:to:from:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to :content-type; bh=BQIbaWfAc0DLmkRrbhqRm47PuouvMVIcEo2TREL0b7I=; b=Ick8lHew+8fPnoxFpCmVPehFKYyVghsZWRRS6Xb11K2t7rD22R0VB/s9nEAI9ucNjd DXvPUNIUWmpPP/eF+SZyhaKpRhmtCjMS/JvKfS9wwA6Et0Q+6zqGYLCgWJhDW3lb2/qc jYqp4PS+Ed+sQuYPtmJXAZQA5upJbAYCQW+KshNqWsqU4sCFkKBAuoNcH9u8zj2Hyzxu +IboMfuH7KOBgglKMiRHDv2ZSjWBlOrEJjo1ggGEdkGFpM8d2xuaaaymEGhErjILgByI YwYm4qD8xmLXTnmn7UGdGkBvvjKZtTbdwcf0u0JipxSTv4I5ymnK82nt3nIlIrw+KFVa mbUg== X-Forwarded-Encrypted: i=1; AKwUvBy+Ih/9zArja8d7i0FlxQCcgU2QoR8rUdBGywC3yuEs9Lp0bcm35SxY02zF3+tEmMicrmlHxA0I52g=@vger.kernel.org X-Gm-Message-State: AFuF++ndeMk7izvHm3qp99MOIteDJOyzjALRgpEXIgqBvMlse43jAASW rmESZd14/kKm9lbQkeh+vLLF4DkAVchvRjddmrMwxdJIborG9oSIMC4h X-Gm-Gg: AYBFou3FAfQ1ZTjDo/S7YVDyz4JuOdHMs+jGftNgjEijrhhPB+52Bw7j330mGWU32bd yaduKX8RBz5uP4uXp56hbbtYiedDbStb3kVEXJoJJ9UNBz+NkemTkB9QBHrX9Vev4h7MDMZBKgw 4IQa4/wdPy6MMH8Q7yxIa4tjDhgwN6iNRcnudA2SFHrC7f7iz4wOwuu9WxI5AwU1aCvDM15SLFT cQxZ7EfPcvmevMtbBX8DwS8rTkL92aCKyjJjtYj4AIMvnzUnB8YfzXqcRNHNETuuuKsWqGyCq4H GX532a+Fk94xQ2TMuo4A6x3XXNEAnshfh6KojucZSu134kXkBSrB7WJnMd7wsxFd+n0CjepsXme aicHtW+czd2CuGEd6KEfeABxQHGlcvq0nVHp+Y0kdnGWIvVCIkV07g3Th0Sc2Q8gLtXUaUHjkA1 PrmJy6SqLJqLV6ivqffBVIKwqvKB1AEU3tDapdC7tj+DHEPxLH0qFPRg== X-Received: by 2002:a17:90b:2ecd:b0:396:40e6:f631 with SMTP id 98e67ed59e1d1-39b2609f306mr42451766a91.3.1788814923606; Mon, 07 Sep 2026 14:02:03 -0700 (PDT) Received: from unix.. ([181.229.23.179]) by smtp.gmail.com with ESMTPSA id a92af1059eb24-1432424d27asm40416334c88.2.2026.09.07.14.02.01 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 07 Sep 2026 14:02:03 -0700 (PDT) From: =?UTF-8?q?Iv=C3=A1n=20Ezequiel=20Rodriguez?= To: Heikki Krogerus , Huang Wei Cc: Greg Kroah-Hartman , Fan Wu , linux-usb@vger.kernel.org, linux-kernel@vger.kernel.org, =?UTF-8?q?Iv=C3=A1n=20Ezequiel=20Rodriguez?= Subject: [RFC] usb: typec: ucsi: add software PPM harness for teardown races Date: Mon, 7 Sep 2026 18:01:58 -0300 Message-ID: <20260907210158.766980-1-ivanrwcm25@gmail.com> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20260903232121.271776-1-ivanrwcm25@gmail.com> References: <20260903232121.271776-1-ivanrwcm25@gmail.com> Precedence: bulk X-Mailing-List: linux-usb@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Huang Wei asked for the software-PPM harness that produced the v2 numbers, so the ordering comparison is in the archives. This is an RFC. I am not asking for it to be merged unless it is useful to keep. The module is gated on DEBUG_KERNEL and defaults to n. It drives the real UCSI core through a software PPM that owns the notify source, the same role as ucsi_acpi.c. The three teardown orderings and the two deterministic checks: scenario=0 quiesce, then unregister, then unlink ~150 ms, clean scenario=1 unlink before unregister (v1) ~10 s stall scenario=2 unregister, then late connector notify KASAN UAF scenario=8 CCI mask + positive control MASKOK scenario=9 notify_lock held by an in-flight notify BARRIEROK qemu ... -append 'ucsi_race_test.scenario=N oops=panic' Link: https://lore.kernel.org/linux-usb/20260903232121.271776-1-ivanrwcm25@gmail.com/ Signed-off-by: Iván Ezequiel Rodriguez --- drivers/usb/typec/ucsi/Kconfig | 16 + drivers/usb/typec/ucsi/Makefile | 1 + drivers/usb/typec/ucsi/ucsi_race_test.c | 817 ++++++++++++++++++++++++ 3 files changed, 834 insertions(+) create mode 100644 drivers/usb/typec/ucsi/ucsi_race_test.c diff --git a/drivers/usb/typec/ucsi/Kconfig b/drivers/usb/typec/ucsi/Kconfig index 87dd992a4b9e..87c875bac204 100644 --- a/drivers/usb/typec/ucsi/Kconfig +++ b/drivers/usb/typec/ucsi/Kconfig @@ -104,4 +104,20 @@ config UCSI_HUAWEI_GAOKUN To compile the driver as a module, choose M here: the module will be called ucsi_huawei_gaokun. +config UCSI_TEARDOWN_RACE_TEST + tristate "UCSI teardown race harness" + depends on DEBUG_KERNEL + help + Software PPM that drives the real UCSI core through the teardown + orderings discussed on the ucsi_acpi use-after-free thread, so they + can be compared under KASAN, lockdep and KCSAN without a PNP0CA0 + ACPI device. + + This is a developer tool, not needed on production systems. Load it + with ucsi_race_test.scenario=N; see the header of ucsi_race_test.c + for the scenario list. + + To compile this as a module, choose M here: the module will be + called ucsi_race_test. + endif diff --git a/drivers/usb/typec/ucsi/Makefile b/drivers/usb/typec/ucsi/Makefile index c7e38bf01350..c531aacfc47c 100644 --- a/drivers/usb/typec/ucsi/Makefile +++ b/drivers/usb/typec/ucsi/Makefile @@ -28,3 +28,4 @@ obj-$(CONFIG_UCSI_PMIC_GLINK) += ucsi_glink.o obj-$(CONFIG_CROS_EC_UCSI) += cros_ec_ucsi.o obj-$(CONFIG_UCSI_LENOVO_YOGA_C630) += ucsi_yoga_c630.o obj-$(CONFIG_UCSI_HUAWEI_GAOKUN) += ucsi_huawei_gaokun.o +obj-$(CONFIG_UCSI_TEARDOWN_RACE_TEST) += ucsi_race_test.o diff --git a/drivers/usb/typec/ucsi/ucsi_race_test.c b/drivers/usb/typec/ucsi/ucsi_race_test.c new file mode 100644 index 000000000000..26172fc61e7d --- /dev/null +++ b/drivers/usb/typec/ucsi/ucsi_race_test.c @@ -0,0 +1,817 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Software PPM for UCSI teardown-ordering races + * + * Copyright (C) 2026 Iván Ezequiel Rodriguez + * + * Drives the real UCSI core (ucsi_create / ucsi_register / ucsi_unregister / + * ucsi_destroy / ucsi_notify_common) through a software PPM so that the + * teardown orderings discussed on the ucsi_acpi UAF thread can be compared + * under KASAN, lockdep and KCSAN without a PNP0CA0 ACPI device. + * + * The backend plays the same role as ucsi_acpi.c: it owns the notify source + * that signals ucsi->complete, and it is the thing that must be quiesced + * correctly during teardown. + * + * Module parameter ucsi_race_test.scenario: + * 0 quiesce connector changes, keep completions, then unlink (the fix) + * 1 unlink the notify source before ucsi_unregister() (v1; stalls) + * 2 no quiesce; late connector notify after ucsi_unregister() (UAF) + * 3 baseline: nothing in flight, correct order + * 4 concurrent notify storm during the fixed teardown + * 5 mid-init teardown with the fix + * 6 mid-init teardown with the v1 ordering (stalls) + * 8 CCI mask: completion kept, connector change dropped + positive control + * 9 notify_lock is a real barrier, not just a flag + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ucsi.h" + +#define RACE_NUM_CONNECTORS 2 +#define RACE_TAG "UCSI-RACE" + +/* Connector Change Indication field of the CCI. */ +#define UCSI_RACE_CONNECTOR_FIELD GENMASK(7, 1) + +static int scenario; +module_param(scenario, int, 0444); +MODULE_PARM_DESC(scenario, + "0=fixed 1=v1-unlink-first 2=late-notify-uaf 3=baseline 4=storm 5=mid-init 6=mid-init-v1 8=cci-mask 9=lock-barrier"); + +static int cycles = 5; +module_param(cycles, int, 0444); +MODULE_PARM_DESC(cycles, "teardown cycles for the storm scenario"); + +static int storm_delay_ms = 300; +module_param(storm_delay_ms, int, 0444); +MODULE_PARM_DESC(storm_delay_ms, "in-flight command delay per storm cycle"); + +struct race_ppm { + struct device *dev; + struct ucsi *ucsi; + + /* Mirrors ucsi_acpi: serialises notify against start of teardown. */ + struct mutex notify_lock; + bool quiescing; + bool unlinked; + + /* + * Scenario 9 only: hold notify_lock for this long inside the handler, + * so that the start of teardown provably has to wait for a notify that + * entered the barrier first. + */ + unsigned int block_ms; + bool in_notify; + + /* Protects cci, last_cmd and oneshot_delay_ms. */ + spinlock_t cci_lock; + u32 cci; + u64 last_cmd; + + struct ucsi_capability cap; + atomic_t cmds; + + struct workqueue_struct *notify_wq; + struct delayed_work notify_work; + /* + * Delay applied to the next command only, so that exactly one command + * is left in flight when teardown starts. Every other command is + * answered synchronously, which keeps the measured baseline low and + * makes a missing completion show up as the full UCSI_TIMEOUT_MS. + */ + unsigned int oneshot_delay_ms; +}; + +/* + * CCI is a single register in which the connector-change field and the + * command status bits coexist. Raising a connector change must therefore + * not clobber a command completion that the OS has not acknowledged yet, + * otherwise the emulator invents timeouts that no real PPM would produce. + */ +static void race_raise_connector_change(struct race_ppm *rp, u8 num) +{ + unsigned long flags; + + spin_lock_irqsave(&rp->cci_lock, flags); + rp->cci = (rp->cci & ~UCSI_RACE_CONNECTOR_FIELD) | + (((u32)num << 1) & UCSI_RACE_CONNECTOR_FIELD); + spin_unlock_irqrestore(&rp->cci_lock, flags); +} + +static u32 race_get_cci(struct race_ppm *rp) +{ + unsigned long flags; + u32 cci; + + spin_lock_irqsave(&rp->cci_lock, flags); + cci = rp->cci; + spin_unlock_irqrestore(&rp->cci_lock, flags); + + return cci; +} + +/* + * The backend notify path. This is the code under test: it is the exact + * shape of ucsi_acpi_notify() after the fix. + */ +static void race_deliver_notify(struct race_ppm *rp) +{ + u32 cci; + + mutex_lock(&rp->notify_lock); + + /* Emulates the ACPI handler having been unlinked already. */ + if (rp->unlinked) + goto out_unlock; + + if (rp->block_ms) { + unsigned int block = rp->block_ms; + + rp->block_ms = 0; + WRITE_ONCE(rp->in_notify, true); + msleep(block); + } + + cci = race_get_cci(rp); + + if (rp->quiescing) + cci &= UCSI_CCI_BUSY | UCSI_CCI_ACK_COMPLETE | + UCSI_CCI_COMMAND_COMPLETE; + + ucsi_notify_common(rp->ucsi, cci); + +out_unlock: + mutex_unlock(&rp->notify_lock); +} + +static void race_notify_work(struct work_struct *work) +{ + struct race_ppm *rp = container_of(to_delayed_work(work), + struct race_ppm, notify_work); + + race_deliver_notify(rp); +} + +/* ------------------------- emulated PPM operations ----------------------- */ + +static int race_read_version(struct ucsi *ucsi, u16 *version) +{ + *version = UCSI_VERSION_1_2; + return 0; +} + +static int race_read_cci(struct ucsi *ucsi, u32 *cci) +{ + *cci = race_get_cci(ucsi_get_drvdata(ucsi)); + return 0; +} + +static int race_poll_cci(struct ucsi *ucsi, u32 *cci) +{ + return race_read_cci(ucsi, cci); +} + +static int race_read_message_in(struct ucsi *ucsi, void *val, size_t val_len) +{ + struct race_ppm *rp = ucsi_get_drvdata(ucsi); + unsigned long flags; + u64 cmd; + + spin_lock_irqsave(&rp->cci_lock, flags); + cmd = rp->last_cmd; + spin_unlock_irqrestore(&rp->cci_lock, flags); + + memset(val, 0, val_len); + + if (UCSI_COMMAND(cmd) == UCSI_GET_CAPABILITY) + memcpy(val, &rp->cap, min(val_len, sizeof(rp->cap))); + + return 0; +} + +static int race_write_message_out(struct ucsi *ucsi, void *data, size_t len) +{ + return 0; +} + +/* + * Accept any command and answer it with the matching completion. The + * completion is delivered from notify_wq, which models the ACPI notify + * workqueue: it is a context the teardown path has to cooperate with. + */ +static int race_async_control(struct ucsi *ucsi, u64 command) +{ + struct race_ppm *rp = ucsi_get_drvdata(ucsi); + unsigned long flags; + unsigned int delay; + u32 cci; + + atomic_inc(&rp->cmds); + + spin_lock_irqsave(&rp->cci_lock, flags); + + /* A pending connector change survives until the OS acknowledges it. */ + cci = rp->cci & UCSI_RACE_CONNECTOR_FIELD; + + switch (UCSI_COMMAND(command)) { + case UCSI_PPM_RESET: + cci = UCSI_CCI_RESET_COMPLETE; + break; + case UCSI_ACK_CC_CI: + if (command & UCSI_ACK_CONNECTOR_CHANGE) + cci &= ~UCSI_RACE_CONNECTOR_FIELD; + cci |= UCSI_CCI_ACK_COMPLETE; + break; + default: + cci |= UCSI_CCI_COMMAND_COMPLETE | UCSI_SET_CCI_LENGTH(0x10); + break; + } + + rp->last_cmd = command; + rp->cci = cci; + + /* PPM_RESET is polled, not notified, so it must not eat the delay. */ + if (UCSI_COMMAND(command) == UCSI_PPM_RESET) { + delay = 0; + } else { + delay = rp->oneshot_delay_ms; + rp->oneshot_delay_ms = 0; + } + + spin_unlock_irqrestore(&rp->cci_lock, flags); + + /* PPM_RESET is observed by polling, no notify needed. */ + if (UCSI_COMMAND(command) == UCSI_PPM_RESET) + return 0; + + if (delay) + queue_delayed_work(rp->notify_wq, &rp->notify_work, + msecs_to_jiffies(delay)); + else + race_deliver_notify(rp); + + return 0; +} + +static const struct ucsi_operations race_ops = { + .read_version = race_read_version, + .read_cci = race_read_cci, + .poll_cci = race_poll_cci, + .read_message_in = race_read_message_in, + .write_message_out = race_write_message_out, + .sync_control = ucsi_sync_control_common, + .async_control = race_async_control, +}; + +/* ------------------------------ the scenarios ---------------------------- */ + +/* Fire a connector-change notification for connector @num. */ +static void race_fire_connector_change(struct race_ppm *rp, u8 num) +{ + race_raise_connector_change(rp, num); + race_deliver_notify(rp); +} + +static void race_unlink_notify(struct race_ppm *rp) +{ + /* + * Models acpi_remove_notify_handler(): stop dispatching and then wait + * for anything already dispatched. The flag must not be set while + * holding nothing else, and the flush must happen outside the lock, + * exactly like the real path. + */ + mutex_lock(&rp->notify_lock); + rp->unlinked = true; + mutex_unlock(&rp->notify_lock); + + cancel_delayed_work_sync(&rp->notify_work); + flush_workqueue(rp->notify_wq); +} + +static void race_teardown(struct race_ppm *rp) +{ + unsigned long flags; + ktime_t t0; + s64 ms; + + /* + * Put a connector work in flight: it issues GET_CONNECTOR_STATUS and + * blocks in wait_for_completion_timeout() until notify_wq answers. + * Scenario 3 skips this to measure the teardown floor. + */ + if (scenario != 3) { + spin_lock_irqsave(&rp->cci_lock, flags); + rp->oneshot_delay_ms = 1500; + spin_unlock_irqrestore(&rp->cci_lock, flags); + + race_fire_connector_change(rp, 1); + + /* Let the work reach the wait before teardown starts. */ + msleep(80); + + /* + * The whole test is meaningless unless a connector work is + * really parked in wait_for_completion_timeout() right now. + */ + pr_info(RACE_TAG ": ntfy=0x%llx flags=0x%lx conn_change_en=%d\n", + rp->ucsi->ntfy, rp->ucsi->flags, + !!(rp->ucsi->ntfy & UCSI_ENABLE_NTFY_CONNECTOR_CHANGE)); + pr_info(RACE_TAG ": cmds=%d notify_pending=%d cmd_pending=%d event_pending=%d\n", + atomic_read(&rp->cmds), + !!delayed_work_pending(&rp->notify_work), + test_bit(COMMAND_PENDING, &rp->ucsi->flags), + test_bit(EVENT_PENDING, &rp->ucsi->flags)); + + if (delayed_work_pending(&rp->notify_work) && + test_bit(COMMAND_PENDING, &rp->ucsi->flags)) + pr_info(RACE_TAG ": PRECOND=OK work parked on completion\n"); + else + pr_err(RACE_TAG ": PRECOND=FAIL no work parked on completion\n"); + } + + pr_info(RACE_TAG ": scenario %d teardown start\n", scenario); + t0 = ktime_get(); + + switch (scenario) { + case 3: + /* Baseline: nothing in flight, correct order. */ + mutex_lock(&rp->notify_lock); + rp->quiescing = true; + mutex_unlock(&rp->notify_lock); + + ucsi_unregister(rp->ucsi); + race_unlink_notify(rp); + break; + case 1: + /* + * The v1 ordering: kill the notify source first. The connector + * work is now waiting for a completion that can never arrive, + * so ucsi_unregister() must sit through UCSI_TIMEOUT_MS. + */ + race_unlink_notify(rp); + ucsi_unregister(rp->ucsi); + break; + case 2: + /* + * Handler kept alive across unregister but connector changes + * are never suppressed. The late notify below reaches + * ucsi_connector_change() after the connector array was freed. + */ + ucsi_unregister(rp->ucsi); + pr_info(RACE_TAG ": firing late connector notify after unregister\n"); + race_fire_connector_change(rp, 1); + race_unlink_notify(rp); + break; + default: + /* + * The fix: suppress connector changes, keep completions, drain, + * then unlink and flush before the object is freed. + */ + mutex_lock(&rp->notify_lock); + rp->quiescing = true; + mutex_unlock(&rp->notify_lock); + + ucsi_unregister(rp->ucsi); + + /* A late notify must now be harmless even with connector bits. */ + pr_info(RACE_TAG ": firing late connector notify after unregister\n"); + race_fire_connector_change(rp, 1); + + race_unlink_notify(rp); + break; + } + + ms = ktime_ms_delta(ktime_get(), t0); + pr_info(RACE_TAG ": teardown_ms=%lld\n", ms); + + if (ms >= 5000) + pr_err(RACE_TAG ": RESULT=STALL teardown blocked %lld ms\n", ms); + else + pr_info(RACE_TAG ": RESULT=NOSTALL teardown %lld ms\n", ms); + + ucsi_destroy(rp->ucsi); + rp->ucsi = NULL; + + pr_info(RACE_TAG ": destroy done\n"); +} + +/* + * Scenario 4: hammer the notify path from several CPUs while the fixed + * teardown runs, repeatedly. Any missing barrier in the quiesce protocol + * shows up as a KASAN report on the connector array or on the ucsi object, + * or as a lockdep splat on notify_lock. + */ +#define RACE_STORM_THREADS 4 + +static int race_storm_thread(void *data) +{ + struct race_ppm *rp = data; + + while (!kthread_should_stop()) { + race_raise_connector_change(rp, 1); + race_deliver_notify(rp); + usleep_range(20, 120); + } + + return 0; +} + +static void race_storm_cycle(struct race_ppm *rp, int cycle) +{ + struct task_struct *th[RACE_STORM_THREADS]; + unsigned long flags; + int i, n = 0; + + rp->quiescing = false; + rp->unlinked = false; + + rp->ucsi = ucsi_create(rp->dev, &race_ops); + if (IS_ERR(rp->ucsi)) { + pr_err(RACE_TAG ": cycle %d ucsi_create failed\n", cycle); + rp->ucsi = NULL; + return; + } + ucsi_set_drvdata(rp->ucsi, rp); + + if (ucsi_register(rp->ucsi)) { + pr_err(RACE_TAG ": cycle %d ucsi_register failed\n", cycle); + goto out_destroy; + } + + for (i = 0; i < 500; i++) { + if (rp->ucsi->connector && rp->ucsi->ntfy) + break; + msleep(20); + } + + if (!rp->ucsi->connector || !rp->ucsi->ntfy) { + pr_err(RACE_TAG ": cycle %d init did not land\n", cycle); + ucsi_unregister(rp->ucsi); + race_unlink_notify(rp); + goto out_destroy; + } + + for (i = 0; i < RACE_STORM_THREADS; i++) { + th[n] = kthread_run(race_storm_thread, rp, "ucsi_storm%d", i); + if (!IS_ERR(th[n])) + n++; + } + + /* Park one command mid-flight, as in the single-shot scenarios. */ + spin_lock_irqsave(&rp->cci_lock, flags); + rp->oneshot_delay_ms = storm_delay_ms; + spin_unlock_irqrestore(&rp->cci_lock, flags); + + msleep(min(storm_delay_ms / 4 + 1, 60)); + + /* The fix, under concurrent notifies. */ + mutex_lock(&rp->notify_lock); + rp->quiescing = true; + mutex_unlock(&rp->notify_lock); + + ucsi_unregister(rp->ucsi); + + race_unlink_notify(rp); + + for (i = 0; i < n; i++) + kthread_stop(th[i]); + +out_destroy: + ucsi_destroy(rp->ucsi); + rp->ucsi = NULL; + if (cycles <= 20 || !((cycle + 1) % 100)) + pr_info(RACE_TAG ": cycle %d done\n", cycle); +} + +/* + * Scenario 8: deterministic proof of what the CCI mask does and does not do. + * + * A single CCI carrying both a connector change and a command completion is + * delivered while quiescing. The completion must still be signalled and the + * connector path must not be entered. The same CCI is then delivered with + * quiescing off as a positive control, so that a test that silently stopped + * exercising ucsi_connector_change() cannot pass. + */ +static void race_prove_mask(struct race_ppm *rp) +{ + struct ucsi *ucsi = rp->ucsi; + bool ev_quiesced, cmd_cleared, ev_control; + /* UCSI_CCI_CONNECTOR() extracts the field, so build it by hand. */ + u32 cci = ((u32)1 << 1) | UCSI_CCI_COMMAND_COMPLETE; + unsigned long flags; + + /* Deliver the crafted CCI rather than whatever init left behind. */ + spin_lock_irqsave(&rp->cci_lock, flags); + rp->cci = cci; + spin_unlock_irqrestore(&rp->cci_lock, flags); + + pr_info(RACE_TAG ": mask test cci=0x%08x connector=%u\n", + cci, (unsigned int)UCSI_CCI_CONNECTOR(cci)); + + /* Quiescing: completion expected, connector dispatch not. */ + clear_bit(EVENT_PENDING, &ucsi->flags); + set_bit(COMMAND_PENDING, &ucsi->flags); + reinit_completion(&ucsi->complete); + + mutex_lock(&rp->notify_lock); + rp->quiescing = true; + mutex_unlock(&rp->notify_lock); + + race_deliver_notify(rp); + + ev_quiesced = test_bit(EVENT_PENDING, &ucsi->flags); + cmd_cleared = !test_bit(COMMAND_PENDING, &ucsi->flags); + + pr_info(RACE_TAG ": quiesced: event_pending=%d command_completed=%d\n", + ev_quiesced, cmd_cleared); + + /* Positive control: same CCI, quiescing off, connector must dispatch. */ + clear_bit(EVENT_PENDING, &ucsi->flags); + set_bit(COMMAND_PENDING, &ucsi->flags); + reinit_completion(&ucsi->complete); + + mutex_lock(&rp->notify_lock); + rp->quiescing = false; + mutex_unlock(&rp->notify_lock); + + race_deliver_notify(rp); + + ev_control = test_bit(EVENT_PENDING, &ucsi->flags); + + pr_info(RACE_TAG ": control: event_pending=%d\n", ev_control); + + if (!ev_quiesced && cmd_cleared && ev_control) + pr_info(RACE_TAG ": RESULT=MASKOK completion kept, connector dropped\n"); + else + pr_err(RACE_TAG ": RESULT=MASKFAIL quiesced_ev=%d cmd=%d control_ev=%d\n", + ev_quiesced, cmd_cleared, ev_control); + + /* Let the control's connector work settle before tearing down. */ + msleep(200); + + mutex_lock(&rp->notify_lock); + rp->quiescing = true; + mutex_unlock(&rp->notify_lock); + + ucsi_unregister(rp->ucsi); + race_unlink_notify(rp); + ucsi_destroy(rp->ucsi); + rp->ucsi = NULL; +} + +static int race_block_thread(void *data) +{ + race_deliver_notify(data); + return 0; +} + +/* + * Scenario 9: prove that notify_lock is a real barrier and not just a flag. + * + * A notify enters the handler before teardown and stays inside it. Setting + * quiescing must not be able to complete until that notify has returned, + * which is what makes "its schedule_work() happens before ucsi_unregister() + * starts cancelling" true rather than merely likely. + */ +#define RACE_BLOCK_MS 1200 + +static void race_prove_barrier(struct race_ppm *rp) +{ + struct task_struct *th; + ktime_t t0; + s64 waited; + int i; + + race_raise_connector_change(rp, 1); + + mutex_lock(&rp->notify_lock); + rp->block_ms = RACE_BLOCK_MS; + rp->in_notify = false; + mutex_unlock(&rp->notify_lock); + + th = kthread_run(race_block_thread, rp, "ucsi_block"); + if (IS_ERR(th)) { + pr_err(RACE_TAG ": RESULT=BARRIERFAIL cannot start notify\n"); + return; + } + + /* Wait until the notify is provably inside the critical section. */ + for (i = 0; i < 500; i++) { + if (READ_ONCE(rp->in_notify)) + break; + usleep_range(1000, 2000); + } + + if (!READ_ONCE(rp->in_notify)) { + pr_err(RACE_TAG ": RESULT=BARRIERFAIL notify never entered\n"); + return; + } + + pr_info(RACE_TAG ": notify inside barrier, starting teardown\n"); + + t0 = ktime_get(); + mutex_lock(&rp->notify_lock); + rp->quiescing = true; + mutex_unlock(&rp->notify_lock); + waited = ktime_ms_delta(ktime_get(), t0); + + pr_info(RACE_TAG ": quiesce waited %lld ms for the in-flight notify\n", + waited); + + if (waited >= RACE_BLOCK_MS / 2) + pr_info(RACE_TAG ": RESULT=BARRIEROK teardown blocked %lld ms\n", + waited); + else + pr_err(RACE_TAG ": RESULT=BARRIERFAIL teardown raced past, %lld ms\n", + waited); + + ucsi_unregister(rp->ucsi); + race_unlink_notify(rp); + ucsi_destroy(rp->ucsi); + rp->ucsi = NULL; +} + +static struct platform_device *race_pdev; + +static int __init race_init(void) +{ + struct race_ppm *rp; + int ret; + + race_pdev = platform_device_register_simple("ucsi_race", -1, NULL, 0); + if (IS_ERR(race_pdev)) + return PTR_ERR(race_pdev); + + rp = kzalloc_obj(*rp); + if (!rp) { + ret = -ENOMEM; + goto err_pdev; + } + + rp->dev = &race_pdev->dev; + mutex_init(&rp->notify_lock); + spin_lock_init(&rp->cci_lock); + INIT_DELAYED_WORK(&rp->notify_work, race_notify_work); + rp->cap.num_connectors = RACE_NUM_CONNECTORS; + + /* kacpi_notify_wq is WQ_PERCPU with default concurrency. */ + rp->notify_wq = alloc_workqueue("ucsi_race_notify", WQ_PERCPU, 0); + if (!rp->notify_wq) { + ret = -ENOMEM; + goto err_free; + } + + if (scenario == 4) { + int c; + + for (c = 0; c < cycles; c++) + race_storm_cycle(rp, c); + + pr_info(RACE_TAG ": RESULT=STORMDONE %d cycles %d threads\n", + cycles, RACE_STORM_THREADS); + goto out_clean; + } + + rp->ucsi = ucsi_create(rp->dev, &race_ops); + if (IS_ERR(rp->ucsi)) { + ret = PTR_ERR(rp->ucsi); + goto err_wq; + } + ucsi_set_drvdata(rp->ucsi, rp); + + ret = ucsi_register(rp->ucsi); + if (ret) { + pr_err(RACE_TAG ": RESULT=REGFAIL ucsi_register returned %d\n", ret); + ucsi_destroy(rp->ucsi); + goto err_wq; + } + + /* + * Scenario 5: tear down while ucsi_init_work() is still running its + * command sequence. ucsi_unregister() starts with + * cancel_delayed_work_sync(&ucsi->work), so the init work must be able + * to finish its in-flight command, which again needs the notify path + * to stay alive across ucsi_unregister(). + */ + if (scenario == 5 || scenario == 6) { + unsigned long flags; + ktime_t t0; + s64 ms; + + /* Park one of the init commands in wait_for_completion(). */ + spin_lock_irqsave(&rp->cci_lock, flags); + rp->oneshot_delay_ms = 1500; + spin_unlock_irqrestore(&rp->cci_lock, flags); + + msleep(40); + + pr_info(RACE_TAG ": mid-init connector=%p ntfy=0x%llx cmds=%d notify_pending=%d cmd_pending=%d\n", + rp->ucsi->connector, rp->ucsi->ntfy, + atomic_read(&rp->cmds), + !!delayed_work_pending(&rp->notify_work), + test_bit(COMMAND_PENDING, &rp->ucsi->flags)); + + if (delayed_work_pending(&rp->notify_work) && + test_bit(COMMAND_PENDING, &rp->ucsi->flags)) + pr_info(RACE_TAG ": PRECOND=OK init command parked\n"); + else + pr_err(RACE_TAG ": PRECOND=FAIL no init command parked\n"); + + t0 = ktime_get(); + + if (scenario == 6) { + /* v1 ordering: the parked init command loses its notify. */ + race_unlink_notify(rp); + ucsi_unregister(rp->ucsi); + } else { + mutex_lock(&rp->notify_lock); + rp->quiescing = true; + mutex_unlock(&rp->notify_lock); + + ucsi_unregister(rp->ucsi); + race_unlink_notify(rp); + } + + ms = ktime_ms_delta(ktime_get(), t0); + pr_info(RACE_TAG ": teardown_ms=%lld\n", ms); + if (ms >= 5000) + pr_err(RACE_TAG ": RESULT=STALL mid-init %lld ms\n", ms); + else + pr_info(RACE_TAG ": RESULT=NOSTALL mid-init %lld ms\n", ms); + + ucsi_destroy(rp->ucsi); + rp->ucsi = NULL; + goto out_clean; + } + + /* + * ucsi_register() only queues ucsi_init_work(); the connectors and the + * notification mask appear later. Wait for init to land, otherwise + * ucsi_connector_change() would bail out on the ntfy gate and no work + * would ever be queued. + */ + for (ret = 0; ret < 500; ret++) { + if (rp->ucsi->connector && rp->ucsi->ntfy) + break; + msleep(20); + } + + if (!rp->ucsi->connector || !rp->ucsi->ntfy) { + pr_err(RACE_TAG ": RESULT=INITFAIL ntfy=0x%llx connector=%p\n", + rp->ucsi->ntfy, rp->ucsi->connector); + ucsi_unregister(rp->ucsi); + race_unlink_notify(rp); + ucsi_destroy(rp->ucsi); + ret = -ENODEV; + goto err_wq; + } + + pr_info(RACE_TAG ": init complete, %u connectors, ntfy=0x%llx\n", + rp->ucsi->cap.num_connectors, rp->ucsi->ntfy); + + if (scenario == 8) + race_prove_mask(rp); + else if (scenario == 9) + race_prove_barrier(rp); + else + race_teardown(rp); + +out_clean: + destroy_workqueue(rp->notify_wq); + kfree(rp); + platform_device_unregister(race_pdev); + + pr_info(RACE_TAG ": DONE\n"); + return 0; + +err_wq: + destroy_workqueue(rp->notify_wq); +err_free: + kfree(rp); +err_pdev: + platform_device_unregister(race_pdev); + return ret; +} + +static void __exit race_exit(void) +{ +} + +module_init(race_init); +module_exit(race_exit); + +MODULE_DESCRIPTION("UCSI teardown race harness"); +MODULE_LICENSE("GPL v2"); -- 2.43.0