igc: Fix race condition in PTP tx code

[ Upstream commit 9c50e2b150c8ee0eee5f8154e2ad168cdd748877 ]

Currently, the igc driver supports timestamping only one tx packet at a
time. During the transmission flow, the skb that requires hardware
timestamping is saved in adapter->ptp_tx_skb. Once hardware has the
timestamp, an interrupt is delivered, and adapter->ptp_tx_work is
scheduled. In igc_ptp_tx_work(), we read the timestamp register, update
adapter->ptp_tx_skb, and notify the network stack.

While the thread executing the transmission flow (the user process
running in kernel mode) and the thread executing ptp_tx_work don't
access adapter->ptp_tx_skb concurrently, there are two other places
where adapter->ptp_tx_skb is accessed: igc_ptp_tx_hang() and
igc_ptp_suspend().

igc_ptp_tx_hang() is executed by the adapter->watchdog_task worker
thread which runs periodically so it is possible we have two threads
accessing ptp_tx_skb at the same time. Consider the following scenario:
right after __IGC_PTP_TX_IN_PROGRESS is set in igc_xmit_frame_ring(),
igc_ptp_tx_hang() is executed. Since adapter->ptp_tx_start hasn't been
written yet, this is considered a timeout and adapter->ptp_tx_skb is
cleaned up.

This patch fixes the issue described above by adding the ptp_tx_lock to
protect access to ptp_tx_skb and ptp_tx_start fields from igc_adapter.
Since igc_xmit_frame_ring() called in atomic context by the networking
stack, ptp_tx_lock is defined as a spinlock, and the irq safe variants
of lock/unlock are used.

With the introduction of the ptp_tx_lock, the __IGC_PTP_TX_IN_PROGRESS
flag doesn't provide much of a use anymore so this patch gets rid of it.

Fixes: 2c344ae245 ("igc: Add support for TX timestamping")
Signed-off-by: Andre Guedes <andre.guedes@intel.com>
Signed-off-by: Vinicius Costa Gomes <vinicius.gomes@intel.com>
Reviewed-by: Kurt Kanzenbach <kurt@linutronix.de>
Tested-by: Naama Meir <naamax.meir@linux.intel.com>
Signed-off-by: Tony Nguyen <anthony.l.nguyen@intel.com>
Signed-off-by: Sasha Levin <sashal@kernel.org>
This commit is contained in:
Vinicius Costa Gomes 2023-06-07 14:32:29 -07:00 committed by Greg Kroah-Hartman
parent 660d4e73ef
commit 1ba91ffa1a
3 changed files with 41 additions and 30 deletions

View File

