Linux PCI subsystem development
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Liao Xuan" <liaoxuan@open-hieco.net>
Cc: linux-pci@vger.kernel.org
Subject: Re: [PATCH 1/3] nvme: Add adaptive PCIe link rate switching function
Date: Thu, 24 Sep 2026 06:29:41 +0000	[thread overview]
Message-ID: <20260924062941.ADFE91F000FF@smtp.kernel.org> (raw)
In-Reply-To: <9e99e007bf05a608b1b82da740c2b33935a27175.1790222172.git.liaoxuan@hygon.cn>

Thank you for your contribution! Sashiko AI review found 9 potential issue(s) to consider:
- [High] nvme: UAF on per-CPU stats and timer re-arming race during teardown
- [High] nvme: infinite link retrain loop on rollback success
- [High] nvme: TOCTOU data race on sw->target_speed causes link mismatch
- [High] nvme: speed switch timer runs during suspend causing deadlocks
- [High] nvme: missing memalloc_noio_save() around queue freeze
- [High] nvme: nvme_unfreeze clears NVME_CTRL_FROZEN prematurely
- [Medium] nvme: synchronous PCIe config space read on block I/O fast path
- [Medium] nvme: data race when zeroing remote per-CPU statistics
- [Medium] nvme: speed switch permanently disabled after controller reset
--

commit 3ad252ce0a89b0f36c40c41b569dd1f1382434b5
Author: Liao Xuan <liaoxuan@open-hieco.net>

nvme: Add adaptive PCIe link rate switching function

This commit introduces dynamic adjustment of the PCIe link rate for NVMe
devices based on I/O activity. The link speed is downgraded during periods
of low activity to save power and upgraded to maximum when I/O load crosses
a calculated threshold.

