diff --git a/drivers/net/ovpn/tcp.c b/drivers/net/ovpn/tcp.c
index 433bd07a4f1b..6cf684699ada 100644
--- a/drivers/net/ovpn/tcp.c
+++ b/drivers/net/ovpn/tcp.c
@@ -148,10 +148,14 @@ static void ovpn_tcp_rcv(struct strparser *strp, struct sk_buff *skb)
 	ovpn_recv(peer, skb);
 	return;
 err:
-	/* take reference for deferred peer deletion. should never fail */
-	if (WARN_ON(!ovpn_peer_hold(peer)))
-		goto err_nopeer;
-	schedule_work(&peer->tcp.defer_del_work);
+	/* schedule deferred peer deletion and take a reference only if the
+	 * work was actually queued: the matching ovpn_peer_put() in
+	 * ovpn_tcp_peer_del_work() runs once per queued work, so re-arming an
+	 * already-pending work must not take another reference (it would be
+	 * leaked, e.g. on a flood of invalid packets)
+	 */
+	if (schedule_work(&peer->tcp.defer_del_work))
+		ovpn_peer_hold(peer);
 	ovpn_dev_dstats_rx_dropped(peer->ovpn->dev);
 err_nopeer:
 	kfree_skb(skb);
@@ -280,15 +284,20 @@ static void ovpn_tcp_send_sock(struct ovpn_peer *peer, struct sock *sk)
 					     peer->id, ret);
 
 			/* in case of TCP error we can't recover the VPN
-			 * stream therefore we abort the connection
+			 * stream therefore we abort the connection.
+			 *
+			 * Take a reference only if the work was actually
+			 * queued: ovpn_tcp_peer_del_work() drops exactly one
+			 * reference per run, so re-arming an already-pending
+			 * work (e.g. already scheduled from the RX path) must
+			 * not take another reference (it would be leaked).
 			 */
-			ovpn_peer_hold(peer);
-			schedule_work(&peer->tcp.defer_del_work);
+			if (schedule_work(&peer->tcp.defer_del_work))
+				ovpn_peer_hold(peer);
 
 			/* we bail out immediately and keep tx_in_progress set
-			 * to true. This way we prevent more TX attempts
-			 * which would lead to more invocations of
-			 * schedule_work()
+			 * to true, so that no further TX is attempted on the
+			 * aborted stream
 			 */
 			return;
 		}