@ -210,6 +210,10 @@ struct igc_adapter {
struct ptp_clock *ptp_clock; struct ptp_clock *ptp_clock;
struct ptp_clock_info ptp_caps; struct ptp_clock_info ptp_caps;
struct work_struct ptp_tx_work; struct work_struct ptp_tx_work;
/* Access to ptp_tx_skb and ptp_tx_start are protected by the
* ptp_tx_lock.
*/
spinlock_t ptp_tx_lock;
struct sk_buff *ptp_tx_skb; struct sk_buff *ptp_tx_skb;
struct hwtstamp_config tstamp_config; struct hwtstamp_config tstamp_config;
unsigned long ptp_tx_start; unsigned long ptp_tx_start;
@ -389,7 +393,6 @@ enum igc_state_t {
__IGC_TESTING, __IGC_TESTING,
__IGC_RESETTING, __IGC_RESETTING,
__IGC_DOWN, __IGC_DOWN,
__IGC_PTP_TX_IN_PROGRESS,
}; };
enum igc_tx_flags { enum igc_tx_flags {

View File

@ -1467,9 +1467,10 @@ static netdev_tx_t igc_xmit_frame_ring(struct sk_buff *skb,
* the other timer registers before skipping the * the other timer registers before skipping the
* timestamping request. * timestamping request.
*/ */
if (adapter->tstamp_config.tx_type == HWTSTAMP_TX_ON && unsigned long flags;
!test_and_set_bit_lock(__IGC_PTP_TX_IN_PROGRESS,
&adapter->state)) { spin_lock_irqsave(&adapter->ptp_tx_lock, flags);
if (adapter->tstamp_config.tx_type == HWTSTAMP_TX_ON && !adapter->ptp_tx_skb) {
skb_shinfo(skb)->tx_flags |= SKBTX_IN_PROGRESS; skb_shinfo(skb)->tx_flags |= SKBTX_IN_PROGRESS;
tx_flags |= IGC_TX_FLAGS_TSTAMP; tx_flags |= IGC_TX_FLAGS_TSTAMP;
@ -1478,6 +1479,8 @@ static netdev_tx_t igc_xmit_frame_ring(struct sk_buff *skb,
} else { } else {
adapter->tx_hwtstamp_skipped++; adapter->tx_hwtstamp_skipped++;
} }
spin_unlock_irqrestore(&adapter->ptp_tx_lock, flags);
} }
/* record initial flags and protocol */ /* record initial flags and protocol */

View File

@ -323,6 +323,7 @@ static int igc_ptp_set_timestamp_mode(struct igc_adapter *adapter,
return 0; return 0;
} }
/* Requires adapter->ptp_tx_lock held by caller. */
static void igc_ptp_tx_timeout(struct igc_adapter *adapter) static void igc_ptp_tx_timeout(struct igc_adapter *adapter)
{ {
struct igc_hw *hw = &adapter->hw; struct igc_hw *hw = &adapter->hw;
@ -330,7 +331,6 @@ static void igc_ptp_tx_timeout(struct igc_adapter *adapter)
dev_kfree_skb_any(adapter->ptp_tx_skb); dev_kfree_skb_any(adapter->ptp_tx_skb);
adapter->ptp_tx_skb = NULL; adapter->ptp_tx_skb = NULL;
adapter->tx_hwtstamp_timeouts++; adapter->tx_hwtstamp_timeouts++;
clear_bit_unlock(__IGC_PTP_TX_IN_PROGRESS, &adapter->state);
/* Clear the tx valid bit in TSYNCTXCTL register to enable interrupt. */ /* Clear the tx valid bit in TSYNCTXCTL register to enable interrupt. */
rd32(IGC_TXSTMPH); rd32(IGC_TXSTMPH);
netdev_warn(adapter->netdev, "Tx timestamp timeout\n"); netdev_warn(adapter->netdev, "Tx timestamp timeout\n");
@ -338,20 +338,20 @@ static void igc_ptp_tx_timeout(struct igc_adapter *adapter)
void igc_ptp_tx_hang(struct igc_adapter *adapter) void igc_ptp_tx_hang(struct igc_adapter *adapter)
{ {
bool timeout = time_is_before_jiffies(adapter->ptp_tx_start + unsigned long flags;
IGC_PTP_TX_TIMEOUT);
if (!test_bit(__IGC_PTP_TX_IN_PROGRESS, &adapter->state)) spin_lock_irqsave(&adapter->ptp_tx_lock, flags);
return;
/* If we haven't received a timestamp within the timeout, it is if (!adapter->ptp_tx_skb)
* reasonable to assume that it will never occur, so we can unlock the goto unlock;
* timestamp bit when this occurs.
*/ if (time_is_after_jiffies(adapter->ptp_tx_start + IGC_PTP_TX_TIMEOUT))
if (timeout) { goto unlock;
cancel_work_sync(&adapter->ptp_tx_work);
igc_ptp_tx_timeout(adapter); igc_ptp_tx_timeout(adapter);
}
unlock:
spin_unlock_irqrestore(&adapter->ptp_tx_lock, flags);
} }
/** /**
@ -361,6 +361,8 @@ void igc_ptp_tx_hang(struct igc_adapter *adapter)
* If we were asked to do hardware stamping and such a time stamp is * If we were asked to do hardware stamping and such a time stamp is
* available, then it must have been for this skb here because we only * available, then it must have been for this skb here because we only
* allow only one such packet into the queue. * allow only one such packet into the queue.
*
* Context: Expects adapter->ptp_tx_lock to be held by caller.
*/ */
static void igc_ptp_tx_hwtstamp(struct igc_adapter *adapter) static void igc_ptp_tx_hwtstamp(struct igc_adapter *adapter)
{ {
@ -396,13 +398,7 @@ static void igc_ptp_tx_hwtstamp(struct igc_adapter *adapter)
shhwtstamps.hwtstamp = shhwtstamps.hwtstamp =
ktime_add_ns(shhwtstamps.hwtstamp, adjust); ktime_add_ns(shhwtstamps.hwtstamp, adjust);
/* Clear the lock early before calling skb_tstamp_tx so that
* applications are not woken up before the lock bit is clear. We use
* a copy of the skb pointer to ensure other threads can't change it
* while we're notifying the stack.
*/
adapter->ptp_tx_skb = NULL; adapter->ptp_tx_skb = NULL;
clear_bit_unlock(__IGC_PTP_TX_IN_PROGRESS, &adapter->state);
/* Notify the stack and free the skb after we've unlocked */ /* Notify the stack and free the skb after we've unlocked */
skb_tstamp_tx(skb, &shhwtstamps); skb_tstamp_tx(skb, &shhwtstamps);
@ -413,24 +409,33 @@ static void igc_ptp_tx_hwtstamp(struct igc_adapter *adapter)
* igc_ptp_tx_work * igc_ptp_tx_work
* @work: pointer to work struct * @work: pointer to work struct
* *
* This work function polls the TSYNCTXCTL valid bit to determine when a * This work function checks the TSYNCTXCTL valid bit to determine when
* timestamp has been taken for the current stored skb. * a timestamp has been taken for the current stored skb.
*/ */
static void igc_ptp_tx_work(struct work_struct *work) static void igc_ptp_tx_work(struct work_struct *work)
{ {
struct igc_adapter *adapter = container_of(work, struct igc_adapter, struct igc_adapter *adapter = container_of(work, struct igc_adapter,
ptp_tx_work); ptp_tx_work);
struct igc_hw *hw = &adapter->hw; struct igc_hw *hw = &adapter->hw;
unsigned long flags;
u32 tsynctxctl; u32 tsynctxctl;
if (!test_bit(__IGC_PTP_TX_IN_PROGRESS, &adapter->state)) spin_lock_irqsave(&adapter->ptp_tx_lock, flags);
return;
if (!adapter->ptp_tx_skb)
goto unlock;
tsynctxctl = rd32(IGC_TSYNCTXCTL); tsynctxctl = rd32(IGC_TSYNCTXCTL);
if (WARN_ON_ONCE(!(tsynctxctl & IGC_TSYNCTXCTL_TXTT_0))) tsynctxctl &= IGC_TSYNCTXCTL_TXTT_0;
return; if (!tsynctxctl) {
WARN_ONCE(1, "Received a TSTAMP interrupt but no TSTAMP is ready.\n");
goto unlock;
}
igc_ptp_tx_hwtstamp(adapter); igc_ptp_tx_hwtstamp(adapter);
unlock:
spin_unlock_irqrestore(&adapter->ptp_tx_lock, flags);
} }
/** /**
@ -506,6 +511,7 @@ void igc_ptp_init(struct igc_adapter *adapter)
return; return;
} }
spin_lock_init(&adapter->ptp_tx_lock);
spin_lock_init(&adapter->tmreg_lock); spin_lock_init(&adapter->tmreg_lock);
INIT_WORK(&adapter->ptp_tx_work, igc_ptp_tx_work); INIT_WORK(&adapter->ptp_tx_work, igc_ptp_tx_work);
@ -559,7 +565,6 @@ void igc_ptp_suspend(struct igc_adapter *adapter)
cancel_work_sync(&adapter->ptp_tx_work); cancel_work_sync(&adapter->ptp_tx_work);
dev_kfree_skb_any(adapter->ptp_tx_skb); dev_kfree_skb_any(adapter->ptp_tx_skb);
adapter->ptp_tx_skb = NULL; adapter->ptp_tx_skb = NULL;
clear_bit_unlock(__IGC_PTP_TX_IN_PROGRESS, &adapter->state);
if (pci_device_is_present(adapter->pdev)) if (pci_device_is_present(adapter->pdev))
igc_ptp_time_save(adapter); igc_ptp_time_save(adapter);