> diff --git a/drivers/nvme/host/core.c b/drivers/nvme/host/core.c
> index 1322c678f4eb8..254053ea15fcb 100644
> --- a/drivers/nvme/host/core.c
> +++ b/drivers/nvme/host/core.c
[ ... ]
> @@ -5089,6 +5090,7 @@ EXPORT_SYMBOL_GPL(nvme_remove_io_tag_set);
>  
>  void nvme_stop_ctrl(struct nvme_ctrl *ctrl)
>  {
> +	nvme_speed_switch_exit(ctrl);
>  	nvme_mpath_stop(ctrl);
>  	nvme_auth_stop(ctrl);

[Severity: Medium]
Since nvme_add_ctrl() only initializes the speed switch once during device
probe, will calling nvme_speed_switch_exit() during nvme_stop_ctrl()
permanently disable the feature after the first controller reset? The reset
recovery path does not appear to call nvme_speed_switch_init() again.

[ ... ]
> diff --git a/drivers/nvme/host/speed_switch.c b/drivers/nvme/host/speed_switch.c
> new file mode 100644
> index 0000000000000..88c046fcdde89
> --- /dev/null
> +++ b/drivers/nvme/host/speed_switch.c
[ ... ]
> +static void nvme_speed_switch_work(struct work_struct *work)
> +{
> +	struct nvme_speed_switch *sw = container_of(work, struct nvme_speed_switch, work);
> +	struct nvme_ctrl *ctrl = container_of(sw, struct nvme_ctrl, speed_switch);
> +	struct pci_dev *pdev = to_pci_dev(ctrl->dev);
> +	struct pci_dev *bridge = pdev->bus->self;
> +	u8 cur_speed = READ_ONCE(sw->cur_speed);
> +	int ret;
> +
> +	if (nvme_ctrl_state(ctrl) != NVME_CTRL_LIVE)
> +		return;

[Severity: High]
Is this guard sufficient to prevent operations during host-managed suspend?
The controller state remains NVME_CTRL_LIVE during host-managed suspend,
which might allow the timer to fire, schedule this work, and attempt to
access the PCIe configuration space of a suspended device.

> +
> +	/* Freeze I/O to avoid timeouts during link retraining. */
> +	nvme_start_freeze(ctrl);
> +	nvme_wait_freeze(ctrl);
> +
> +	ret = pcie_set_target_speed(bridge, nvme_speed_to_bus_speed(sw->target_speed),
> +				    true);

[Severity: High]
Should this section be protected by memalloc_noio_save() and
memalloc_noio_restore()? Code executing while a block queue is frozen
typically needs NOIO protection. Otherwise, memory reclaim interrupting this
section could attempt to flush dirty pages to the frozen NVMe device,
leading to a system deadlock under memory pressure.

[Severity: High]
Could reading sw->target_speed here introduce a TOCTOU data race? This value
is read to program the hardware, but read again below to update sw->cur_speed.

> +	if (ret) {
> +		dev_warn(ctrl->device,
> +			 "failed to set target rate Gen%u (%d), trying rollback to Gen%u\n",
> +			 sw->target_speed, ret, cur_speed);
> +		ret = pcie_set_target_speed(bridge, nvme_speed_to_bus_speed(cur_speed),
> +					    true);
> +		if (ret) {
> +			dev_err(ctrl->device,
> +				"rollback to Gen%u failed (%d), disabling speed switch\n",
> +				cur_speed, ret);
> +			WRITE_ONCE(sw->enabled, false);
> +			nvme_unfreeze(ctrl);
> +			return;
> +		}

[Severity: High]
If the pcie_set_target_speed() rollback succeeds, the code omits disabling the
speed switch. Since the function returns while sw->enabled remains true, won't
the timer fire again 100ms later, see a target speed mismatch, and repeatedly
fail, freezing the queues continuously?

> +	} else {
> +		WRITE_ONCE(sw->cur_speed, sw->target_speed);
> +	}

[Severity: High]
If the timer concurrently updates sw->target_speed between the hardware
programming step and this write, sw->cur_speed will receive the new value
while the hardware was programmed to the old value. Will this cause the
hardware link speed to become permanently out-of-sync with the software state?

> +
> +	nvme_unfreeze(ctrl);

[Severity: High]
Is it safe to unconditionally call nvme_unfreeze() here? nvme_unfreeze()
explicitly clears the NVME_CTRL_FROZEN bit. If this work finishes while a
concurrent controller reset is ongoing, it clears the flag prematurely, which
could break the reset state machine and allow namespaces to be added
concurrently with the reset.

> +	dev_dbg(ctrl->device, "link rate changed to Gen%u\n",
> +		READ_ONCE(sw->cur_speed));
> +}
[ ... ]
> +/* Called on the I/O submission path to accumulate the transferred bytes. */
> +void nvme_update_io_stats(struct nvme_ctrl *ctrl, struct request *req)
> +{
> +	struct nvme_speed_switch *sw = &ctrl->speed_switch;
> +	struct nvme_speed_switch_stats *stat;
> +	enum req_op op = req_op(req);
> +	unsigned int bytes = blk_rq_bytes(req);
> +
> +	if (!READ_ONCE(sw->enabled))
> +		return;
> +
> +	if (op != REQ_OP_READ && op != REQ_OP_WRITE)
> +		return;
> +
> +	if (!sw->stats) {
> +		dev_dbg(ctrl->device, "I/O statistics not allocated, skip accounting\n");
> +		return;
> +	}
> +
> +	stat = get_cpu_ptr(sw->stats);

[Severity: High]
Could the I/O thread locklessly dereference sw->stats after it has been
freed? If the I/O thread is preempted right after checking sw->enabled, and a
concurrent controller reset calls nvme_speed_switch_exit() (which frees
sw->stats), the I/O thread would resume and access the freed memory here.

> +	if (op == REQ_OP_READ)
> +		stat->read_bytes += bytes;
> +	else
> +		stat->write_bytes += bytes;
> +	put_cpu_ptr(sw->stats);
> +
> +	if (atomic_cmpxchg(&sw->timer_active, NVME_SPEED_TIMER_INACTIVE,
> +			   NVME_SPEED_TIMER_ACTIVE) != NVME_SPEED_TIMER_INACTIVE)
> +		return;
> +
> +	/* First I/O of the activity period: compute the threshold and arm the timer. */
> +	WRITE_ONCE(sw->threshold, nvme_calc_speed_threshold(sw,
> +					pcie_get_width_cap(to_pci_dev(ctrl->dev))));

[Severity: Medium]
Does pcie_get_width_cap() perform a synchronous read of the PCIe configuration
space? Evaluating this on the first I/O of an activity period within the block
I/O submission fast path might introduce severe latency spikes for affected
requests.

> +	if (!READ_ONCE(sw->threshold)) {
> +		dev_warn(ctrl->device,
> +			 "failed to compute speed switch threshold, keeping current rate\n");
> +		atomic_set(&sw->timer_active, NVME_SPEED_TIMER_INACTIVE);
> +		return;
> +	}
> +	mod_timer(&sw->timer, jiffies + msecs_to_jiffies(READ_ONCE(sw->monitor_interval)));
> +}

[Severity: High]
Following up on the preemption scenario, if nvme_speed_switch_exit() completes
its teardown (including timer_delete_sync()), will this mod_timer() call
re-arm the timer after teardown has finished, leading to a delayed
use-after-free when the timer fires?

[ ... ]
> +static int nvme_check_io_and_decide_speed(struct nvme_speed_switch *sw)
> +{
> +	struct nvme_ctrl *ctrl = container_of(sw, struct nvme_ctrl, speed_switch);
> +	u64 read_bytes = 0, write_bytes = 0;
> +	unsigned long read_kb, write_kb;
> +	int cpu;
> +
> +	for_each_possible_cpu(cpu) {
> +		struct nvme_speed_switch_stats *stat = per_cpu_ptr(sw->stats, cpu);
> +
> +		read_bytes += stat->read_bytes;
> +		write_bytes += stat->write_bytes;
> +		stat->read_bytes = 0;
> +		stat->write_bytes = 0;
> +	}

[Severity: Medium]
Could zeroing these remote per-CPU statistics lead to a data race? The timer
callback iterates over all CPUs and assigns 0 to stat->read_bytes and
stat->write_bytes of remote CPUs concurrently while the I/O submission path
might be updating these variables. Without atomic operations or
synchronization, this could cause incorrect statistics and dropped increments.

[ ... ]
> +void nvme_speed_switch_exit(struct nvme_ctrl *ctrl)
> +{
> +	struct nvme_speed_switch *sw = &ctrl->speed_switch;
> +
> +	if (!sw->initialized)
> +		return;
> +
> +	sw->initialized = false;
> +	WRITE_ONCE(sw->enabled, false);
> +	timer_delete_sync(&sw->timer);
> +	cancel_work_sync(&sw->work);
> +
> +	free_percpu(sw->stats);
> +	sw->stats = NULL;
> +}

[Severity: High]
This teardown routine frees sw->stats and uses timer_delete_sync(), but does
this adequately prevent the timer from being asynchronously re-armed by an I/O
thread that had already passed the sw->enabled check?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/cover.1790222172.git.liaoxuan@hygon.cn?part=1

  reply	other threads:[~2026-09-24  6:29 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-24  6:15 [PATCH 0/3] Add adaptive NVMe link rate switching function Liao Xuan
2026-09-24  6:15 ` [PATCH 1/3] nvme: Add adaptive PCIe " Liao Xuan
2026-09-24  6:29   ` sashiko-bot [this message]
2026-09-24  6:15 ` [PATCH 2/3] nvme: Add hysteresis and idle detection to link rate switching Liao Xuan
2026-09-24  6:27   ` sashiko-bot
2026-09-24  6:15 ` [PATCH 3/3] nvme: Expose link rate switching tunables via sysfs Liao Xuan
2026-09-24  6:29   ` sashiko-bot

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260924062941.ADFE91F000FF@smtp.kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=liaoxuan@open-hieco.net \
    --cc=linux-pci@vger.kernel.org \
    --cc=sashiko-reviews@lists.linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox