Linux wireless drivers development
 help / color / mirror / Atom feed
* [RFC PATCH 2/3] wifi: ath11k: make external IRQ control idempotent
From: Mark Ruvald Pedersen @ 2026-07-19 21:58 UTC (permalink / raw)
  To: linux-wireless; +Cc: Jeff Johnson, ath11k
In-Reply-To: <20260719215817.2468580-1-wabsie@gmail.com>

ath11k tracks each external IRQ group's NAPI lifecycle with napi_enabled,
but calls the physical IRQ enable and disable helpers outside the state
guards. Repeating a lifecycle disable therefore increments Linux's IRQ
disable depth on AHB and multi-MSI PCI even when NAPI is already
disabled. One later enable leaves the IRQ masked.

Move each physical group transition inside the matching NAPI state
transition. Preserve the ordering: mask IRQs before synchronizing and
disabling NAPI, then enable NAPI before unmasking IRQs. The outer
synchronize_irq() calls remain unchanged.

This makes sequential lifecycle transitions idempotent. It does not
change the temporary disable/enable pairing in interrupt handlers and
NAPI poll completion.

Fixes: d943fdad7589 ("ath11k: Fix napi related hang")
Fixes: bbfdc5a751a6 ("ath11k: Refactor PCI code to support WCN6750")
Tested-on: IPQ8074 hw2.0 AHB WLAN.HK.2.9.0.1-02146-QCAHKSWPL_SILICONZ-1
Signed-off-by: Mark Ruvald Pedersen <wabsie@gmail.com>
---
diff --git a/drivers/net/wireless/ath/ath11k/ahb.c b/drivers/net/wireless/ath/ath11k/ahb.c
index 1e1dea485..1f1562ff3 100644
--- a/drivers/net/wireless/ath/ath11k/ahb.c
+++ b/drivers/net/wireless/ath/ath11k/ahb.c
@@ -235,9 +235,9 @@ static void __ath11k_ahb_ext_irq_disable(struct ath11k_base *ab)
 	for (i = 0; i < ATH11K_EXT_IRQ_GRP_NUM_MAX; i++) {
 		struct ath11k_ext_irq_grp *irq_grp = &ab->ext_irq_grp[i];
 
-		ath11k_ahb_ext_grp_disable(irq_grp);
-
 		if (irq_grp->napi_enabled) {
+			ath11k_ahb_ext_grp_disable(irq_grp);
+
 			napi_synchronize(&irq_grp->napi);
 			napi_disable(&irq_grp->napi);
 			irq_grp->napi_enabled = false;
@@ -380,8 +380,8 @@ static void ath11k_ahb_ext_irq_enable(struct ath11k_base *ab)
 		if (!irq_grp->napi_enabled) {
 			napi_enable(&irq_grp->napi);
 			irq_grp->napi_enabled = true;
+			ath11k_ahb_ext_grp_enable(irq_grp);
 		}
-		ath11k_ahb_ext_grp_enable(irq_grp);
 	}
 }
 
diff --git a/drivers/net/wireless/ath/ath11k/pcic.c b/drivers/net/wireless/ath/ath11k/pcic.c
index 2259adc3b..bf367f60b 100644
--- a/drivers/net/wireless/ath/ath11k/pcic.c
+++ b/drivers/net/wireless/ath/ath11k/pcic.c
@@ -455,9 +455,9 @@ static void __ath11k_pcic_ext_irq_disable(struct ath11k_base *ab)
 	for (i = 0; i < ATH11K_EXT_IRQ_GRP_NUM_MAX; i++) {
 		struct ath11k_ext_irq_grp *irq_grp = &ab->ext_irq_grp[i];
 
-		ath11k_pcic_ext_grp_disable(irq_grp);
-
 		if (irq_grp->napi_enabled) {
+			ath11k_pcic_ext_grp_disable(irq_grp);
+
 			napi_synchronize(&irq_grp->napi);
 			napi_disable(&irq_grp->napi);
 			irq_grp->napi_enabled = false;
@@ -490,8 +490,8 @@ void ath11k_pcic_ext_irq_enable(struct ath11k_base *ab)
 		if (!irq_grp->napi_enabled) {
 			napi_enable(&irq_grp->napi);
 			irq_grp->napi_enabled = true;
+			ath11k_pcic_ext_grp_enable(irq_grp);
 		}
-		ath11k_pcic_ext_grp_enable(irq_grp);
 	}
 
 	set_bit(ATH11K_FLAG_EXT_IRQ_ENABLED, &ab->dev_flags);

^ permalink raw reply related

* [RFC PATCH 1/3] wifi: ath11k: reserve headroom when aligning RX buffers
From: Mark Ruvald Pedersen @ 2026-07-19 21:58 UTC (permalink / raw)
  To: linux-wireless; +Cc: Jeff Johnson, ath11k
In-Reply-To: <20260719215817.2468580-1-wabsie@gmail.com>

RXDMA buffers are allocated with extra space for 128-byte alignment, but
the driver tries to move skb->data with skb_pull(). The skb is empty at
that point, so skb_pull() rejects every positive delta and leaves the
data pointer unchanged.

Use skb_reserve() while the skb is empty. The existing extra allocation
space leaves at least DP_RX_BUFFER_SIZE bytes of DMA tailroom after the
reservation.

Fixes: d5c65159f289 ("ath11k: driver for Qualcomm IEEE 802.11ax devices")
Tested-on: IPQ8074 hw2.0 AHB WLAN.HK.2.9.0.1-02146-QCAHKSWPL_SILICONZ-1
Signed-off-by: Mark Ruvald Pedersen <wabsie@gmail.com>
---
diff --git a/drivers/net/wireless/ath/ath11k/dp_rx.c b/drivers/net/wireless/ath/ath11k/dp_rx.c
index 33425707c..e4ea0c216 100644
--- a/drivers/net/wireless/ath/ath11k/dp_rx.c
+++ b/drivers/net/wireless/ath/ath11k/dp_rx.c
@@ -378,7 +378,7 @@ int ath11k_dp_rxbufs_replenish(struct ath11k_base *ab, int mac_id,
 
 		if (!IS_ALIGNED((unsigned long)skb->data,
 				DP_RX_BUFFER_ALIGN_SIZE)) {
-			skb_pull(skb,
-				 PTR_ALIGN(skb->data, DP_RX_BUFFER_ALIGN_SIZE) -
-				 skb->data);
+			skb_reserve(skb,
+				    PTR_ALIGN(skb->data, DP_RX_BUFFER_ALIGN_SIZE) -
+				    skb->data);
 		}
@@ -2875,7 +2875,7 @@ static struct sk_buff *ath11k_dp_rx_alloc_mon_status_buf(struct ath11k_base *ab,
 
 	if (!IS_ALIGNED((unsigned long)skb->data,
 			DP_RX_BUFFER_ALIGN_SIZE)) {
-		skb_pull(skb, PTR_ALIGN(skb->data, DP_RX_BUFFER_ALIGN_SIZE) -
+		skb_reserve(skb, PTR_ALIGN(skb->data, DP_RX_BUFFER_ALIGN_SIZE) -
 			 skb->data);
 	}
 

^ permalink raw reply related

* [RFC PATCH 0/3] wifi: ath11k: isolate RXDMA page-frag lifetimes
From: Mark Ruvald Pedersen @ 2026-07-19 21:58 UTC (permalink / raw)
  To: linux-wireless; +Cc: Jeff Johnson, ath11k

ath11k fills RXDMA rings with skb heads allocated by dev_alloc_skb().
Those allocations use shared per-CPU page-frag caches, so unrelated RXDMA
rings and other network users can place fragments with different
lifetimes on the same backing page.

On an IPQ8074-based Linksys MX4200, repeater/AP traffic made available
memory fall at roughly 12-16 MiB/min until OOM. Ring and IDR ownership
remained bounded while page-frag backing accumulated. Two ownership
captures found fragments from multiple allocation origins on 86-87% of
the final tracker-visible backing pages.

This RFC:

  1. fixes an existing no-op alignment attempt on empty RX skbs;
  2. makes sequential external IRQ/NAPI lifecycle transitions idempotent;
  3. quiesces non-reset crash recovery before DP teardown, gives each
     RXDMA ring a private page-frag cache, and drains it after the ring's
     DMA mappings and skbs have been released.

No RX buffer or ring sizes are changed, and the existing per-buffer DMA
map/unmap lifecycle is unchanged.

page_pool should also be viable, but it is not a mechanical allocator
substitution here. These skb heads need about 2.5 KiB of backing, so an
order-0 pool uses one 4 KiB page per buffer. An order-1 fragmented pool
restores packing density but lacks page_frag_cache's lower-order fallback
under fragmentation. page_pool also adds fallible setup/unwind and
requires every skb return path to become pool-aware. This RFC keeps those
behaviors unchanged; I can prototype page_pool if that is the preferred
ath11k direction.

The AHB version was built as an OpenWrt backport and remained bounded in
natural-load, resident, and high-packet-rate pressure runs. PCI has not
been runtime-tested. The non-reset firmware-recovery path has not been
validated in isolation, so feedback on patches 2 and 3 is especially
welcome. The IRQ state guards make repeated sequential calls safe; they
do not add synchronization between concurrent lifecycle callers.

OpenWrt discussion:
https://github.com/openwrt/openwrt/pull/24254

Mark Ruvald Pedersen (3):
  wifi: ath11k: reserve headroom when aligning RX buffers
  wifi: ath11k: make external IRQ control idempotent
  wifi: ath11k: use private page-frag caches for RXDMA rings

---
base-commit: 189721a4afa1804315e7dcfca9ca0539c7b1d7af

^ permalink raw reply

* Re: [RFC PATCH 1/2] wifi: ath10k: make in-order rx amsdu buffers persistent
From: David Heidelberg @ 2026-07-19 21:49 UTC (permalink / raw)
  To: Jeff Johnson, Richard Acayan, Jeff Johnson, linux-wireless,
	ath10k
In-Reply-To: <c7646164-134d-49c4-a1dc-0396876455e4@oss.qualcomm.com>

On 12/02/2026 18:38, Jeff Johnson wrote:
> On 2/9/2026 6:12 PM, Richard Acayan wrote:
> 
> some nits from my tooling...
> 
>> @@ -3299,16 +3302,32 @@ static int ath10k_htt_rx_in_ord_ind(struct ath10k *ar, struct sk_buff *skb)
>>   		return -EINVAL;
>>   	}
>>   
>> +	if (!skb_queue_empty(&htt->rx_in_ord_split)) {
>> +		/* It might still be possible to handle this case if there is
> 
> note that networking code no longer has a special block comment style, so all
> new block comments should use the standard style which has the opening "/*" on
> a line by itself.
> 
>> +		 * only one peer that splits at each given moment. We are
>> +		 * bailing out because we should have a test case for this
>> +		 * before trying to fix it.
>> +		 */
>> +		if (tid != htt->rx_in_ord_split_tid
>> +		 || peer_id != htt->rx_in_ord_split_peer_id
>> +		 || offload) {
> 
> checkpatch complains:
> LOGICAL_CONTINUATIONS: Logical continuations should be on the previous line
> LOGICAL_CONTINUATIONS: Logical continuations should be on the previous line
> 
>> +			ath10k_warn(ar, "split amsdu did not resume immediately\n");
>> +			htt->rx_confused = true;
>> +			return -EIO;
>> +		}
>> +	}
>> +
> Just wanted to get those out of the way before I look at the real content of
> the patch.

Hello Jeff,

followup is here:

https://lore.kernel.org/ath10k/20260719-ath10k-a-msdu-v2-1-f479bb9d1217@ixit.cz/

Thank you
David

> 
> /jeff
> 

^ permalink raw reply

* [PATCH RFC v2] wifi: ath10k: make in-order rx amsdu buffers persistent
From: David Heidelberg via B4 Relay @ 2026-07-19 21:45 UTC (permalink / raw)
  To: Jeff Johnson, Johannes Berg, Kalle Valo, Michal Kazior
  Cc: linux-wireless, ath10k, linux-arm-msm, linux-kernel, phone-devel,
	Richard Acayan, David Heidelberg

From: Richard Acayan <mailingradian@gmail.com>

The WCN3990 might split MSDUs among multiple "in-order" indications. The
driver needs information from previous indications to handle MPDUs that
are not started by the same indications that complete them. Move the
list that tracks unprocessed MSDUs to the driver state so the driver can
handle MPDUs that are split in this way and be less confused.

Fixes: c545070e404b ("ath10k: implement rx reorder support")
Signed-off-by: Richard Acayan <mailingradian@gmail.com>
Co-developed-by: David Heidelberg <david@ixit.cz>
Signed-off-by: David Heidelberg <david@ixit.cz>
---
This one we have in-tree forever.

Changes in v2:
- checkpatch & style. (Jeff)
- Improve comments.
- Link to v1: https://lore.kernel.org/linux-wireless/20260210021249.12132-2-mailingradian@gmail.com/
---
 drivers/net/wireless/ath/ath10k/htt.h    |  4 +++
 drivers/net/wireless/ath/ath10k/htt_rx.c | 50 +++++++++++++++++++++++++-------
 2 files changed, 43 insertions(+), 11 deletions(-)

diff --git a/drivers/net/wireless/ath/ath10k/htt.h b/drivers/net/wireless/ath/ath10k/htt.h
index 25c6b2e2f81c8..07c3dc088f07b 100644
--- a/drivers/net/wireless/ath/ath10k/htt.h
+++ b/drivers/net/wireless/ath/ath10k/htt.h
@@ -1924,16 +1924,20 @@ struct ath10k_htt {
 
 	bool tx_mem_allocated;
 	const struct ath10k_htt_tx_ops *tx_ops;
 	const struct ath10k_htt_rx_ops *rx_ops;
 	bool disable_tx_comp;
 	bool bundle_tx;
 	struct sk_buff_head tx_req_head;
 	struct sk_buff_head tx_complete_head;
+
+	u8 rx_in_ord_split_tid;
+	u16 rx_in_ord_split_peer_id;
+	struct sk_buff_head rx_in_ord_split;
 };
 
 struct ath10k_htt_tx_ops {
 	int (*htt_send_rx_ring_cfg)(struct ath10k_htt *htt);
 	int (*htt_send_frag_desc_bank_cfg)(struct ath10k_htt *htt);
 	int (*htt_alloc_frag_desc)(struct ath10k_htt *htt);
 	void (*htt_free_frag_desc)(struct ath10k_htt *htt);
 	int (*htt_tx)(struct ath10k_htt *htt, enum ath10k_hw_txrx_mode txmode,
diff --git a/drivers/net/wireless/ath/ath10k/htt_rx.c b/drivers/net/wireless/ath/ath10k/htt_rx.c
index ab2d373b4750d..732bd3a2f9992 100644
--- a/drivers/net/wireless/ath/ath10k/htt_rx.c
+++ b/drivers/net/wireless/ath/ath10k/htt_rx.c
@@ -291,16 +291,18 @@ void ath10k_htt_rx_free(struct ath10k_htt *htt)
 		return;
 
 	timer_delete_sync(&htt->rx_ring.refill_retry_timer);
 
 	skb_queue_purge(&htt->rx_msdus_q);
 	skb_queue_purge(&htt->rx_in_ord_compl_q);
 	skb_queue_purge(&htt->tx_fetch_ind_q);
 
+	skb_queue_purge(&htt->rx_in_ord_split);
+
 	spin_lock_bh(&htt->rx_ring.lock);
 	ath10k_htt_rx_ring_free(htt);
 	spin_unlock_bh(&htt->rx_ring.lock);
 
 	dma_free_coherent(htt->ar->dev,
 			  ath10k_htt_get_rx_ring_size(htt),
 			  ath10k_htt_get_vaddr_ring(htt),
 			  htt->rx_ring.base_paddr);
@@ -841,16 +843,18 @@ int ath10k_htt_rx_alloc(struct ath10k_htt *htt)
 	htt->rx_ring.sw_rd_idx.msdu_payld = 0;
 	hash_init(htt->rx_ring.skb_table);
 
 	skb_queue_head_init(&htt->rx_msdus_q);
 	skb_queue_head_init(&htt->rx_in_ord_compl_q);
 	skb_queue_head_init(&htt->tx_fetch_ind_q);
 	atomic_set(&htt->num_mpdus_ready, 0);
 
+	skb_queue_head_init(&htt->rx_in_ord_split);
+
 	ath10k_dbg(ar, ATH10K_DBG_BOOT, "htt rx ring size %d fill_level %d\n",
 		   htt->rx_ring.size, htt->rx_ring.fill_level);
 	return 0;
 
 err_dma_idx:
 	dma_free_coherent(htt->ar->dev,
 			  ath10k_htt_get_rx_ring_size(htt),
 			  vaddr_ring,
@@ -3156,16 +3160,20 @@ static int ath10k_htt_rx_extract_amsdu(struct ath10k_hw_params *hw,
 	struct rx_msdu_end_common *rxd_msdu_end_common;
 
 	if (skb_queue_empty(list))
 		return -ENOBUFS;
 
 	if (WARN_ON(!skb_queue_empty(amsdu)))
 		return -EINVAL;
 
+	msdu = skb_peek(list);
+	rxd = HTT_RX_BUF_TO_RX_DESC(hw,
+				    (void *)msdu->data - hw->rx_desc_ops->rx_desc_size);
+
 	while ((msdu = __skb_dequeue(list))) {
 		__skb_queue_tail(amsdu, msdu);
 
 		rxd = HTT_RX_BUF_TO_RX_DESC(hw,
 					    (void *)msdu->data -
 					    hw->rx_desc_ops->rx_desc_size);
 
 		rxd_msdu_end_common = ath10k_htt_rx_desc_get_msdu_end(hw, rxd);
@@ -3257,17 +3265,16 @@ static void ath10k_htt_rx_h_rx_offload(struct ath10k *ar,
 	}
 }
 
 static int ath10k_htt_rx_in_ord_ind(struct ath10k *ar, struct sk_buff *skb)
 {
 	struct ath10k_htt *htt = &ar->htt;
 	struct htt_resp *resp = (void *)skb->data;
 	struct ieee80211_rx_status *status = &htt->rx_status;
-	struct sk_buff_head list;
 	struct sk_buff_head amsdu;
 	u16 peer_id;
 	u16 msdu_count;
 	u8 vdev_id;
 	u8 tid;
 	bool offload;
 	bool frag;
 	int ret;
@@ -3292,64 +3299,85 @@ static int ath10k_htt_rx_in_ord_ind(struct ath10k *ar, struct sk_buff *skb)
 		   "htt rx in ord vdev %i peer %i tid %i offload %i frag %i msdu count %i\n",
 		   vdev_id, peer_id, tid, offload, frag, msdu_count);
 
 	if (skb->len < msdu_count * sizeof(*resp->rx_in_ord_ind.msdu_descs32)) {
 		ath10k_warn(ar, "dropping invalid in order rx indication\n");
 		return -EINVAL;
 	}
 
-	/* The event can deliver more than 1 A-MSDU. Each A-MSDU is later
-	 * extracted and processed.
+	if (!skb_queue_empty(&htt->rx_in_ord_split)) {
+		/*
+		 * It might still be possible to handle this case if there is
+		 * only one peer that splits at each given moment. We are
+		 * bailing out because we should have a test case for this
+		 * before trying to fix it.
+		 */
+		if (tid != htt->rx_in_ord_split_tid ||
+		    peer_id != htt->rx_in_ord_split_peer_id ||
+		    offload) {
+			ath10k_warn(ar, "split amsdu did not resume immediately\n");
+			htt->rx_confused = true;
+			ath10k_core_start_recovery(ar);
+			return -EIO;
+		}
+	}
+
+	/*
+	 * The event can deliver more than 1 A-MSDU or continue a previous one.
+	 * Each A-MSDU is later extracted and processed.
 	 */
-	__skb_queue_head_init(&list);
 	if (ar->hw_params.target_64bit)
 		ret = ath10k_htt_rx_pop_paddr64_list(htt, &resp->rx_in_ord_ind,
-						     &list);
+						     &htt->rx_in_ord_split);
 	else
 		ret = ath10k_htt_rx_pop_paddr32_list(htt, &resp->rx_in_ord_ind,
-						     &list);
+						     &htt->rx_in_ord_split);
 
 	if (ret < 0) {
 		ath10k_warn(ar, "failed to pop paddr list: %d\n", ret);
 		htt->rx_confused = true;
 		ath10k_core_start_recovery(ar);
 		return -EIO;
 	}
 
 	/* Offloaded frames are very different and need to be handled
 	 * separately.
 	 */
 	if (offload)
-		ath10k_htt_rx_h_rx_offload(ar, &list);
+		ath10k_htt_rx_h_rx_offload(ar, &htt->rx_in_ord_split);
 
-	while (!skb_queue_empty(&list)) {
+	while (!skb_queue_empty(&htt->rx_in_ord_split)) {
 		__skb_queue_head_init(&amsdu);
-		ret = ath10k_htt_rx_extract_amsdu(&ar->hw_params, &list, &amsdu);
+		ret = ath10k_htt_rx_extract_amsdu(&ar->hw_params,
+						  &htt->rx_in_ord_split, &amsdu);
 		switch (ret) {
 		case 0:
 			/* Note: The in-order indication may report interleaved
 			 * frames from different PPDUs meaning reported rx rate
 			 * to mac80211 isn't accurate/reliable. It's still
 			 * better to report something than nothing though. This
 			 * should still give an idea about rx rate to the user.
 			 */
 			ath10k_htt_rx_h_ppdu(ar, &amsdu, status, vdev_id);
 			ath10k_htt_rx_h_filter(ar, &amsdu, status, NULL);
 			ath10k_htt_rx_h_mpdu(ar, &amsdu, status, false, NULL,
 					     NULL, peer_id, frag);
 			ath10k_htt_rx_h_enqueue(ar, &amsdu, status);
 			break;
 		case -EAGAIN:
-			fallthrough;
+			htt->rx_in_ord_split_tid = tid;
+			htt->rx_in_ord_split_peer_id = peer_id;
+
+			return -EIO;
 		default:
 			/* Should not happen. */
 			ath10k_warn(ar, "failed to extract amsdu: %d\n", ret);
 			htt->rx_confused = true;
-			__skb_queue_purge(&list);
+			__skb_queue_purge(&htt->rx_in_ord_split);
 			ath10k_core_start_recovery(ar);
 			return -EIO;
 		}
 	}
 	return ret;
 }
 
 static void ath10k_htt_rx_tx_fetch_resp_id_confirm(struct ath10k *ar,

---
base-commit: 0718283ab28bc3907e10b61a6b4be6fefa1cbb2f
change-id: 20260719-ath10k-a-msdu-c49334eb091d

Best regards,
-- 
David Heidelberg <david@ixit.cz>



^ permalink raw reply related

* Re: [RFC PATCH v1 0/9] wifi: rtw88: add RTL8723B/RTL8723BS support
From: Luka Gejak @ 2026-07-19 18:12 UTC (permalink / raw)
  To: Peter Robinson, linux-wireless; +Cc: pkshih, straube.linux, luka.gejak
In-Reply-To: <CALeDE9PgQmpMfDt1DgfLD4tBFGH0MZ7GncV6RUEOHhHbKF+TdQ@mail.gmail.com>

Hi Peter,
Thanks a lot for testing on the Pine64 boards, and for the Tested-by. 
I will carry it on the series in v2.

> rtw88_8723bs mmc0:0001:1: failed to get tx report from firmware

I looked into this one. The v41 firmware does send the per-frame CCX TX
reports, but on some SDIO hosts they arrive after the 500 ms report
purge window, so the queued entries are dropped and the warning fires.
On a faster host here every report matched in time and the warning never
appeared, which matches your slower boards hitting it. v2 extends the
longer report timeout already used for 8723D USB to cover 8723BS SDIO,
which should clear these. I would appreciate a retest on the Pine64s
once v2 is out, since the race does not reproduce here.

> rtw88_8723bs mmc0:0001:1: firmware failed to leave lps state

The leave-LPS path polls REG_TCR for the firmware to finish its
null-packet handshake. The v41 firmware does not complete it the way the
poll expects, so it times out and force-clears the bit. The end state is
correct (hence no functional impact you saw), but the warning is noise.
I am still characterising whether to extend the poll or skip it for this
firmware rather than just silence the message, and will address it
properly.

> There's precedent for including the realtek firmware in linux-firmware
> like this, you should include in the notes where you got the firmware
> from and how it was extracted so it's verifiable, as well as cc:ing
> Ping-Ke Shih who will then ACK/review it.

Thanks, that is exactly what I needed. I have sent a linux-firmware
submission for rtw8723b_fw.bin with the extraction provenance documented
(vendor driver version, source array, and the convert_firmware tool),
with Ping-Ke Shih on CC. The v2 cover letter will reference it.

> I am happy to see this, I had started to play around with a similar
> patchset but still had a way to go and little time to move it on.

Glad it is useful. Happy to have you review or test v2 when it lands.
Best regards,
Luka Gejak

^ permalink raw reply

* [PATCH] rtw88: add firmware v41.0.0 for RTL8723B
From: luka.gejak @ 2026-07-19 18:00 UTC (permalink / raw)
  To: linux-firmware; +Cc: Ping-Ke Shih, linux-wireless, luka.gejak

From: Luka Gejak <luka.gejak@linux.dev>

Add the RTL8723B NIC firmware image used by the rtw88 driver. Support for
the RTL8723BS (SDIO) device that uses it is being added to the mainline
rtw88 driver.

The image was extracted from the Realtek "rtl8723bs" vendor driver,
version v5.2.17.1_26955.20180307_COEX20180201-6f52, from the C array
array_mp_8723b_fw_nic[] in hal/rtl8723b/hal8723b_fw.c, using the
convert_firmware.c tool. A public copy of that vendor driver is at
https://github.com/MocLG/rtl8723bs-5.2.17

The image reports version 41.0.0 and is covered by the existing Realtek
redistribution licence (LICENCE.rtlwifi_firmware.txt).

Signed-off-by: Luka Gejak <luka.gejak@linux.dev>
---
 WHENCE                |   1 +
 rtw88/rtw8723b_fw.bin | Bin 0 -> 32272 bytes
 2 files changed, 1 insertion(+)
 create mode 100644 rtw88/rtw8723b_fw.bin

diff --git a/WHENCE b/WHENCE
index 6cfe142d4e91..b4b7e6acda1c 100644
--- a/WHENCE
+++ b/WHENCE
@@ -3352,6 +3352,7 @@ File: rtw88/rtw8822c_fw.bin
 File: rtw88/rtw8822c_wow_fw.bin
 File: rtw88/README
 File: rtw88/rtw8723d_fw.bin
+File: rtw88/rtw8723b_fw.bin
 File: rtw88/rtw8821c_fw.bin
 File: rtw88/rtw8703b_fw.bin
 File: rtw88/rtw8703b_wow_fw.bin
diff --git a/rtw88/rtw8723b_fw.bin b/rtw88/rtw8723b_fw.bin
new file mode 100644
index 0000000000000000000000000000000000000000..2058fda105c4b8ec86c2836267e7ec817847f404
GIT binary patch
literal 32272
zcmV+re(!++QxE_t00007B@{#OeE<L&H2?qr000000000000002Mk)e;EdT%j00000
z000000007kPXGV_00000000000007jVE_OC000000000000000000000000000000
z0007jhyVZp0007q=Kufz0007z1RwDK4*&oF5AY8F0000F@c$10000m04*&oF01)v1
z4*&oF5bzHG0002>4*&oF008h00000001W^D000005b*#10000G@IL?m000ID2nh-c
z3=IwrGBPAGGDAy4OG8UpGBQI;LrYm&GBPAGGDAy4OG8UpSXfwCSXfwCQd3k_Q&d$}
zS6EqER##YAT3cLQ00001000320RjR8009930RjR80RaI40RjUA1qKHQ0RjUA1qKHQ
z0t5&U5EK|393UVd5EK|393UVd0R#dB1Op5N3<CfF0RjO50s{mF2nhiK0|W*L2?`ko
z1OfmU000000000000000000000000000000000000000000000000000000000000
z00000000000000000000000000000000000000000000000000000000000000000
z00000000000000000000000000000000000000000000000000000000000000000
z00000000000000000000000000000000000000000000000000000000000000000
z00000000000000000000000000000000000000000000000000000000000000000
z00000000000000000000000000000000000000000000000000000000000000000
z000000000000000000000002OuYmqC5<--P&<l0Y2&&-1jOB_XXZ4EZj5K-mjM9wk
zB!~FsyaEDr|HGAmv_Jqr&UpiPfEMNma0|k(<}l#{L>TtcuL#-g>Wk1K<qZELB!T!3
z2nfFe1b+Z{f#xve@&M%w!=F$f1q^hCB@FoH{icBk=B^CB0s?gZ&G_hTVBiSmz~F%P
z<qXrGKquu4B!~Fst_;2c0(AfV80O9R<$-Nr2GHR47=ZQV3?zog_7x0()8z~!B!T#U
z1j4W8FyI3a;tYQPFyRL<;{<;QRP<DQ#?r4;fI<f%c!s8dbOK-o{|I*f-}`?9<amJg
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

literal 0
KcmV+b0RR6000031

-- 
2.55.0


^ permalink raw reply related

* Re: [RFC PATCH net-next v0.1 1/1] net: add GeoNetworking protocol
From: Andrew Lunn @ 2026-07-19 17:28 UTC (permalink / raw)
  To: Simon Dietz
  Cc: netdev, andrew+netdev, davem, edumazet, johannes, kuniyu,
	linux-wireless, dietz23838
In-Reply-To: <20260718210046.2357882-2-simon.dietz@plantwatch.de>

> +#define GN_EPOCH_UNIX_MS 1072915232000LLU
> +
> +/* time until location table entry becomes invalid */
> +#define GN_LOC_TE_LIFETIME 20000
> +/* time between two beacons */
> +#define GN_BEACON_RETRANSMIT_TIME 3000
> +/* time between retransmits of an unanswered location service request */
> +#define GN_LS_RETRANSMIT_TIME 1000

It would be nice to have some units in these #defines. Classical bugs
are forgetting to convert from milliseconds to jiffies, or
milliseconds to microseconds. Having the unit in the name helpers
avoid that.

> +/* sizes in KiB */
> +#define GN_UC_BUF_SIZE 256
> +#define GN_BC_BUF_SIZE 1024
> +#define GN_CBF_BUF_SIZE 256

Again, units would be useful, the default would be to these are bytes,
not KB.

There is also linux/sizes.h.

	Andrew

^ permalink raw reply

* Re: [RFC PATCH net-next v0.1 1/1] net: add GeoNetworking protocol
From: Andrew Lunn @ 2026-07-19 17:16 UTC (permalink / raw)
  To: Simon Dietz
  Cc: netdev, andrew+netdev, davem, edumazet, johannes, kuniyu,
	linux-wireless, dietz23838
In-Reply-To: <20260718210046.2357882-2-simon.dietz@plantwatch.de>

Can this:

> +#define GN_MAX_HEADER_SZ 88

Be defined in terms of:

> +#define GN_GUC_HLEN sizeof(struct gn_guc_header)
> +#define GN_GXC_HLEN sizeof(struct gn_gxc_header)
> +#define GN_SHB_HLEN sizeof(struct gn_shb_header)
> +#define GN_TSB_HLEN sizeof(struct gn_tsb_header)
> +#define GN_BEACON_HLEN sizeof(struct gn_beacon_header)
> +#define GN_LS_REQUEST_HLEN sizeof(struct gn_ls_request_header)
> +#define GN_LS_REPLY_HLEN sizeof(struct gn_ls_reply_header)
> +#define BTP_HLEN sizeof(struct btp_header)

#define GN_MAX_HEADER_SZ (GN_GUC_HLEN + GN_GXC_HLEN + GN_SHB_HLEN + GN_TSB_HLEN + ...)

Doing it like this makes it also documentation, the assumption is, the
biggest header contains these sub headers.

> +/**
> + *	struct gn_iface - GeoNetworking interface
> + *	@dev - Network device associated with this interface
> + *	@address - Our address
> + *	@local_sn - Current sequence number
> + */
> +struct gn_iface {
> +	struct net_device	*dev;
> +	gn_address_t	address;
> +	struct gn_position pos;
> +	atomic_t local_sn;
> +	struct hlist_node hnode;
> +	struct rcu_head rcu;
> +};

It does not look like all members have the same indentation here. I
would suggest they are all indented to the same level, or none are
indented. This is a general comment for the code as a whole.

> +enum ITS_TYPE {
> +	UNKNOWN,

It is typical to use

	UNKNOWN = 0,

> +	PEDESTRIAN,
> +	CYCLIST,

and then the rest without explicit values. I don't know what the C
standard says, but maybe without the = 0, a language lawyer might
argue it is allowed to start at 42 and count upwards from there?

> +	MOPED,
> +	MOTORCYCLE,

Are these from the standard?  Maybe reference the clause in the
standard? The point being, we reviewers want to know if random
developer can add HOT_AIR_BALLOON, and HORSE_AND_TRAP at the end, or
if additions must first be accepted at the standards level and
assigned numbers.

	Andrew

^ permalink raw reply

* Re: [RFC PATCH net-next v0.1 0/1] add GeoNetworking protocol
From: Andrew Lunn @ 2026-07-19 16:21 UTC (permalink / raw)
  To: Simon Dietz
  Cc: netdev, andrew+netdev, davem, edumazet, johannes, kuniyu,
	linux-wireless, dietz23838
In-Reply-To: <20260718210046.2357882-1-simon.dietz@plantwatch.de>

On Sat, Jul 18, 2026 at 11:00:32PM +0200, Simon Dietz wrote:
> Implement the GeoNetworking / ETSI ITS-G5 ('net/gn') protocol which
> is based on 802.11p wifi and used for vehicle2x applications. It is
> standardized by the ETSI and used by some car manufacturers
> (especially in europe). It enables ad-hoc, multi-hop geographical
> communication and routing among vehicles (and road- or railside
> infrastructure).
> 
> Most work of this implementation has been done by the bachelor
> project 2018/2019 of the operating systems and middleware group of
> the Hasso Plattner Institute, University of Potsdam, which the author
> was part of.

Hi Simon

Is there are architecture documentation somewhere?

One of my comments was about routing tables. Should there be a user
space component determining the routes, and the kernel just has a
static routing table? That would be typical for IP.

There also seems to be a need for location information. How does that
get into the kernel? Is there a daemon for that? Patches to gpsd?

    Andrew

^ permalink raw reply

* Re: [PATCH] wifi: cfg80211: Constify struct cfg80211_ops
From: Jeff Johnson @ 2026-07-19 15:02 UTC (permalink / raw)
  To: Christophe JAILLET; +Cc: linux-kernel, kernel-janitors, linux-wireless
In-Reply-To: <5aace954b6ef5c42017b83a1bffb859618e9498a.1784013180.git.christophe.jaillet@wanadoo.fr>


On Tue, 14 Jul 2026 09:14:00 +0200, Christophe JAILLET wrote:
> 'struct cfg80211_ops' is not modified in this driver.
> 
> Constifying this structure moves some data to a read-only section, so
> increases overall security, especially when the structure holds some
> function pointers.
> 
> On a x86_64, with allmodconfig, as an example:
> Before:
> ======
>    text	   data	    bss	    dec	    hex	filename
>  143726	  34579	    192	 178497	  2b941	drivers/net/wireless/ath/ath6kl/cfg80211.o
> 
> [...]

Applied, thanks!

[1/1] wifi: cfg80211: Constify struct cfg80211_ops
      commit: 612701854b588759c0df3094197d415869ada159

Best regards,
-- 
Jeff Johnson <jeff.johnson@oss.qualcomm.com>


^ permalink raw reply

* Re: [PATCH ath-next] wifi: ath12k: Set DTIM policy to stick mode for station interface
From: Jeff Johnson @ 2026-07-19 15:02 UTC (permalink / raw)
  To: jjohnson, Daizhuang Bai; +Cc: ath12k, linux-wireless, linux-kernel
In-Reply-To: <20260717014302.284034-1-daizhuang.bai@oss.qualcomm.com>


On Fri, 17 Jul 2026 07:13:02 +0530, Daizhuang Bai wrote:
> Currently, the station always follows the listen interval regardless of
> the DTIM value. The DTIM function does not work as expected. The default
> value of the listen interval is 5 so that the STA wakes up every 500ms
> when power save is on. This can cause a data transmission delay.
> 
> Set the DTIM policy to DTIM stick mode so that the station follows the AP
> DTIM interval rather than the listen interval, which is set in the peer
> assoc command. DTIM stick mode is preferable per the firmware team's request.
> 
> [...]

Applied, thanks!

[1/1] wifi: ath12k: Set DTIM policy to stick mode for station interface
      commit: 4834ac1d4894a2c3513e8f28d5693170829b1e68

Best regards,
-- 
Jeff Johnson <jeff.johnson@oss.qualcomm.com>


^ permalink raw reply

* Re: [PATCH v3] wifi: ath12k: fix survey indexing across bands
From: Rameshkumar Sundaram @ 2026-07-19 13:51 UTC (permalink / raw)
  To: Matthew Leach, Jeff Johnson, Vasanthakumar Thiagarajan,
	Baochen Qiang, Ramya Gnanasekar, Karthikeyan Periyasamy
  Cc: Kalle Valo, Pradeep Kumar Chitrapu, P Praneesh, Sriram R,
	linux-wireless, ath12k, linux-kernel, kernel, Nicolas Escande,
	Rameshkumar Sundaram, Jeff Johnson
In-Reply-To: <20260703-ath12-survey-band-fix-v3-1-2fb050c2505a@collabora.com>

On 7/3/2026 9:26 PM, Matthew Leach wrote:
> When running 'iw dev wlan0 survey dump' the values for the channel busy
> time have the same sequence across bands. This is caused by indexing
> into the ath12k survey array using a band-local index rather than the
> global index passed by mac80211. This results in surveys for 5 GHz and 6
> GHz channels returning values from 2.4 GHz slots, making the survey
> unusable on those bands. Further, there are redundant survey slots for
> multi-radio/single-phy instances.
> 
> Fix by moving the survey data into ath12k_hw so multiple radios under a
> single wiphy share one table, and index into it using the global
> mac80211 index. A new spinlock in ath12k_hw serialises access to the
> survey array, which is now shared across all radios under a single hw.
> 
> Band busy-times Before this fix:
> 
> 2.4 GHz: 9, 2, 2, 2, 4, 2, 10, 16, 4, 12, 5
> 5 GHz:   9, 2, 2, 2, 4, 2, 10, 16, 4, 12, 5
> 6 GHz:   9, 2, 2, 2, 4, 2, 10, 16, 4, 12, 5
> 
> After this fix, times are independent:
> 
> 2.4 GHz: 23, 5,  5,  12, 2,   12,  26,  5,   3,  1,  27
> 5 GHz:   30, 40, 29, 27, 118, 118, 112, 120, 11, 11, 11
> 6 GHz:   1,  0,  0,  0,  0,   0,   0,   0,   0,  0,  1
> 
> Tested-on: wcn7850 hw2.0 PCI WLAN.IOE_HMT.1.1-00018-QCAHMTSWPL_V1.0_V2.0_SILICONZ-1
> 
> Fixes: 4f242b1d6996 ("wifi: ath12k: support get_survey mac op for single wiphy")
> Signed-off-by: Matthew Leach <matthew.leach@collabora.com>
Reviewed-by: Rameshkumar Sundaram <rameshkumar.sundaram@oss.qualcomm.com>

^ permalink raw reply

* Re: [PATCH] nfc: trf7970a: Use NULL when no response is expected
From: David Heidelberg @ 2026-07-19 12:11 UTC (permalink / raw)
  To: Linmao Li, Mark Greer; +Cc: linux-wireless, oe-linux-nfc, linux-kernel
In-Reply-To: <20260706075743.564658-1-lilinmao@kylinos.cn>


On Mon, 06 Jul 2026 15:57:42 +0800, Linmao Li wrote:
 > nfc: trf7970a: Use NULL when no response is expected

Applied, thanks!

[1/1] nfc: trf7970a: Use NULL when no response is expected
       commit: 32fff7986e5d1ebae98838f02cec2463dde5df9f

Best regards,
-- 
David Heidelberg <david@ixit.cz>

^ permalink raw reply

* pull-request: wifi: iwlwifi: fixes - 2026-07-19
From: Korenblit, Miriam Rachel @ 2026-07-19 10:58 UTC (permalink / raw)
  To: linux-wireless, Berg, Johannes

The following changes since commit a0d82fb8505326cbc53dc9a0c08f97d11197bb30:

  Merge tag 'wireless-2026-07-09' of https://git.kernel.org/pub/scm/linux/kernel/git/wireless/wireless (2026-07-10 16:27:45 +0200)

are available in the Git repository at:

  https://git.kernel.org/pub/scm/linux/kernel/git/iwlwifi/iwlwifi-fixes.git/ iwlwifi-fixes-2026-07-19

for you to fetch changes up to d13d5d299c11b7bd3362d5692c56225d9e176664:

  wifi: iwlwifi: validate SEC_RT TLV minimum size (2026-07-18 22:37:38 +0300)

----------------------------------------------------------------
wifi: iwlwif: fixes - 2026-07-19

This PR contains quite a few fixes, mostly found by LLMs. Notably:
- missing validation of TLVs length
- missing validation on the revision of BIOS tables
- missing validation of fw responses length
- protect against double free of RX pointers
- avoid double deregistration of the tzone core
- validate fields in FW responses before using them
- fix off-by-one in WLAN_EID_EXT_CAPABILITY parsing
- Add missing support for UNII-9

----------------------------------------------------------------
Avraham Stern (2):
      wifi: iwlwifi: mvm: verify scan id reported by firmware
      wifi: iwlwifi: mvm: copy the correct TK length for ranging

Emmanuel Grumbach (30):
      wifi: iwlwifi: mvm: fix an off-by-1 boundary check
      wifi: iwlwifi: mld: fix an off-by-1 boundary check
      wifi: iwlwifi: mld: don't parse a notif before checking its length
      wifi: iwlwifi: mvm: fix the FCS truncation logic in d3
      wifi: iwlwifi: mld: validate reorder BAID
      wifi: iwlwifi: mvm: parse beacon notif per layout
      wifi: iwlwifi: mvm: validate MCC header before n_channels
      wifi: iwlwifi: mvm: validate sta_id in TLC notif
      wifi: iwlwifi: mvm: validate sta_id in BA window status notif
      wifi: iwlwifi: mvm: validate mac_link_id in session protect notif
      wifi: iwlwifi: mld: clear tzone on fail
      wifi: iwlwifi: mvm: fix sched scan IE sizing
      wifi: iwlwifi: pcie: null RX pointers after free
      wifi: iwlwifi: mvm: d3: validate D3 resume notification payloads
      wifi: iwlwifi: mld: validate txq_id in TX response handler
      wifi: iwlwifi: add support for additional channels in NVM_GET_INFO
      wifi: iwlwifi: mvm: validate TX_CMD response layout
      wifi: iwlwifi: fix pointer arithmetic in iwl_add_mcc_to_tas_block_list
      wifi: iwlwifi: validate payload length in iwl_pnvm_complete_fn
      wifi: iwlwifi: mvm: ptp: free response on success path
      wifi: iwlwifi: pcie: validate FW section counts in iwl_pcie_init_fw_sec
      wifi: iwlwifi: mvm: fix out-of-bounds tid_data access in BA notif
      wifi: iwlwifi: mvm: add a check on the tid coming from the firmware
      wifi: iwlwifi: mvm: fix a possible underflow
      wifi: iwlwifi: adapt ND match notif sizing to fixed matches array
      wifi: iwlwifi: bound aligned TLV advance in FW parser
      wifi: iwlwifi: dbg-tlv: bound aligned TLV walk length
      wifi: iwlwifi: acpi: validate WGDS table revision index
      wifi: iwlwifi: uefi: bound PPAG revision bitmap shift
      wifi: iwlwifi: validate SEC_RT TLV minimum size

Pagadala Yesu Anjaneyulu (1):
      wifi: iwlwifi: mvm: validate SAR GEO response payload size

Shahar Tzarfati (2):
      wifi: iwlwifi: fw: validate SMEM response size
      wifi: iwlwifi: mvm: fix read in wake packet notification handler

 drivers/net/wireless/intel/iwlwifi/cfg/rf-pe.c     |  1 +
 drivers/net/wireless/intel/iwlwifi/fw/acpi.c       |  5 ++
 .../net/wireless/intel/iwlwifi/fw/api/nvm-reg.h    | 33 ++++++++++-
 drivers/net/wireless/intel/iwlwifi/fw/api/scan.h   |  4 +-
 drivers/net/wireless/intel/iwlwifi/fw/pnvm.c       |  9 ++-
 drivers/net/wireless/intel/iwlwifi/fw/regulatory.c |  2 +-
 drivers/net/wireless/intel/iwlwifi/fw/smem.c       | 28 ++++++++--
 drivers/net/wireless/intel/iwlwifi/fw/uefi.c       |  2 +-
 drivers/net/wireless/intel/iwlwifi/iwl-config.h    |  2 +
 drivers/net/wireless/intel/iwlwifi/iwl-dbg-tlv.c   | 13 ++++-
 drivers/net/wireless/intel/iwlwifi/iwl-drv.c       | 18 +++++-
 drivers/net/wireless/intel/iwlwifi/iwl-nvm-parse.c | 64 ++++++++++++++--------
 drivers/net/wireless/intel/iwlwifi/mld/agg.c       |  7 ++-
 drivers/net/wireless/intel/iwlwifi/mld/mac80211.c  |  2 +-
 drivers/net/wireless/intel/iwlwifi/mld/mcc.c       | 10 +++-
 drivers/net/wireless/intel/iwlwifi/mld/thermal.c   |  9 ++-
 drivers/net/wireless/intel/iwlwifi/mld/tx.c        |  4 ++
 drivers/net/wireless/intel/iwlwifi/mvm/d3.c        | 46 +++++++++++-----
 .../net/wireless/intel/iwlwifi/mvm/ftm-initiator.c | 55 +++++++++++++------
 drivers/net/wireless/intel/iwlwifi/mvm/fw.c        | 10 ++++
 drivers/net/wireless/intel/iwlwifi/mvm/mac-ctxt.c  | 25 +++++----
 drivers/net/wireless/intel/iwlwifi/mvm/mac80211.c  |  7 +--
 drivers/net/wireless/intel/iwlwifi/mvm/mvm.h       |  3 -
 drivers/net/wireless/intel/iwlwifi/mvm/nvm.c       | 43 ++++++++++++---
 drivers/net/wireless/intel/iwlwifi/mvm/ptp.c       |  4 +-
 drivers/net/wireless/intel/iwlwifi/mvm/rs-fw.c     |  9 ++-
 drivers/net/wireless/intel/iwlwifi/mvm/rx.c        |  5 ++
 drivers/net/wireless/intel/iwlwifi/mvm/scan.c      | 10 +++-
 .../net/wireless/intel/iwlwifi/mvm/time-event.c    |  5 ++
 drivers/net/wireless/intel/iwlwifi/mvm/tx.c        | 45 ++++++++++++++-
 .../net/wireless/intel/iwlwifi/pcie/ctxt-info.c    |  7 ++-
 .../net/wireless/intel/iwlwifi/pcie/gen1_2/rx.c    |  9 ++-
 32 files changed, 381 insertions(+), 115 deletions(-)

^ permalink raw reply

* [PATCH mt76] wifi: mt76: mt7603: add 0x7592 EEPROM chip ID
From: Ahmed Naseef @ 2026-07-19  8:25 UTC (permalink / raw)
  To: Felix Fietkau, Lorenzo Bianconi, Ryder Lee, Shayne Chen,
	Sean Wang, Matthias Brugger, AngeloGioacchino Del Regno,
	linux-wireless, linux-kernel, linux-arm-kernel, linux-mediatek
  Cc: Ahmed Naseef

Some EcoNet based routers ship an on-flash EEPROM whose chip-id is
0x7592 instead of the expected 0x7603. The device probes as PCI
14c3:7603 and the hardware MT_HW_CHIPID register reports 0x7603,
independent of the EEPROM value.

This is seen across multiple EcoNet EN751221 and EN7528 based devices
(for example the Genexis Platinum 4410).

Signed-off-by: Ahmed Naseef <naseefkm@gmail.com>
---
 drivers/net/wireless/mediatek/mt76/mt7603/eeprom.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/drivers/net/wireless/mediatek/mt76/mt7603/eeprom.c b/drivers/net/wireless/mediatek/mt76/mt7603/eeprom.c
index b89db2db6573..05f7aaefaf89 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7603/eeprom.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7603/eeprom.c
@@ -142,6 +142,7 @@ static int mt7603_check_eeprom(struct mt76_dev *dev)
 	case 0x7628:
 	case 0x7603:
 	case 0x7600:
+	case 0x7592:
 		return 0;
 	default:
 		return -EINVAL;

base-commit: 50a7f9f9d48eb50c0e95bef53358acb5af5cb3c6
-- 
2.34.1


^ permalink raw reply related

* [PATCH RESEND mt76 v2] wifi: mt76: mt7915: add thermal zone device registration
From: Ryan Leung @ 2026-07-19  2:14 UTC (permalink / raw)
  To: Felix Fietkau, Lorenzo Bianconi, Ryder Lee, Shayne Chen,
	Sean Wang, Matthias Brugger, AngeloGioacchino Del Regno
  Cc: linux-wireless, linux-kernel, linux-arm-kernel, linux-mediatek,
	Ryan Leung

Register the mt7915 phy as a thermal zone sensor using
devm_thermal_of_zone_register() so that device tree thermal-zones
nodes can reference the Wi-Fi chip as a temperature source. This
allows the kernel thermal governor to control external cooling
devices such as PWM fans based on Wi-Fi chip temperature.

Registration is non-fatal: -ENODEV is returned when no
thermal-sensors DT property references this device, which is the
expected case on platforms without a thermal zone configured.

Signed-off-by: Ryan Leung <untilscour@protonmail.com>
---
v2: Use phy->mt76->band_idx instead of hardcoded 0 as sensor ID
---
 drivers/net/wireless/mediatek/mt76/mt7915/init.c   | 30 ++++++++++++++++++++++
 drivers/net/wireless/mediatek/mt76/mt7915/mt7915.h |  1 +
 2 files changed, 31 insertions(+)

diff --git a/drivers/net/wireless/mediatek/mt76/mt7915/init.c b/drivers/net/wireless/mediatek/mt76/mt7915/init.c
index 250c2d2479b0..6568d7b6bc0a 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7915/init.c
+++ b/drivers/net/wireless/mediatek/mt76/mt7915/init.c
@@ -177,6 +177,25 @@ static const struct thermal_cooling_device_ops mt7915_thermal_ops = {
 	.set_cur_state = mt7915_thermal_set_cur_throttle_state,
 };
 
+static int mt7915_thermal_get_temp(struct thermal_zone_device *tz, int *temp)
+{
+	struct mt7915_phy *phy = thermal_zone_device_priv(tz);
+	int val;
+
+	mutex_lock(&phy->dev->mt76.mutex);
+	val = mt7915_mcu_get_temperature(phy);
+	mutex_unlock(&phy->dev->mt76.mutex);
+	if (val < 0)
+		return val;
+
+	*temp = val * 1000;
+	return 0;
+}
+
+static const struct thermal_zone_device_ops mt7915_tz_ops = {
+	.get_temp = mt7915_thermal_get_temp,
+};
+
 static void mt7915_unregister_thermal(struct mt7915_phy *phy)
 {
 	struct wiphy *wiphy = phy->mt76->hw->wiphy;
@@ -213,6 +232,17 @@ static int mt7915_thermal_init(struct mt7915_phy *phy)
 	phy->throttle_temp[MT7915_CRIT_TEMP_IDX] = MT7915_CRIT_TEMP;
 	phy->throttle_temp[MT7915_MAX_TEMP_IDX] = MT7915_MAX_TEMP;
 
+	phy->tzone = devm_thermal_of_zone_register(phy->dev->mt76.dev,
+						   phy->mt76->band_idx, phy,
+						   &mt7915_tz_ops);
+	if (IS_ERR(phy->tzone)) {
+		if (PTR_ERR(phy->tzone) != -ENODEV)
+			dev_warn(phy->dev->mt76.dev,
+				 "failed to register thermal zone: %ld\n",
+				 PTR_ERR(phy->tzone));
+		phy->tzone = NULL;
+	}
+
 	if (!IS_REACHABLE(CONFIG_HWMON))
 		return 0;
 
diff --git a/drivers/net/wireless/mediatek/mt76/mt7915/mt7915.h b/drivers/net/wireless/mediatek/mt76/mt7915/mt7915.h
index bf1d915a3ca2..92e0f9f0169c 100644
--- a/drivers/net/wireless/mediatek/mt76/mt7915/mt7915.h
+++ b/drivers/net/wireless/mediatek/mt76/mt7915/mt7915.h
@@ -205,6 +205,7 @@ struct mt7915_phy {
 
 	struct ieee80211_vif *monitor_vif;
 
+	struct thermal_zone_device *tzone;
 	struct thermal_cooling_device *cdev;
 	u8 cdev_state;
 	u8 throttle_state;

---
base-commit: ac798f757d6475dc6fee2ec899980d6740714596
change-id: 20260718-mt7915-thermal-zone-device-registration-e6d7bdc80aa5

Best regards,
--  
Ryan Leung <untilscour@protonmail.com>



^ permalink raw reply related

* [PATCH] wifi: mt76: mt7996: validate UNI PS sync events
From: Zhiwen Zhou via B4 Relay @ 2026-07-18 22:38 UTC (permalink / raw)
  To: nbd; +Cc: shayne.chen, sean.wang, ryder.lee, lorenzo, linux-wireless,
	Zhiwen Zhou

From: Zhiwen Zhou <zhiwen_zhou@seu.edu.cn>

The firmware may send a PS sync event with a zero-length TLV or
trailing padding. The parser only checks whether the TLV length
exceeds the remaining data. A zero-length TLV therefore leaves both
the skb and remaining length unchanged and loops forever in the
threaded NAPI poller.

On affected MT7996 devices, this pins napi/phy0-0 in
mt7996_mcu_rx_event(), starves the RCU grace-period kthread and
eventually makes all radios unresponsive.

Validate the event header and every TLV before accessing it. Also
validate the variable-length multi-client array against the TLV
length. Drop malformed events with a rate-limited warning instead of
stalling the CPU.

Fixes: b0af99f238f7 ("wifi: mt76: mt7996: handle UNI PS sync events")
Signed-off-by: Zhiwen Zhou <zhiwen_zhou@seu.edu.cn>
---
 mt7996/mcu.c | 36 ++++++++++++++++++++++++++++++------
 1 file changed, 30 insertions(+), 6 deletions(-)

diff --git a/mt7996/mcu.c b/mt7996/mcu.c
index 1bd32c0..ee85b20 100644
--- a/mt7996/mcu.c
+++ b/mt7996/mcu.c
@@ -843,24 +843,33 @@ mt7996_mcu_rx_ps_sync(struct mt7996_dev *dev, struct sk_buff *skb)
 {
 	struct mt7996_mcu_ps_sync_event *event = (void *)skb->data;
 	struct tlv *tlv;
+	u16 tag = 0, tag_len = 0;
 	int len;
 
+	if (skb->len < sizeof(*event)) {
+		dev_warn_ratelimited(dev->mt76.dev,
+				     "short PS sync event: %u bytes\n",
+				     skb->len);
+		return;
+	}
+
 	skb_pull(skb, sizeof(*event));
 
 	len = skb->len;
-	while (len > sizeof(*tlv)) {
-		u16 tag, tag_len;
-
+	while (len >= sizeof(*tlv)) {
 		tlv = (struct tlv *)skb->data;
 		tag = le16_to_cpu(tlv->tag);
 		tag_len = le16_to_cpu(tlv->len);
-		if (tag_len > len)
-			break;
+		if (tag_len < sizeof(*tlv) || tag_len > len)
+			goto invalid;
 
 		switch (tag) {
 		case UNI_PS_CLIENT_INFO: {
 			struct mt7996_mcu_ps_client_info *info = (void *)tlv;
 
+			if (tag_len < sizeof(*info))
+				goto invalid;
+
 			mt7996_mcu_ps_transition(dev,
 						 le16_to_cpu(info->wlan_idx),
 						 info->ps_bit);
@@ -868,9 +877,17 @@ mt7996_mcu_rx_ps_sync(struct mt7996_dev *dev, struct sk_buff *skb)
 		}
 		case UNI_PS_MULTI_CLIENT_INFO: {
 			struct mt7996_mcu_ps_multi_client_info *info = (void *)tlv;
-			u16 cnt = le16_to_cpu(info->sta_cnt);
+			u16 cnt;
 			int i;
 
+			if (tag_len < sizeof(*info))
+				goto invalid;
+
+			cnt = le16_to_cpu(info->sta_cnt);
+			if (cnt > (tag_len - sizeof(*info)) /
+				  sizeof(info->sta_ps_info[0]))
+				goto invalid;
+
 			for (i = 0; i < cnt; i++) {
 				u16 entry = le16_to_cpu(info->sta_ps_info[i]);
 
@@ -895,6 +912,13 @@ mt7996_mcu_rx_ps_sync(struct mt7996_dev *dev, struct sk_buff *skb)
 		skb_pull(skb, tag_len);
 		len -= tag_len;
 	}
+
+	return;
+
+invalid:
+	dev_warn_ratelimited(dev->mt76.dev,
+			     "invalid PS sync TLV: tag=%u len=%u remaining=%d\n",
+			     tag, tag_len, len);
 }
 
 static void

---
base-commit: 59676919ea408b0b13a9d23f2e2e1a1ab407fba1
change-id: 20260719-fix-mt7996-ps-sync-tlv-validation-3a609b215c1e

Best regards,
--  
Zhiwen Zhou <zhiwen_zhou@seu.edu.cn>



^ permalink raw reply related

* [RFC PATCH net-next v0.1 1/1] net: add GeoNetworking protocol
From: Simon Dietz @ 2026-07-18 21:00 UTC (permalink / raw)
  To: netdev
  Cc: andrew+netdev, davem, edumazet, johannes, kuniyu, linux-wireless,
	simon.dietz, dietz23838
In-Reply-To: <20260718210046.2357882-1-simon.dietz@plantwatch.de>

Implement the GeoNetworking / ETSI ITS-G5 ('net/gn') protocol which
is based on 802.11p wifi and used for vehicle2x applications. It is
standardized by the ETSI and used by some car manufacturers
(especially in europe). It enables ad-hoc, multi-hop geographical
communication and routing among vehicles (and road- or railside
infrastructure).

Most work of this implementation has been done by the bachelor
project 2018/2019 of the operating systems and middleware group of
the Hasso Plattner Institute, University of Potsdam, which the author
was part of.

The code is published under the GPL v2 at this location:
https://gitlab.com/hpi-potsdam/osm/g5-on-linux/linux-geonetworking/

This patch set is not merge-ready, yet. E.g. network namespaces are
not supported at all, documentation is missing, ...

Purpose of this RFC is to determine if GeoNetworking support in the
linux kernel is desirable and if so what steps would have to be done
next.

In the original implementation new ioctl calls have been used. For the
removal of them and changing their functionality to use netlink (and
only this change) help of AI has been used as stated below.

Signed-off-by: Simon Dietz <simon.dietz@plantwatch.de>
Assisted-by: Gemini:gemini-3.1-pro antigravity-ide
---
 include/linux/gn.h                  |  363 +++++
 include/linux/gn_routing.h          |   68 +
 include/linux/socket.h              |    6 +-
 include/uapi/linux/gn.h             |   84 ++
 include/uapi/linux/if_ether.h       |    1 +
 include/uapi/linux/if_link.h        |   10 +
 net/Kconfig                         |    2 +
 net/Makefile                        |    1 +
 net/gn/Kconfig                      |    4 +
 net/gn/Makefile                     |    7 +
 net/gn/gn_netlink.c                 |  266 ++++
 net/gn/gn_prot.c                    | 1908 +++++++++++++++++++++++++++
 net/gn/gn_routing.c                 |  646 +++++++++
 net/gn/sysctl_net_gn.c              |   33 +
 security/selinux/hooks.c            |    5 +-
 security/selinux/include/classmap.h |    3 +-
 16 files changed, 3403 insertions(+), 4 deletions(-)
 create mode 100644 include/linux/gn.h
 create mode 100644 include/linux/gn_routing.h
 create mode 100644 include/uapi/linux/gn.h
 create mode 100644 net/gn/Kconfig
 create mode 100644 net/gn/Makefile
 create mode 100644 net/gn/gn_netlink.c
 create mode 100644 net/gn/gn_prot.c
 create mode 100644 net/gn/gn_routing.c
 create mode 100644 net/gn/sysctl_net_gn.c

diff --git a/include/linux/gn.h b/include/linux/gn.h
new file mode 100644
index 000000000000..5940e3d85cae
--- /dev/null
+++ b/include/linux/gn.h
@@ -0,0 +1,363 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifndef __LINUX_GN_H__
+#define __LINUX_GN_H__
+
+#include <net/sock.h>
+#include <uapi/linux/gn.h>
+#include <linux/types.h>
+#include <linux/atomic.h>
+#include <linux/bitfield.h>
+#include <asm/byteorder.h>
+#include <linux/etherdevice.h>
+
+#define GN_MAX_HEADER_SZ 88
+#define GN_MAXSZ (ETH_DATA_LEN - GN_MAX_HEADER_SZ)
+#define GN_MAXHOPS 255 /* 8 bits of hop counter */
+
+#define GN_VERSION 1
+#define GN_BROADCAST_ADDR 0xffffffffffffULL
+/* itsGnDefaultHopLimit: Default hop limit (for BH RHL/CH MHL) */
+#define DEFAULT_HOP_LIMIT 10
+
+/* UNIX timestamp in ms for the ETSI ITS Epoch (2004-01-01 00:00:00 TAI).
+ * 1072915200000 ms (2004-01-01 UTC) + 32000 ms (32 leap seconds).
+ */
+#define GN_EPOCH_UNIX_MS 1072915232000LLU
+
+/* time until location table entry becomes invalid */
+#define GN_LOC_TE_LIFETIME 20000
+/* time between two beacons */
+#define GN_BEACON_RETRANSMIT_TIME 3000
+/* time between retransmits of an unanswered location service request */
+#define GN_LS_RETRANSMIT_TIME 1000
+
+/* sizes in KiB */
+#define GN_UC_BUF_SIZE 256
+#define GN_BC_BUF_SIZE 1024
+#define GN_CBF_BUF_SIZE 256
+
+#define GN_UC_BUF 0
+#define GN_BC_BUF 1
+#define GN_CBF_BUF 2
+
+#define BH_NH_ANY 0
+#define BH_NH_COMMON_HEADER 1
+#define BH_NH_SECURED_PACKET 2
+
+#define GN_LT_BASE_50MS  (0 << 6)
+#define GN_LT_BASE_1S    (1 << 6)
+#define GN_LT_BASE_10S   (2 << 6)
+#define GN_LT_BASE_100S  (3 << 6)
+#define GN_ENCODE_LT(base, mult) ((base) | ((mult) & 0x3f))
+
+/* itsGnDefaultPacketLifetime: Default packet lifetime encoded as 60s (Base=1s, Mult=60) */
+#define BH_LT_DEFAULT GN_ENCODE_LT(GN_LT_BASE_1S, 60)
+
+#define CH_NH_ANY 0
+#define CH_NH_BTPA 1
+#define CH_NH_BTPB 2
+#define CH_NH_IPV6 3
+
+#define CH_FLAG_MOBILE (1 << 7)
+
+/* itsGnDefaultTrafficClass: Default traffic class */
+#define CH_TC_DEFAULT 0
+
+#define CH_HT_BEACON 1
+#define CH_HT_GUC 2
+#define CH_HT_GAC 3
+#define CH_HT_GBC 4
+#define CH_HT_TSB 5
+#define CH_HT_LS 6
+
+#define CH_HST_GABC_CIRCLE 0
+#define CH_HST_GABC_RECT 1
+#define CH_HST_GABC_ELIP 2
+
+#define CH_HST_TSB_SINGLE_HOP 0
+#define CH_HST_TSB_MULTI_HOP 1
+
+#define CH_HST_LS_REQUEST 0
+#define CH_HST_LS_REPLY 1
+
+#define CH_HST_UNSPECIFIED 0
+#define CH_HST_BEACON 1
+
+#define CH_PL_EMPTY 0L
+#define BEACON_MHL 1
+
+#define GN_BASE_HEADER_SIZE \
+	(sizeof(struct gn_basic_header) + sizeof(struct gn_common_header))
+#define GN_SET_BTP(skb, btp_h, extended_header_type)                         \
+	do {                                                                 \
+		skb_set_transport_header(                                    \
+			skb,                                                 \
+			GN_BASE_HEADER_SIZE + sizeof(extended_header_type)); \
+		btp_h = (struct btp_header *)skb_transport_header(skb);      \
+	} while (0)
+
+#define GN_GUC_HLEN sizeof(struct gn_guc_header)
+#define GN_GXC_HLEN sizeof(struct gn_gxc_header)
+#define GN_SHB_HLEN sizeof(struct gn_shb_header)
+#define GN_TSB_HLEN sizeof(struct gn_tsb_header)
+#define GN_BEACON_HLEN sizeof(struct gn_beacon_header)
+#define GN_LS_REQUEST_HLEN sizeof(struct gn_ls_request_header)
+#define GN_LS_REPLY_HLEN sizeof(struct gn_ls_reply_header)
+#define BTP_HLEN sizeof(struct btp_header)
+
+/**
+ *	struct gn_iface - GeoNetworking interface
+ *	@dev - Network device associated with this interface
+ *	@address - Our address
+ *	@local_sn - Current sequence number
+ */
+struct gn_iface {
+	struct net_device	*dev;
+	gn_address_t	address;
+	struct gn_position pos;
+	atomic_t local_sn;
+	struct hlist_node hnode;
+	struct rcu_head rcu;
+};
+
+struct gn_sock {
+	/* struct sock has to be the first member of gn_sock */
+	struct sock	sk;
+	struct gn_scope scope;
+	gn_address_t src_addr;
+	gn_address_t dst_addr;
+	u16 src_port;
+	u16 dst_port;
+	u8 protocol;
+};
+
+static inline struct gn_sock *gn_sk(struct sock *sk)
+{
+	return (struct gn_sock *)sk;
+}
+
+struct btp_header {
+	__be16 dst_port;
+	__be16 src_port;
+} __packed;
+
+enum ITS_TYPE {
+	UNKNOWN,
+	PEDESTRIAN,
+	CYCLIST,
+	MOPED,
+	MOTORCYCLE,
+	PASSENGER_CAR,
+	BUS,
+	LIGHT_TRUCK,
+	HEAVY_TRUCK,
+	TAILER,
+	SPECIAL_VEHICLE,
+	TRAM,
+	ROAD_SIDE_UNIT
+};
+
+#define GN_ADDRESS_M BIT(63)
+#define GN_ADDRESS_ST GENMASK(62, 58)
+#define GN_ADDRESS_RES GENMASK(57, 48)
+#define GN_ADDRESS_MID GENMASK(47, 0)
+
+typedef __be16 gn_spai_t;
+
+#define GN_LPV_PAI BIT(15)
+#define GN_LPV_S GENMASK(14, 0)
+
+struct gn_lpv {
+	gn_address_t addr;
+	__be32 tst;
+	__be32 lat;
+	__be32 lon;
+	gn_spai_t spai;
+	__be16 h; //signed
+} __packed;
+
+struct gn_spv {
+	gn_address_t addr;
+	__be32 tst;
+	__be32 lat; //short
+	__be32 lon;
+} __packed;
+
+/*version: Identifies the version of the GeoNetworking protocol
+ *
+ *nh: Identifies the type of header immediately following the GeoNetworking
+ *    Basic Header
+ *reserved: Reserved. Set to 0
+ *
+ *lt: Lifetime field.
+ *Indicates the maximum tolerable time a packet may be buffered until it
+ *reaches its destination Bit 0 to Bit 5:
+ *lt_sub:
+ *0) 50 ms
+ *1) 1s
+ *2) 10 s
+ *3) 100 s
+ *4) 600 s
+ *5) 1000s
+ *
+ *Bit 6 to Bit 11:
+ *lt_mul: Multiplier for lt_sub, between 0 and 63
+ *
+ *rhl: Remaining hop limit. Set to the maximum hop limit (mhl) initially and
+ *decremented by 1 at each hop.
+ *The packet is dropped when rhl reaches 0
+ */
+struct gn_basic_header {
+#if defined(__LITTLE_ENDIAN_BITFIELD)
+	__u8 nh : 4;
+	__u8 version : 4;
+#elif defined(__BIG_ENDIAN_BITFIELD)
+	__u8 version : 4;
+	__u8 nh : 4;
+#else
+#error "please fix asm/byteorder.h"
+#endif
+	__u8 reserved;
+	__u8 lt;
+	__u8 rhl;
+} __packed;
+
+struct gn_common_header {
+#if defined(__LITTLE_ENDIAN_BITFIELD)
+	__u8 reserved : 4;
+	__u8 nh : 4;
+	__u8 hst : 4;
+	__u8 ht : 4;
+#elif defined(__BIG_ENDIAN_BITFIELD)
+	__u8 nh : 4;
+	__u8 reserved : 4;
+	__u8 ht : 4;
+	__u8 hst : 4;
+#else
+#error "please fix asm/byteorder.h"
+#endif
+
+	__u8 tc;
+	__u8 flags;
+	__be16 pl;
+	__u8 mhl;
+	__u8 reserved2;
+} __packed;
+
+/*there are 6 types of headers:
+ * 1) GUC packet header (clause 9.8.2).
+ * 2) TSB packet header (clause 9.8.3).
+ * 3) SHB packet header (clause 9.8.4).
+ * 4) GBC and GAC packet headers (clause 9.8.5).
+ * 5) BEACON packet header (clause 9.8.6).
+ * 6) LS Request and LS Reply packet headers (clause 9.8.7 and clause 9.8.8).
+ */
+
+struct gn_guc_header {
+	__be16 sn;
+	__be16 reserved;
+	struct gn_lpv sopv;
+	struct gn_spv depv;
+} __packed;
+
+struct gn_tsb_header {
+	__be16 sn;
+	__be16 reserved;
+	struct gn_lpv sopv;
+} __packed;
+
+struct gn_shb_header {
+	struct gn_lpv sopv;
+	__be32 mdd;
+} __packed;
+
+/* GAC and GBC share the same header structure */
+struct gn_gxc_header {
+	__be16 sn;
+	__be16 reserved;
+	struct gn_lpv sopv;
+	__be32 gap_lat; //signed
+	__be32 gap_lon; //signed
+	__be16 da;
+	__be16 db;
+	__be16 angle;
+	__be16 reserved2;
+} __packed;
+
+#define gn_gac_header gn_gxc_header
+#define gn_gbc_header gn_gxc_header
+
+struct gn_beacon_header {
+	struct gn_lpv sopv;
+} __packed;
+
+struct gn_ls_request_header {
+	__be16 sn;
+	__be16 reserved;
+	struct gn_lpv sopv;
+	gn_address_t addr;
+} __packed;
+
+//same structure as gn_guc_header, left in for abstraction
+struct gn_ls_reply_header {
+	__be16 sn;
+	__be16 reserved;
+	struct gn_lpv sopv;
+	struct gn_spv depv;
+} __packed;
+
+struct gn_header {
+	struct gn_basic_header gb_h;
+	struct gn_common_header gc_h;
+	union {
+		struct gn_guc_header guc_h;
+		union {
+			struct gn_gxc_header gac_h;
+			struct gn_gxc_header gbc_h;
+		};
+		struct gn_tsb_header tsb_h;
+		struct gn_shb_header shb_h;
+		struct gn_beacon_header beacon_h;
+		struct gn_ls_request_header ls_request_h;
+		struct gn_ls_reply_header ls_reply_h;
+		__be16 sn;
+	};
+} __packed;
+
+/* Inter module exports */
+
+u32 gn_tai_to_gn(ktime_t tai_time);
+u32 gn_timestamp_now(void);
+
+void gn_fill_sopv(struct gn_iface *gnif, struct gn_lpv *sopv,
+		  gn_address_t addr);
+
+extern struct hlist_head gn_sockets;
+extern rwlock_t gn_sockets_lock;
+
+extern struct hlist_head gn_interfaces;
+extern spinlock_t gn_interfaces_lock;
+
+struct gn_iface *gn_if_add_device(struct net_device *dev,
+				  struct sockaddr_gn *sa);
+void gn_if_drop_device(struct net_device *dev);
+int gn_validate_pos(struct gn_position *pos);
+
+int gn_netlink_init(void);
+void gn_netlink_exit(void);
+
+#ifdef CONFIG_SYSCTL
+extern int gn_register_sysctl(void);
+extern void gn_unregister_sysctl(void);
+#else
+static inline int gn_register_sysctl(void)
+{
+	return 0;
+}
+static inline void gn_unregister_sysctl(void)
+{
+}
+#endif
+
+#endif /* __LINUX_GN_H__ */
diff --git a/include/linux/gn_routing.h b/include/linux/gn_routing.h
new file mode 100644
index 000000000000..64ccc80f5a90
--- /dev/null
+++ b/include/linux/gn_routing.h
@@ -0,0 +1,68 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifndef __LINUX_GN_ROUTING_H__
+#define __LINUX_GN_ROUTING_H__
+
+#include <linux/gn.h>
+#include <linux/types.h>
+#include <linux/if_ether.h>
+
+#define GN_MAX_PDR_EMA_BETA 90
+#define GN_MAX_PDR 100
+#define GN_DPL_SIZE 8
+#define GN_LSB_SIZE 16	// this is the number of entries per entry (has to be a power of 2 atm)
+			// ETSI sets a maximum of 1024 octets/bytes, which is not trivial to maintain
+
+#define GN_NON_AREA_FORWARDING_UNSPECIFIED	0
+#define GN_NON_AREA_FORWARDING_GREEDY		1
+#define GN_NON_AREA_FORWARDING_CBF		2
+#define GN_NON_AREA_FORWARDING GN_NON_AREA_FORWARDING_GREEDY
+
+#define GN_AREA_FORWARDING_UNSPECIFIED	0
+#define GN_AREA_FORWARDING_SIMPLE	1
+#define GN_AREA_FORWARDING_CBF		2
+#define GN_AREA_FORWARDING_ADVANCED	3
+#define GN_AREA_FORWARDING GN_AREA_FORWARDING_SIMPLE
+
+#define GN_QUEUE_DIRECT		0
+#define GN_QUEUE_LS_PENDING	1
+#define GN_QUEUE_LS_STALE	2
+#define GN_QUEUE_ERROR		3
+
+#define GN_FORWARD_BROADCAST	0
+#define GN_FORWARD_NEXT_HOP	1
+#define GN_FORWARD_BUFFER	2
+#define GN_FORWARD_DISCARD	3
+
+struct gn_dpd_buf {
+	u16 sn[GN_DPL_SIZE];
+	u8 head;
+};
+
+struct loc_te {
+	gn_address_t addr;
+	u8 ll_address[ETH_ALEN];
+	struct gn_lpv pv;
+	struct gn_dpd_buf dpl;
+	struct sk_buff_head lsb;
+	u32 tst_addr;
+	u32 pdr;
+	struct hlist_node hnode;
+	u8 ls_pending: 1;
+	u8 is_neighbour: 1;
+};
+
+s64 gn_F(struct gn_coord self, struct gn_geo_scope scope);
+int gn_gxc_forward(struct gn_iface *gnif, s64 f, u8 *addr, struct gn_lpv *depv);
+
+struct gn_iface *gn_find_interface(gn_address_t addr);
+struct gn_iface *gn_find_interface_by_dev(struct net_device *dev);
+int gn_query_ll_address(gn_address_t addr, u8 *ll_address);
+int gn_query_ll_nexthop(struct gn_iface *gnif, gn_address_t query_addr, u8 *ll_address);
+int gn_fill_depv(struct gn_spv *depv, gn_address_t dest_addr);
+int gn_ls_queue(gn_address_t dest_addr, struct sk_buff *skb);
+void gn_ls_flush(gn_address_t dest_addr);
+int gn_update_location_table(struct gn_lpv *pv, bool make_neighbour, const u8 *ll_address, const __be16 *sn);
+void gn_routing_exit(void);
+
+#endif // __LINUX_GN_ROUTING_H__
diff --git a/include/linux/socket.h b/include/linux/socket.h
index 2a8d7b14f1d1..17d8c466f120 100644
--- a/include/linux/socket.h
+++ b/include/linux/socket.h
@@ -254,8 +254,8 @@ struct ucred {
 #define AF_MCTP		45	/* Management component
 				 * transport protocol
 				 */
-
-#define AF_MAX		46	/* For now.. */
+#define AF_GN		46	/* ETSI ITS GeoNetworking */
+#define AF_MAX		47	/* For now.. */
 
 /* Protocol families, same as address families. */
 #define PF_UNSPEC	AF_UNSPEC
@@ -306,6 +306,7 @@ struct ucred {
 #define PF_SMC		AF_SMC
 #define PF_XDP		AF_XDP
 #define PF_MCTP		AF_MCTP
+#define PF_GN		AF_GN
 #define PF_MAX		AF_MAX
 
 /* Maximum queue length specifiable by listen.  */
@@ -400,6 +401,7 @@ struct ucred {
 #define SOL_MCTP	285
 #define SOL_SMC		286
 #define SOL_VSOCK	287
+#define SOL_GN		288
 
 /* IPX options */
 #define IPX_TYPE	1
diff --git a/include/uapi/linux/gn.h b/include/uapi/linux/gn.h
new file mode 100644
index 000000000000..96cafbda54de
--- /dev/null
+++ b/include/uapi/linux/gn.h
@@ -0,0 +1,84 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+
+#ifndef _UAPI__LINUX_GN_H__
+#define _UAPI__LINUX_GN_H__
+
+#include <linux/types.h>
+#include <asm/byteorder.h>
+#include <linux/socket.h>
+
+#include <linux/time_types.h>
+
+/*
+ * GeoNetworking structures
+ *
+ */
+#define GNPORT_ANY	0
+#define GNPORT_FIRST	1
+#define GNPORT_RESERVED	3000
+#define GNPORT_LAST	65535
+
+#define GNADDR_BROADCAST 0x0000FFFFFFFFFFFFLLU
+
+/* Valid protocols */
+#define GN_PROTO_ANY	0
+#define GN_PROTO_BTP_A	1
+#define GN_PROTO_BTP_B	2
+#define GN_PROTO_INET6	3
+#define GN_PROTO_MAX	GN_PROTO_INET6
+
+/* protocol-specific ioctls */
+#define SIOCGNSPOSITION			(SIOCPROTOPRIVATE + 0)
+
+/* gn_position flags */
+#define GN_POSITION_MOBILE		(1 << 0)
+
+/* {get,set}sockopt options */
+#define GN_SCOPE 1
+
+#define GN_SCOPE_UNSPECIFIED		0
+#define GN_SCOPE_TOPOLOGICAL		1
+#define GN_SCOPE_GEOGRAPHICAL		2
+#define GN_SCOPE_GEOGRAPHICAL_ANYCAST	3
+#define GN_SCOPE_MAX			GN_SCOPE_GEOGRAPHICAL_ANYCAST
+
+#define GN_SHAPE_UNSPECIFIED	0
+#define GN_SHAPE_CIRCLE		1
+#define GN_SHAPE_ELLIPSE	2
+#define GN_SHAPE_RECTANGLE	3
+
+typedef __be64 gn_address_t;
+
+struct gn_coord {
+	__s32 lat;
+	__s32 lon;
+};
+
+struct gn_geo_scope {
+	struct gn_coord coord;
+	__u16 angle;
+	__u16 a, b;
+	__u8 shape;
+};
+
+struct gn_scope {
+	__u8 scope_type;
+	union {
+		__u8 topo_hops;
+		struct gn_geo_scope geo_scope;
+	};
+};
+
+struct gn_position {
+	struct __kernel_timespec tst;
+	struct gn_coord coord;
+	__u8 flags;
+};
+
+struct sockaddr_gn {
+	__kernel_sa_family_t sgn_family;
+	gn_address_t sgn_addr;
+	__u16 sgn_port;
+} __packed;
+
+#endif /* _UAPI__LINUX_GN_H__ */
diff --git a/include/uapi/linux/if_ether.h b/include/uapi/linux/if_ether.h
index 1ffac52c39df..e7b0baa0d15b 100644
--- a/include/uapi/linux/if_ether.h
+++ b/include/uapi/linux/if_ether.h
@@ -113,6 +113,7 @@
 #define ETH_P_FIP	0x8914		/* FCoE Initialization Protocol */
 #define ETH_P_80221	0x8917		/* IEEE 802.21 Media Independent Handover Protocol */
 #define ETH_P_HSR	0x892F		/* IEC 62439-3 HSRv1	*/
+#define ETH_P_GN	0x8947		/* ETSI-ITS Network Header */
 #define ETH_P_NSH	0x894F		/* Network Service Header */
 #define ETH_P_LOOPBACK	0x9000		/* Ethernet loopback packet, per IEEE 802.3 */
 #define ETH_P_QINQ1	0x9100		/* deprecated QinQ VLAN [ NOT AN OFFICIALLY REGISTERED ID ] */
diff --git a/include/uapi/linux/if_link.h b/include/uapi/linux/if_link.h
index 43cecca49f01..6fe5276b1998 100644
--- a/include/uapi/linux/if_link.h
+++ b/include/uapi/linux/if_link.h
@@ -2074,4 +2074,14 @@ enum {
 
 #define IFLA_OVPN_MAX	(__IFLA_OVPN_MAX - 1)
 
+/* GN section */
+
+enum {
+	IFLA_GN_UNSPEC,
+	IFLA_GN_POSITION,
+	__IFLA_GN_MAX,
+};
+
+#define IFLA_GN_MAX (__IFLA_GN_MAX - 1)
+
 #endif /* _UAPI_LINUX_IF_LINK_H */
diff --git a/net/Kconfig b/net/Kconfig
index e38477393551..b49b1e04b0da 100644
--- a/net/Kconfig
+++ b/net/Kconfig
@@ -26,6 +26,8 @@ menuconfig NET
 
 if NET
 
+source "net/gn/Kconfig"
+
 config WANT_COMPAT_NETLINK_MESSAGES
 	bool
 	help
diff --git a/net/Makefile b/net/Makefile
index 5b2dd7f07a85..bd3be868b2d8 100644
--- a/net/Makefile
+++ b/net/Makefile
@@ -75,3 +75,4 @@ obj-$(CONFIG_MPTCP)		+= mptcp/
 obj-$(CONFIG_MCTP)		+= mctp/
 obj-$(CONFIG_NET_HANDSHAKE)	+= handshake/
 obj-$(CONFIG_NET_SHAPER)	+= shaper/
+obj-$(CONFIG_GN)		+= gn/
diff --git a/net/gn/Kconfig b/net/gn/Kconfig
new file mode 100644
index 000000000000..4857e43f02c5
--- /dev/null
+++ b/net/gn/Kconfig
@@ -0,0 +1,4 @@
+config GN
+    tristate "Enable GeoNetworking support"
+    default y
+    select LLC
diff --git a/net/gn/Makefile b/net/gn/Makefile
new file mode 100644
index 000000000000..d8452508a162
--- /dev/null
+++ b/net/gn/Makefile
@@ -0,0 +1,7 @@
+#
+# Makefile for the Linux GeoNetworking layer.
+#
+
+obj-$(CONFIG_GN) += gn.o
+gn-y			:= gn_prot.o gn_routing.o gn_netlink.o
+gn-$(CONFIG_SYSCTL)	+= sysctl_net_gn.o
diff --git a/net/gn/gn_netlink.c b/net/gn/gn_netlink.c
new file mode 100644
index 000000000000..96cb89ea5c42
--- /dev/null
+++ b/net/gn/gn_netlink.c
@@ -0,0 +1,266 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * GeoNetworking Netlink interface
+ */
+
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/netlink.h>
+#include <linux/rtnetlink.h>
+#include <linux/if_link.h>
+#include <linux/if_addr.h>
+#include <linux/if_arp.h>
+#include <linux/slab.h>
+#include <linux/rculist.h>
+#include <linux/gn.h>
+#include <linux/gn_routing.h>
+#include <net/sock.h>
+
+static const struct nla_policy ifa_gn_policy[IFA_MAX + 1] = {
+	[IFA_ADDRESS]		= { .type = NLA_U64 },
+	[IFA_LOCAL]		= { .type = NLA_U64 },
+};
+
+static int gn_rtm_newaddr(struct sk_buff *skb, struct nlmsghdr *nlh,
+			  struct netlink_ext_ack *extack)
+{
+	struct net *net = sock_net(skb->sk);
+	struct nlattr *tb[IFA_MAX + 1];
+	struct net_device *dev;
+	struct sockaddr_gn sa;
+	struct gn_iface *gnif;
+	struct ifaddrmsg *ifm;
+	gn_address_t *addr;
+	int rc;
+
+	rc = nlmsg_parse(nlh, sizeof(*ifm), tb, IFA_MAX, ifa_gn_policy, extack);
+	if (rc < 0)
+		return rc;
+
+	ifm = nlmsg_data(nlh);
+	if (ifm->ifa_family != AF_GN)
+		return -EPFNOSUPPORT;
+
+	if (tb[IFA_LOCAL])
+		addr = nla_data(tb[IFA_LOCAL]);
+	else if (tb[IFA_ADDRESS])
+		addr = nla_data(tb[IFA_ADDRESS]);
+	else
+		return -EINVAL;
+
+	dev = __dev_get_by_index(net, ifm->ifa_index);
+	if (!dev)
+		return -ENODEV;
+
+	if (dev->type != ARPHRD_ETHER)
+		return -EINVAL;
+
+	if (gn_find_interface_by_dev(dev))
+		return -EEXIST;
+
+	memset(&sa, 0, sizeof(sa));
+	sa.sgn_family = AF_GN;
+	sa.sgn_addr = *addr;
+	sa.sgn_port = GNPORT_ANY;
+
+	gnif = gn_if_add_device(dev, &sa);
+	if (!gnif)
+		return -ENOMEM;
+
+	return 0;
+}
+
+static int gn_rtm_deladdr(struct sk_buff *skb, struct nlmsghdr *nlh,
+			  struct netlink_ext_ack *extack)
+{
+	struct net *net = sock_net(skb->sk);
+	struct nlattr *tb[IFA_MAX + 1];
+	struct net_device *dev;
+	struct ifaddrmsg *ifm;
+	int rc;
+
+	rc = nlmsg_parse(nlh, sizeof(*ifm), tb, IFA_MAX, ifa_gn_policy, extack);
+	if (rc < 0)
+		return rc;
+
+	ifm = nlmsg_data(nlh);
+	if (ifm->ifa_family != AF_GN)
+		return -EPFNOSUPPORT;
+
+	dev = __dev_get_by_index(net, ifm->ifa_index);
+	if (!dev)
+		return -ENODEV;
+
+	if (!gn_find_interface_by_dev(dev))
+		return -EADDRNOTAVAIL;
+
+	gn_if_drop_device(dev);
+	return 0;
+}
+
+static int gn_fill_addrinfo(struct sk_buff *skb, struct gn_iface *gnif,
+			    int msg_type, u32 portid, u32 seq, int flag)
+{
+	struct ifaddrmsg *hdr;
+	struct nlmsghdr *nlh;
+
+	nlh = nlmsg_put(skb, portid, seq, msg_type, sizeof(*hdr), flag);
+	if (!nlh)
+		return -EMSGSIZE;
+
+	hdr = nlmsg_data(nlh);
+	memset(hdr, 0, sizeof(*hdr));
+	hdr->ifa_family = AF_GN;
+	hdr->ifa_prefixlen = 0;
+	hdr->ifa_flags = 0;
+	hdr->ifa_scope = 0;
+	hdr->ifa_index = gnif->dev->ifindex;
+
+	if (nla_put_u64_64bit(skb, IFA_LOCAL, gnif->address, IFA_UNSPEC) ||
+	    nla_put_u64_64bit(skb, IFA_ADDRESS, gnif->address, IFA_UNSPEC)) {
+		nlmsg_cancel(skb, nlh);
+		return -EMSGSIZE;
+	}
+
+	nlmsg_end(skb, nlh);
+	return 0;
+}
+
+static int gn_dump_addrinfo(struct sk_buff *skb, struct netlink_callback *cb)
+{
+	struct net *net = sock_net(skb->sk);
+	struct ifaddrmsg *hdr;
+	struct gn_iface *gnif;
+	int ifindex = 0;
+	int idx = 0;
+	int s_idx = cb->args[0];
+	int rc = 0;
+
+	hdr = nlmsg_payload(cb->nlh, sizeof(*hdr));
+	if (hdr)
+		ifindex = hdr->ifa_index;
+
+	rcu_read_lock();
+	hlist_for_each_entry_rcu(gnif, &gn_interfaces, hnode) {
+		if (!net_eq(dev_net(gnif->dev), net))
+			continue;
+		if (ifindex && ifindex != gnif->dev->ifindex)
+			continue;
+		if (idx < s_idx) {
+			idx++;
+			continue;
+		}
+		rc = gn_fill_addrinfo(skb, gnif, RTM_NEWADDR,
+				      NETLINK_CB(cb->skb).portid,
+				      cb->nlh->nlmsg_seq, NLM_F_MULTI);
+		if (rc < 0)
+			break;
+		idx++;
+	}
+	rcu_read_unlock();
+
+	cb->args[0] = idx;
+	return skb->len;
+}
+
+static const struct nla_policy ifla_gn_policy[IFLA_GN_MAX + 1] = {
+	[IFLA_GN_POSITION]	= NLA_POLICY_EXACT_LEN(sizeof(struct gn_position)),
+};
+
+static int gn_fill_link_af(struct sk_buff *skb, const struct net_device *dev,
+			   u32 ext_filter_mask)
+{
+	struct gn_iface *gnif;
+
+	gnif = gn_find_interface_by_dev((struct net_device *)dev);
+	if (!gnif)
+		return -ENODATA;
+
+	if (nla_put(skb, IFLA_GN_POSITION, sizeof(gnif->pos), &gnif->pos))
+		return -EMSGSIZE;
+
+	return 0;
+}
+
+static size_t gn_get_link_af_size(const struct net_device *dev,
+				  u32 ext_filter_mask)
+{
+	struct gn_iface *gnif;
+
+	gnif = gn_find_interface_by_dev((struct net_device *)dev);
+	if (!gnif)
+		return 0;
+
+	return nla_total_size(sizeof(struct gn_position));
+}
+
+static int gn_set_link_af(struct net_device *dev, const struct nlattr *attr,
+			  struct netlink_ext_ack *extack)
+{
+	struct nlattr *tb[IFLA_GN_MAX + 1];
+	struct gn_position pos;
+	struct gn_iface *gnif;
+	int rc;
+
+	rc = nla_parse_nested(tb, IFLA_GN_MAX, attr, ifla_gn_policy, extack);
+	if (rc < 0)
+		return rc;
+
+	if (!tb[IFLA_GN_POSITION])
+		return 0;
+
+	if (!capable(CAP_NET_ADMIN))
+		return -EPERM;
+
+	if (nla_len(tb[IFLA_GN_POSITION]) < sizeof(struct gn_position))
+		return -EINVAL;
+
+	memcpy(&pos, nla_data(tb[IFLA_GN_POSITION]), sizeof(pos));
+	rc = gn_validate_pos(&pos);
+	if (rc < 0)
+		return rc;
+
+	gnif = gn_find_interface_by_dev(dev);
+	if (!gnif)
+		return -EADDRNOTAVAIL;
+
+	memcpy(&gnif->pos, &pos, sizeof(pos));
+	return 0;
+}
+
+static struct rtnl_af_ops gn_af_ops __read_mostly = {
+	.family			= PF_GN,
+	.fill_link_af		= gn_fill_link_af,
+	.get_link_af_size	= gn_get_link_af_size,
+	.set_link_af		= gn_set_link_af,
+};
+
+static const struct rtnl_msg_handler gn_rtnl_msg_handlers[] = {
+	{ .owner = THIS_MODULE, .protocol = PF_GN, .msgtype = RTM_NEWADDR,
+	  .doit = gn_rtm_newaddr },
+	{ .owner = THIS_MODULE, .protocol = PF_GN, .msgtype = RTM_DELADDR,
+	  .doit = gn_rtm_deladdr },
+	{ .owner = THIS_MODULE, .protocol = PF_GN, .msgtype = RTM_GETADDR,
+	  .dumpit = gn_dump_addrinfo },
+};
+
+int __init gn_netlink_init(void)
+{
+	int err;
+
+	err = rtnl_af_register(&gn_af_ops);
+	if (err)
+		return err;
+
+	err = rtnl_register_many(gn_rtnl_msg_handlers);
+	if (err)
+		rtnl_af_unregister(&gn_af_ops);
+
+	return err;
+}
+
+void gn_netlink_exit(void)
+{
+	rtnl_unregister_many(gn_rtnl_msg_handlers);
+	rtnl_af_unregister(&gn_af_ops);
+}
diff --git a/net/gn/gn_prot.c b/net/gn/gn_prot.c
new file mode 100644
index 000000000000..58095c4dc475
--- /dev/null
+++ b/net/gn/gn_prot.c
@@ -0,0 +1,1908 @@
+// SPDX-License-Identifier: GPL-2.0
+
+/*
+ * GeoNetworking
+ */
+
+#include <asm-generic/errno-base.h>
+#define pr_fmt(fmt) KBUILD_MODNAME ": %s: " fmt, __func__
+#include <linux/if_arp.h>
+#include <linux/slab.h>
+#include <linux/termios.h>
+#include <net/sock.h>
+#include <net/datalink.h>
+#include <net/psnap.h>
+#include <linux/gn.h>
+#include <linux/gn_routing.h>
+#include <linux/delay.h>
+#include <linux/init.h>
+#include <linux/export.h>
+#include <linux/etherdevice.h>
+#include <linux/timer.h>
+#include <linux/time.h>
+#include <linux/random.h>
+#include <linux/atomic.h>
+#include <linux/limits.h>
+#include <linux/if_ether.h>
+#include <linux/bug.h>
+
+struct datalink_proto *gn_dl;
+static const struct proto_ops gn_dgram_ops;
+static struct timer_list gn_beacon_timer;
+
+/* Handlers for the socket list. */
+
+DEFINE_RWLOCK(gn_sockets_lock);
+HLIST_HEAD(gn_sockets);
+
+static void __gn_insert_socket(struct sock *sk)
+{
+	sk_add_node(sk, &gn_sockets);
+}
+
+static void gn_remove_socket(struct sock *sk)
+{
+	write_lock_bh(&gn_sockets_lock);
+	sk_del_node_init(sk);
+	write_unlock_bh(&gn_sockets_lock);
+}
+
+#define from_timer(var, callback_timer, timer_fieldname) \
+	container_of(callback_timer, typeof(*(var)), timer_fieldname)
+
+static void gn_destroy_timer(struct timer_list *t)
+{
+	struct sock *sk = from_timer(sk, t, sk_timer);
+
+	if (sk_has_allocations(sk)) {
+		sk->sk_timer.expires = jiffies + SOCK_DESTROY_TIME;
+		add_timer(&sk->sk_timer);
+	} else {
+		sock_put(sk);
+	}
+}
+
+static void gn_destroy_socket(struct sock *sk)
+{
+	gn_remove_socket(sk);
+	skb_queue_purge(&sk->sk_receive_queue);
+
+	if (sk_has_allocations(sk)) {
+		timer_setup(&sk->sk_timer, gn_destroy_timer, 0);
+		sk->sk_timer.expires = jiffies + SOCK_DESTROY_TIME;
+		add_timer(&sk->sk_timer);
+	} else {
+		sock_put(sk);
+	}
+}
+
+/* Handling for system calls applied via the various interfaces to a
+ * GeoNetworking socket object.
+ */
+
+static struct proto gn_proto = {
+	.name = "GN",
+	.owner = THIS_MODULE,
+	.obj_size = sizeof(struct gn_sock),
+};
+
+HLIST_HEAD(gn_interfaces);
+DEFINE_SPINLOCK(gn_interfaces_lock);
+
+static void gn_activate_beacon(void);
+
+/**
+ * gn_iface - add device to the interface the socketaddress is bound to
+ */
+struct gn_iface *gn_if_add_device(struct net_device *dev,
+				  struct sockaddr_gn *sa)
+{
+	struct gn_iface *new_gnif = kzalloc_obj(*new_gnif, GFP_KERNEL);
+	struct gn_iface *gnif;
+	bool was_empty;
+
+	if (!new_gnif)
+		return NULL;
+
+	new_gnif->address = sa->sgn_addr;
+	new_gnif->dev = dev;
+
+	pr_info("Add interface %s with address %llx", dev->name,
+		new_gnif->address);
+
+	spin_lock_bh(&gn_interfaces_lock);
+	hlist_for_each_entry_rcu(gnif, &gn_interfaces, hnode) {
+		if (gnif->dev == dev) {
+			// Replace existing interface address
+			atomic_set(&new_gnif->local_sn,
+				   atomic_read(&gnif->local_sn));
+			memcpy(&new_gnif->pos, &gnif->pos, sizeof(gnif->pos));
+			hlist_replace_rcu(&gnif->hnode, &new_gnif->hnode);
+			spin_unlock_bh(&gn_interfaces_lock);
+			kfree_rcu(gnif, rcu);
+			return new_gnif;
+		}
+	}
+	was_empty = hlist_empty(&gn_interfaces);
+
+	// No existing interface address
+	hlist_add_head_rcu(&new_gnif->hnode, &gn_interfaces);
+	spin_unlock_bh(&gn_interfaces_lock);
+
+	if (was_empty)
+		gn_activate_beacon();
+
+	return new_gnif;
+}
+
+void gn_if_drop_device(struct net_device *dev)
+{
+	struct hlist_node *tmp;
+	struct gn_iface *gnif;
+
+	spin_lock_bh(&gn_interfaces_lock);
+	hlist_for_each_entry_safe(gnif, tmp, &gn_interfaces, hnode) {
+		if (gnif->dev == dev) {
+			hlist_del_rcu(&gnif->hnode);
+			kfree_rcu(gnif, rcu);
+			break;
+		}
+	}
+	spin_unlock_bh(&gn_interfaces_lock);
+}
+
+static void gn_interfaces_clear(void)
+{
+	struct hlist_node *tmp;
+	struct gn_iface *gnif;
+
+	spin_lock_bh(&gn_interfaces_lock);
+	hlist_for_each_entry_safe(gnif, tmp, &gn_interfaces, hnode) {
+		hlist_del_rcu(&gnif->hnode);
+		kfree_rcu(gnif, rcu);
+	}
+	spin_unlock_bh(&gn_interfaces_lock);
+}
+
+/*
+ * find the interface to which the socketaddress is bound
+ */
+struct gn_iface *gn_find_interface(gn_address_t addr)
+{
+	struct gn_iface *gnif;
+	bool found = false;
+
+	rcu_read_lock();
+	hlist_for_each_entry_rcu(gnif, &gn_interfaces, hnode) {
+		if (gnif->address == addr) {
+			found = true;
+			break;
+		}
+	}
+	rcu_read_unlock();
+	return found ? gnif : NULL;
+}
+
+/*
+ * find an interface by the device it belongs to
+ */
+struct gn_iface *gn_find_interface_by_dev(struct net_device *dev)
+{
+	struct gn_iface *gnif;
+	bool found = false;
+
+	rcu_read_lock();
+	hlist_for_each_entry_rcu(gnif, &gn_interfaces, hnode) {
+		if (gnif->dev == dev) {
+			found = true;
+			break;
+		}
+	}
+	rcu_read_unlock();
+	return found ? gnif : NULL;
+}
+
+/*
+ * A device event has occurred. Watch for devices going down and
+ * delete our use of them (iface and route).
+ */
+static int gn_device_event(struct notifier_block *this, unsigned long event,
+			   void *ptr)
+{
+	struct net_device *dev = netdev_notifier_info_to_dev(ptr);
+
+	if (!net_eq(dev_net(dev), &init_net))
+		return NOTIFY_DONE;
+
+	if (dev->type != ARPHRD_ETHER)
+		return NOTIFY_DONE;
+
+	if (event == NETDEV_DOWN || event == NETDEV_UNREGISTER)
+		gn_if_drop_device(dev);
+
+	return NOTIFY_DONE;
+}
+
+/*
+ * Convert TAI timestamp in milliseconds to GN timestamp
+ */
+u32 gn_tai_to_gn(ktime_t tai_time)
+{
+	const ktime_t tai_offset = ms_to_ktime(GN_EPOCH_UNIX_MS);
+
+	WARN_ONCE(ktime_before(tai_time, tai_offset),
+		  "timestamp %lld out of bounds", tai_time);
+	/* truncate timestamp (GN timestamp has 32 bits) */
+	return (u32)ktime_sub(tai_time, tai_offset);
+}
+
+u32 gn_timestamp_now(void)
+{
+	return gn_tai_to_gn(ktime_get_clocktai());
+}
+
+/*
+ * Create a socket. Initialise the socket, blank the addresses
+ * set the state.
+ */
+static int gn_create(struct net *net, struct socket *sock, int protocol,
+		     int kern)
+{
+	struct sock *sk;
+	int rc;
+
+	rc = -EAFNOSUPPORT;
+	if (!net_eq(net, &init_net))
+		goto out;
+
+	rc = -ESOCKTNOSUPPORT;
+	if (sock->type != SOCK_DGRAM)
+		goto out;
+
+	if (protocol < GN_PROTO_ANY || protocol > GN_PROTO_MAX)
+		goto out;
+
+	/* Note: Only BTP/GeoNetworking protocols supported; IPv6 encap not enabled */
+	if (protocol == GN_PROTO_INET6)
+		goto out;
+
+	rc = -ENOMEM;
+	sk = sk_alloc(net, PF_GN, GFP_KERNEL, &gn_proto, kern);
+	if (!sk)
+		goto out;
+	rc = 0;
+	sock->ops = &gn_dgram_ops;
+	sock_init_data(sock, sk);
+	if (protocol == GN_PROTO_ANY)
+		gn_sk(sk)->protocol = GN_PROTO_BTP_A;
+	else
+		gn_sk(sk)->protocol = protocol;
+
+	/* Checksums on by default */
+//	sock_set_flag(sk, SOCK_ZAPPED);
+out:
+	return rc;
+}
+
+/* Free a socket. No work needed */
+static int gn_release(struct socket *sock)
+{
+	struct sock *sk = sock->sk;
+
+	if (sk) {
+		sock_hold(sk);
+		lock_sock(sk);
+
+		sock_orphan(sk);
+		sock->sk = NULL;
+		gn_destroy_socket(sk);
+
+		release_sock(sk);
+		sock_put(sk);
+	}
+	return 0;
+}
+
+static int gn_pick_and_bind_port(struct sock *sk, struct sockaddr_gn *sgn)
+{
+	int retval;
+
+	write_lock_bh(&gn_sockets_lock);
+
+	for (sgn->sgn_port = GNPORT_RESERVED; sgn->sgn_port < GNPORT_LAST;
+	     sgn->sgn_port++) {
+		struct sock *s;
+
+		sk_for_each(s, &gn_sockets) {
+			struct gn_sock *gn = gn_sk(s);
+
+			if (gn->src_port == sgn->sgn_port &&
+			    gn->src_addr == sgn->sgn_addr)
+				goto try_next_port;
+		}
+
+		/* Wheee, it's free, assign and insert. */
+		__gn_insert_socket(sk);
+		gn_sk(sk)->src_port = sgn->sgn_port;
+		retval = 0;
+		goto out;
+
+try_next_port:;
+	}
+
+	retval = -EBUSY;
+out:
+	write_unlock_bh(&gn_sockets_lock);
+	return retval;
+}
+
+static struct sock *gn_find_or_insert_socket(struct sock *sk,
+					     struct sockaddr_gn *sgn)
+{
+	struct gn_sock *gn;
+	struct sock *s;
+
+	write_lock_bh(&gn_sockets_lock);
+	sk_for_each(s, &gn_sockets) {
+		gn = gn_sk(s);
+
+		if (gn->src_port == sgn->sgn_port)
+			goto found;
+	}
+	s = NULL;
+	gn = gn_sk(sk);
+	gn->src_addr = sgn->sgn_addr;
+	gn->src_port = sgn->sgn_port;
+	pr_info("gn: Add socket addr=%llx port=%d", gn->src_addr, gn->src_port);
+	__gn_insert_socket(sk); /* Wheee, it's free, assign and insert. */
+found:
+	write_unlock_bh(&gn_sockets_lock);
+	return s;
+}
+
+static int gn_autobind(struct sock *sock)
+{
+	return -EOPNOTSUPP;
+}
+
+/* Set the address 'our end' of the connection */
+static int gn_bind(struct socket *sock, struct sockaddr_unsized *uaddr,
+		   int addr_len)
+{
+	DECLARE_SOCKADDR(struct sockaddr_gn *, addr, uaddr);
+	struct sock *sk = sock->sk;
+	struct gn_sock *gn;
+	int err;
+
+	gn = gn_sk(sk);
+
+	if (!sock_flag(sk, SOCK_ZAPPED) ||
+	    addr_len != sizeof(struct sockaddr_gn))
+		return -EINVAL;
+
+	if (addr->sgn_family != AF_GN)
+		return -EAFNOSUPPORT;
+
+	lock_sock(sk);
+	if (addr->sgn_addr != 0 && !gn_find_interface(addr->sgn_addr)) {
+		release_sock(sk);
+		return -EADDRNOTAVAIL;
+	}
+	gn->src_addr = addr->sgn_addr;
+
+	if (addr->sgn_port == GNPORT_ANY) {
+		err = gn_pick_and_bind_port(sk, addr);
+
+		if (err < 0)
+			goto out;
+	} else {
+		err = -EADDRINUSE;
+		if (gn_find_or_insert_socket(sk, addr))
+			goto out;
+	}
+	sock_reset_flag(sk, SOCK_ZAPPED);
+	err = 0;
+
+out:
+	release_sock(sk);
+	return err;
+}
+
+/* Set the address we talk to */
+static int gn_connect(struct socket *sock, struct sockaddr_unsized *uaddr,
+		      int addr_len, int flags)
+{
+	struct sock *sk = sock->sk;
+	struct sockaddr_gn *addr;
+	struct gn_sock *gn;
+	int err;
+
+	gn = gn_sk(sk);
+
+	sk->sk_state = TCP_CLOSE;
+	sock->state = SS_UNCONNECTED;
+
+	if (addr_len != sizeof(*addr))
+		return -EINVAL;
+
+	addr = (struct sockaddr_gn *)uaddr;
+
+	if (addr->sgn_family != AF_GN)
+		return -EAFNOSUPPORT;
+
+	lock_sock(sk);
+	err = -EBUSY;
+	if (sock_flag(sk, SOCK_ZAPPED))
+		if (gn_autobind(sk) < 0)
+			goto out;
+
+	/* Note: Route resolution for connected sockets occurs during gn_sendmsg */
+
+	gn->dst_port = addr->sgn_port;
+	gn->dst_addr = addr->sgn_addr;
+
+	sock->state = SS_CONNECTED;
+	sk->sk_state = TCP_ESTABLISHED;
+	err = 0;
+out:
+	release_sock(sk);
+	return err;
+}
+
+static struct sock *gn_search_socket(struct sockaddr_gn *tosgn,
+				     struct gn_iface *gnif)
+{
+	struct gn_sock *gn;
+	struct sock *s;
+
+	read_lock_bh(&gn_sockets_lock);
+	sk_for_each(s, &gn_sockets) {
+		gn = gn_sk(s);
+
+		if (gn->src_port != tosgn->sgn_port)
+			continue;
+		if (!gnif || gn->src_addr == gnif->address) {
+			sock_hold(s);
+			goto out;
+		}
+	}
+	s = NULL;
+out:
+	read_unlock_bh(&gn_sockets_lock);
+	return s;
+}
+
+static u16 gn_if_next_sn(struct gn_iface *gnif)
+{
+	return atomic_inc_return(&gnif->local_sn) % USHRT_MAX;
+}
+
+void gn_fill_sopv(struct gn_iface *gnif, struct gn_lpv *sopv, gn_address_t addr)
+{
+	/* Note: Speed and Heading zeroed until velocity sensors integrated */
+
+	ktime_t tst = ktime_set(gnif->pos.tst.tv_sec, gnif->pos.tst.tv_nsec);
+	// fill empty or invalid timestamp with current timestamp
+	// (must be >= 2004-01-01 ETSI Epoch)
+	if (tst == 0 || ktime_before(tst, ms_to_ktime(GN_EPOCH_UNIX_MS)))
+		sopv->tst = cpu_to_be32(gn_timestamp_now());
+	else
+		sopv->tst = cpu_to_be32(gn_tai_to_gn(tst));
+	sopv->lon = cpu_to_be32(gnif->pos.coord.lon);
+	sopv->lat = cpu_to_be32(gnif->pos.coord.lat);
+	sopv->addr = addr;
+	sopv->spai = cpu_to_be16(FIELD_PREP(GN_LPV_PAI, 0) |
+				 FIELD_PREP(GN_LPV_S, 0));
+	sopv->h = cpu_to_be16(0);
+}
+
+static void gn_fill_bh_ch(struct gn_iface *gnif, struct gn_header *gh,
+			  u8 packet_type, u8 packet_subtype, u8 rhl,
+			  u8 next_header, __be16 payload_size)
+{
+	const u8 mhl = DEFAULT_HOP_LIMIT;
+	/* rhl should never be greater than mhl */
+	if (WARN_ON(rhl > mhl))
+		rhl = DEFAULT_HOP_LIMIT;
+
+	gh->gb_h.version = GN_VERSION;
+	gh->gb_h.nh = BH_NH_COMMON_HEADER;
+	gh->gb_h.reserved = 0;
+	gh->gb_h.lt = BH_LT_DEFAULT;
+	gh->gb_h.rhl = rhl;
+
+	gh->gc_h.nh = next_header;
+	gh->gc_h.reserved = 0;
+	gh->gc_h.reserved2 = 0;
+	gh->gc_h.ht = packet_type;
+	gh->gc_h.hst = packet_subtype;
+	gh->gc_h.tc = CH_TC_DEFAULT;
+	gh->gc_h.pl = payload_size;
+	if (packet_type == CH_HT_BEACON)
+		gh->gc_h.mhl = BEACON_MHL;
+	else
+		gh->gc_h.mhl = DEFAULT_HOP_LIMIT;
+
+	if (gnif->pos.flags & GN_POSITION_MOBILE)
+		gh->gc_h.flags = CH_FLAG_MOBILE;
+	else
+		gh->gc_h.flags = 0;
+}
+
+static void gn_fill_bh_ch_nopayload(struct gn_iface *gnif,
+				    struct gn_header *gn_h, u8 packet_type,
+				    u8 packet_subtype, u8 rhl)
+{
+	gn_fill_bh_ch(gnif, gn_h, packet_type, packet_subtype, rhl, CH_NH_ANY,
+		      0);
+}
+
+static void gn_location_service_req(struct gn_iface *gnif, gn_address_t saddr,
+				    gn_address_t daddr)
+{
+	struct gn_ls_request_header *gls_h;
+	struct gn_common_header *gc_h;
+	struct gn_basic_header *gb_h;
+	struct sk_buff *skb;
+	int size = 0;
+
+	size += gn_dl->header_length;
+	size += gnif->dev->hard_header_len;
+	size += sizeof(struct gn_basic_header);
+	size += sizeof(struct gn_common_header);
+	size += sizeof(struct gn_ls_request_header);
+
+	skb = netdev_alloc_skb(gnif->dev, size);
+	if (!skb)
+		return;
+	skb_reserve(skb, gn_dl->header_length);
+	skb_reserve(skb, gnif->dev->hard_header_len);
+	skb_reserve(skb, sizeof(struct gn_basic_header));
+	skb_reserve(skb, sizeof(struct gn_common_header));
+	skb_reserve(skb, sizeof(struct gn_ls_request_header));
+
+	gls_h = skb_push(skb, sizeof(struct gn_ls_request_header));
+	gc_h = skb_push(skb, sizeof(struct gn_common_header));
+	gb_h = skb_push(skb, sizeof(struct gn_basic_header));
+
+	gn_fill_bh_ch_nopayload(gnif, (struct gn_header *)gb_h, CH_HT_LS,
+				CH_HST_LS_REQUEST, DEFAULT_HOP_LIMIT);
+
+	gls_h->sn = cpu_to_be16(gn_if_next_sn(gnif));
+	gls_h->reserved = 0;
+	gls_h->addr = daddr;
+	gn_fill_sopv(gnif, &gls_h->sopv, saddr);
+
+	gn_dl->request(gn_dl, skb, gnif->dev->broadcast);
+}
+
+static void gn_location_service_reply(struct gn_spv *depv,
+				      struct gn_iface *gnif, gn_address_t addr,
+				      u64 llc)
+{
+	struct gn_ls_reply_header *gls_h;
+	struct gn_common_header *gc_h;
+	struct gn_basic_header *gb_h;
+	struct sk_buff *skb;
+	int size;
+
+	size = 0;
+	size += gn_dl->header_length;
+	size += gnif->dev->hard_header_len;
+	size += sizeof(struct gn_basic_header);
+	size += sizeof(struct gn_common_header);
+	size += sizeof(struct gn_ls_reply_header);
+
+	skb = netdev_alloc_skb(gnif->dev, size);
+	if (!skb)
+		return;
+	skb_reserve(skb, gn_dl->header_length);
+	skb_reserve(skb, gnif->dev->hard_header_len);
+	skb_reserve(skb, sizeof(struct gn_basic_header));
+	skb_reserve(skb, sizeof(struct gn_common_header));
+	skb_reserve(skb, sizeof(struct gn_ls_reply_header));
+
+	gls_h = skb_push(skb, sizeof(struct gn_ls_reply_header));
+	gc_h = skb_push(skb, sizeof(struct gn_common_header));
+	gb_h = skb_push(skb, sizeof(struct gn_basic_header));
+
+	gn_fill_bh_ch_nopayload(gnif, (struct gn_header *)gb_h, CH_HT_LS,
+				CH_HST_LS_REPLY, DEFAULT_HOP_LIMIT);
+
+	gls_h->sn = cpu_to_be16(gn_if_next_sn(gnif));
+	gls_h->reserved = 0;
+	gn_fill_sopv(gnif, &gls_h->sopv, addr);
+	memcpy(&gls_h->depv, depv, sizeof(*depv));
+
+	gn_dl->request(gn_dl, skb, gnif->dev->broadcast);
+}
+
+static int gn_pass_payload_sock(struct sockaddr_gn *tosgn, struct sk_buff *skb)
+{
+	int rc = NET_RX_DROP;
+	struct sock *sock;
+
+	sock = gn_search_socket(tosgn, NULL);
+	if (!sock)
+		return NET_RX_DROP;
+	if (sock_queue_rcv_skb(sock, skb) == 0)
+		rc = NET_RX_SUCCESS;
+	sock_put(sock);
+	return rc;
+}
+
+static int gn_forward_guc_packet(struct sk_buff *skb, gn_address_t dest_addr)
+{
+	struct sk_buff *forward_skb;
+	u8 next_hop_mac[ETH_ALEN];
+	struct gn_header *fwd_gh;
+	struct gn_iface *gnif;
+
+	fwd_gh = (struct gn_header *)skb_network_header(skb);
+	if (fwd_gh->gb_h.rhl <= 0)
+		return -EINVAL;
+
+	gnif = gn_find_interface_by_dev(skb->dev);
+	if (!gnif)
+		return -ENODEV;
+
+	forward_skb = skb_copy(skb, GFP_ATOMIC);
+	if (!forward_skb) {
+		pr_warn("Dropping packet during GUC forwarding\n");
+		return -ENOMEM;
+	}
+
+	fwd_gh = (struct gn_header *)skb_network_header(forward_skb);
+	fwd_gh->gb_h.rhl--;
+
+	if (gn_query_ll_nexthop(gnif, dest_addr, next_hop_mac) == 0)
+		gn_dl->request(gn_dl, forward_skb, next_hop_mac);
+	else
+		gn_dl->request(gn_dl, forward_skb, skb->dev->broadcast);
+
+	return 0;
+}
+
+static int gn_forward_tsb_packet(struct sk_buff *skb)
+{
+	struct sk_buff *forward_skb;
+	struct gn_header *fwd_gh;
+
+	fwd_gh = (struct gn_header *)skb_network_header(skb);
+	if (fwd_gh->gb_h.rhl <= 0)
+		return -EINVAL;
+
+	forward_skb = skb_copy(skb, GFP_ATOMIC);
+	if (!forward_skb) {
+		pr_warn("Dropping packet during TSB forwarding\n");
+		return -ENOMEM;
+	}
+
+	fwd_gh = (struct gn_header *)skb_network_header(forward_skb);
+	fwd_gh->gb_h.rhl--;
+
+	gn_dl->request(gn_dl, forward_skb, skb->dev->broadcast);
+	return 0;
+}
+
+static int gn_process_guc_packet(struct sk_buff *skb)
+{
+	struct btp_header *btp_h;
+	struct sockaddr_gn tosgn;
+	struct gn_iface *gnif;
+	struct gn_header *gh;
+
+	gh = (struct gn_header *)skb_network_header(skb);
+	GN_SET_BTP(skb, btp_h, struct gn_guc_header);
+
+	skb_reset_transport_header(skb);
+
+	tosgn.sgn_family = PF_GN;
+	tosgn.sgn_addr = gh->guc_h.depv.addr;
+	tosgn.sgn_port = be16_to_cpu(btp_h->dst_port);
+
+	if (gn_update_location_table(&gh->guc_h.sopv, false, NULL,
+				     &gh->guc_h.sn))
+		goto drop;
+
+	gnif = gn_find_interface(tosgn.sgn_addr);
+	if (!gnif) {
+		if (gn_forward_guc_packet(skb, tosgn.sgn_addr) == 0) {
+			kfree_skb(skb);
+			return NET_RX_SUCCESS;
+		}
+		goto drop;
+	}
+
+	if (gn_pass_payload_sock(&tosgn, skb) != NET_RX_SUCCESS)
+		goto drop;
+
+	return NET_RX_SUCCESS;
+drop:
+	kfree_skb(skb);
+	return NET_RX_DROP;
+}
+
+static u8 gn_decode_shape(u8 hst)
+{
+	switch (hst) {
+	case CH_HST_GABC_CIRCLE:
+		return GN_SHAPE_CIRCLE;
+	case CH_HST_GABC_RECT:
+		return GN_SHAPE_RECTANGLE;
+	case CH_HST_GABC_ELIP:
+		return GN_SHAPE_ELLIPSE;
+	default:
+		return GN_SHAPE_UNSPECIFIED;
+	};
+}
+
+static u8 gn_encode_shape(u8 shape)
+{
+	switch (shape) {
+	case GN_SHAPE_CIRCLE:
+		return CH_HST_GABC_CIRCLE;
+	case GN_SHAPE_RECTANGLE:
+		return CH_HST_GABC_RECT;
+	case GN_SHAPE_ELLIPSE:
+		return CH_HST_GABC_ELIP;
+	default:
+		return CH_HST_UNSPECIFIED;
+	};
+}
+
+static struct gn_geo_scope gn_decode_geo_scope(struct gn_header *gh)
+{
+	struct gn_geo_scope scope = {
+		.a = be16_to_cpu(gh->gbc_h.da),
+		.b = be16_to_cpu(gh->gbc_h.db),
+		.angle = be16_to_cpu(gh->gbc_h.angle),
+		.coord.lat = be32_to_cpu(gh->gbc_h.gap_lat),
+		.coord.lon = be32_to_cpu(gh->gbc_h.gap_lon),
+		.shape = gn_decode_shape(gh->gc_h.hst),
+	};
+	return scope;
+}
+
+static int gn_process_gxc_packet(struct sk_buff *skb)
+{
+	unsigned long long next_hop_addr;
+	struct gn_geo_scope scope;
+	struct btp_header *btp_h;
+	struct sockaddr_gn tosgn;
+	struct gn_iface *gnif;
+	struct gn_header *gh;
+	bool run_dpd;
+	s64 f_value;
+
+	next_hop_addr = GN_BROADCAST_ADDR;
+
+	gh = (struct gn_header *)skb_network_header(skb);
+	GN_SET_BTP(skb, btp_h, struct gn_gxc_header);
+
+	tosgn.sgn_family = PF_GN;
+	tosgn.sgn_addr = gh->gbc_h.sopv.addr;
+	tosgn.sgn_port = be16_to_cpu(btp_h->dst_port);
+
+	/* Resolve local GeoNetworking interface to check area membership */
+	gnif = gn_find_interface_by_dev(skb->dev);
+	if (!gnif)
+		goto drop;
+
+	scope = gn_decode_geo_scope(gh);
+	if (scope.shape == GN_SHAPE_UNSPECIFIED)
+		goto drop;
+
+	f_value = gn_F(gnif->pos.coord, scope);
+
+	if (f_value >= 0) {
+		// GeoAdhoc router is outside specified area
+		switch (GN_AREA_FORWARDING) {
+		case GN_AREA_FORWARDING_UNSPECIFIED:
+		case GN_AREA_FORWARDING_SIMPLE:
+			run_dpd = true;
+			break;
+		default:
+			run_dpd = false;
+			break;
+		}
+	} else {
+		// GeoAdhoc router is inside specified area
+		switch (GN_NON_AREA_FORWARDING) {
+		case GN_NON_AREA_FORWARDING_UNSPECIFIED:
+		case GN_NON_AREA_FORWARDING_GREEDY:
+			run_dpd = true;
+			break;
+		default:
+			run_dpd = false;
+		}
+	}
+	if (gn_update_location_table(&gh->gbc_h.sopv, false, NULL,
+				     run_dpd ? &gh->gbc_h.sn : NULL))
+		goto drop;
+	// Forwarding
+	if (gh->gb_h.rhl > 0 && ((gh->gc_h.ht == CH_HT_GAC && f_value >= 0) ||
+				 gh->gc_h.ht == CH_HT_GBC)) {
+		struct gn_basic_header *fwd_gb_h;
+		struct sk_buff *forward_skb;
+		u8 dest_addr[ETH_ALEN];
+		int rc;
+
+		forward_skb = skb_copy(skb, GFP_ATOMIC);
+		if (!forward_skb) {
+			pr_debug("dropping packet");
+			goto drop;
+		}
+		fwd_gb_h = (struct gn_basic_header *)skb_network_header(
+			forward_skb);
+		fwd_gb_h->rhl--;
+
+		rc = gn_gxc_forward(gnif, f_value, dest_addr, &gh->gbc_h.sopv);
+		switch (rc) {
+		case GN_FORWARD_NEXT_HOP:
+			gn_dl->request(gn_dl, forward_skb, dest_addr);
+			break;
+		case GN_FORWARD_BROADCAST:
+			gn_dl->request(gn_dl, forward_skb, skb->dev->broadcast);
+			break;
+		case GN_FORWARD_BUFFER:
+			pr_debug("forwarding buffers not implemented");
+			kfree_skb(forward_skb);
+			break;
+		case GN_FORWARD_DISCARD:
+			kfree_skb(forward_skb);
+			break;
+		default:
+			pr_debug("internal error: unexpected return value");
+			kfree_skb(forward_skb);
+			goto drop;
+		}
+	}
+
+	// Local delivery
+	if (f_value >= 0) {
+		if (gn_pass_payload_sock(&tosgn, skb) != NET_RX_SUCCESS)
+			goto drop;
+	} else {
+		kfree_skb(skb);
+	}
+
+	return NET_RX_SUCCESS;
+drop:
+	kfree_skb(skb);
+	return NET_RX_DROP;
+}
+
+static int gn_process_shb_packet(struct sk_buff *skb, const u8 *ll_address)
+{
+	struct btp_header *btp_h;
+	struct sockaddr_gn tosgn;
+	struct gn_header *gh;
+
+	gh = (struct gn_header *)skb_network_header(skb);
+	GN_SET_BTP(skb, btp_h, struct gn_shb_header);
+
+	/* Step 3: Duplicate Address Detection (DAD) check */
+
+	// 4. update PV in the LocTE
+	if (gn_update_location_table(&gh->shb_h.sopv, true, ll_address, NULL))
+		goto drop;
+
+	// 7. pass payload of GN_PDU to the upper protocol unit
+	tosgn.sgn_family = PF_GN;
+	tosgn.sgn_addr = gh->shb_h.sopv.addr;
+	tosgn.sgn_port = be16_to_cpu(btp_h->dst_port);
+
+	if (gn_pass_payload_sock(&tosgn, skb) != NET_RX_SUCCESS)
+		goto drop;
+
+	/* Step 8: Flush pending store-carry-forward buffers for source node */
+	gn_ls_flush(gh->shb_h.sopv.addr);
+
+	return NET_RX_SUCCESS;
+drop:
+	kfree_skb(skb);
+	return NET_RX_DROP;
+}
+
+static int gn_process_tsb_packet(struct sk_buff *skb)
+{
+	struct btp_header *btp_h;
+	struct sockaddr_gn tosgn;
+	struct gn_header *gh;
+
+	gh = (struct gn_header *)skb_network_header(skb);
+	GN_SET_BTP(skb, btp_h, struct gn_tsb_header);
+
+	/* Step 3: Duplicate Address Detection (DAD) check */
+
+	if (gn_update_location_table(&gh->tsb_h.sopv, false, NULL,
+				     &gh->tsb_h.sn))
+		goto drop;
+
+	if (gh->gb_h.rhl > 0)
+		gn_forward_tsb_packet(skb);
+
+	// 7. pass payload of GN_PDU to the upper protocol unit
+	tosgn.sgn_family = PF_GN;
+	tosgn.sgn_addr = gh->tsb_h.sopv.addr;
+	tosgn.sgn_port = be16_to_cpu(btp_h->dst_port);
+
+	if (gn_pass_payload_sock(&tosgn, skb) != NET_RX_SUCCESS)
+		goto drop;
+
+	/* Step 8: Flush pending store-carry-forward buffers for source node */
+	gn_ls_flush(gh->tsb_h.sopv.addr);
+
+	return NET_RX_SUCCESS;
+drop:
+	kfree_skb(skb);
+	return NET_RX_DROP;
+}
+
+static int gn_process_beacon_packet(struct sk_buff *skb, const u8 *llc)
+{
+	struct gn_header *gh = (struct gn_header *)skb_network_header(skb);
+
+	if (gn_update_location_table(&gh->beacon_h.sopv, true, llc, NULL)) {
+		pr_debug("LocT update failure\n");
+		kfree_skb(skb);
+		return NET_RX_DROP;
+	}
+
+	gn_ls_flush(gh->beacon_h.sopv.addr);
+	kfree_skb(skb);
+	return NET_RX_SUCCESS;
+}
+
+static int gn_process_ls_packet(struct sk_buff *skb)
+{
+	gn_address_t dest_addr;
+	struct gn_iface *gnif;
+	struct gn_header *gh;
+
+	gh = (struct gn_header *)skb_network_header(skb);
+
+	if (gh->gc_h.hst == CH_HST_LS_REQUEST) {
+		struct gn_ls_request_header *gls_req_h = &gh->ls_request_h;
+
+		// 2. execute dad
+		// 3. update loc_te
+		dest_addr = gls_req_h->addr;
+		if (gn_update_location_table(&gls_req_h->sopv, false, NULL,
+					     &gls_req_h->sn))
+			goto drop;
+		// 4. find out if the packet has to be answered or forwarded
+		gnif = gn_find_interface(dest_addr);
+		if (gnif) {
+			// has to be answered
+			/* Note: gn_spv is a prefix (addr, tst, lat, lon) of gn_lpv */
+			gn_location_service_reply(
+				(struct gn_spv *)&gls_req_h->sopv, gnif,
+				dest_addr, 0);
+		} else {
+			// has to be forwarded like a tsb
+			// 5. try to flush own forward buffer
+			gn_ls_flush(gls_req_h->sopv.addr);
+			// 6. forward like a tsb
+			gn_forward_tsb_packet(skb);
+		}
+	} else if (gh->gc_h.hst == CH_HST_LS_REPLY) {
+		struct gn_ls_reply_header *gls_rep_h = &gh->ls_reply_h;
+		// 2. execute dad
+		// 3. update loc_te
+		if (gn_update_location_table(&gls_rep_h->sopv, false, NULL,
+					     &gls_rep_h->sn))
+			goto drop;
+		dest_addr = gls_rep_h->depv.addr;
+
+		// 4. flush forward buffer
+		gn_ls_flush(gls_rep_h->sopv.addr);
+		// 5. find out if the packet has to be forwarded
+		if (!gn_find_interface(dest_addr))
+			gn_forward_guc_packet(skb, dest_addr);
+	} else {
+		goto drop;
+	}
+	kfree_skb(skb);
+	return NET_RX_SUCCESS;
+drop:
+	//pr_info("Packet was dropped.");
+	kfree_skb(skb);
+	return NET_RX_DROP;
+}
+
+/**	gn_rcv() - Receive a packet (in skb) from device dev
+ *	@skb - packet received
+ *	@dev - network device where the packet comes from
+ *	@pt - packet type
+ *
+ *	Receive a packet (in skb) from device dev. This has come from the SNAP
+ *	decoder, and on entry skb->network_header is the GN Basic Header.
+ *	The physical headers have been extracted.
+ */
+static int gn_rcv(struct sk_buff *skb, struct net_device *dev,
+		  struct packet_type *pt, struct net_device *orig_dev)
+{
+	struct gn_header *gh;
+	struct ethhdr *eth;
+
+	if (!net_eq(dev_net(dev), &init_net))
+		goto drop;
+
+	if (dev->type != ARPHRD_ETHER)
+		goto drop;
+
+	if (!gn_find_interface_by_dev(dev))
+		goto drop;
+
+	skb = skb_share_check(skb, GFP_ATOMIC);
+
+	if (!skb)
+		goto drop;
+
+	eth = (struct ethhdr *)skb_mac_header(skb);
+	// TODO: validate
+	if (skb->pkt_type == PACKET_LOOPBACK ||
+	    ether_addr_equal(eth->h_source, dev->dev_addr))
+		goto drop;
+
+	skb_reset_network_header(skb);
+
+	if (!pskb_may_pull(skb, GN_BASE_HEADER_SIZE))
+		goto drop;
+
+	// Basic Header processing
+	gh = (struct gn_header *)skb_network_header(skb);
+
+	if (gh->gb_h.version != GN_VERSION ||
+	    gh->gb_h.nh != BH_NH_COMMON_HEADER) {
+		pr_debug("corrupt packet");
+		goto drop;
+	}
+
+	/*
+	 * Retrieve information from socketbuffer an insert it into according headers;
+	 * this is necessary to determine if the packet is ours and find the matching
+	 * socket, or if the packet has to be forwarded
+	 */
+	// Common Header processing
+
+	//skb_trim(skb, min_t(unsigned int, skb->len, gc_h->pl + sizeof(*gb_h)
+	// + sizeof(*gc_h)));
+
+	if (gh->gc_h.mhl < gh->gb_h.rhl)
+		goto drop;
+
+	switch (gh->gc_h.ht) {
+	case CH_HT_GUC:
+		if (!pskb_may_pull(skb, GN_BASE_HEADER_SIZE + GN_GUC_HLEN +
+						BTP_HLEN))
+			goto drop;
+		return gn_process_guc_packet(skb);
+	case CH_HT_GAC:
+	case CH_HT_GBC:
+		if (!pskb_may_pull(skb, GN_BASE_HEADER_SIZE +
+						GN_GXC_HLEN + BTP_HLEN)
+			goto drop;
+		return gn_process_gxc_packet(skb);
+	case CH_HT_TSB:
+		if (gh->gc_h.hst == CH_HST_TSB_SINGLE_HOP) {
+			if (!pskb_may_pull(skb, GN_BASE_HEADER_SIZE +
+							GN_SHB_HLEN + BTP_HLEN))
+				goto drop;
+			return gn_process_shb_packet(skb, eth->h_source);
+		}
+		if (gh->gc_h.hst == CH_HST_TSB_MULTI_HOP) {
+			if (!pskb_may_pull(skb, GN_BASE_HEADER_SIZE +
+							GN_TSB_HLEN + BTP_HLEN))
+				goto drop;
+			return gn_process_tsb_packet(skb);
+		}
+		goto drop;
+	case CH_HT_BEACON:
+		if (!pskb_may_pull(skb, GN_BASE_HEADER_SIZE + GN_BEACON_HLEN))
+			goto drop;
+		return gn_process_beacon_packet(skb, eth->h_source);
+	case CH_HT_LS:
+		if (gh->gc_h.hst == CH_HST_LS_REQUEST) {
+			if (!pskb_may_pull(skb, GN_BASE_HEADER_SIZE +
+							GN_LS_REQUEST_HLEN))
+				goto drop;
+		} else if (gh->gc_h.hst == CH_HST_LS_REPLY) {
+			if (!pskb_may_pull(skb, GN_BASE_HEADER_SIZE +
+							GN_LS_REPLY_HLEN))
+				goto drop;
+		} else {
+			goto drop;
+		}
+		return gn_process_ls_packet(skb);
+	default:
+		goto drop;
+	}
+drop:
+	kfree_skb(skb);
+	return NET_RX_DROP;
+}
+
+static int gn_fill_guc_header(struct gn_guc_header *guc_h,
+			      struct gn_iface *gnif, gn_address_t dest_addr,
+			      struct gn_spv *depv, struct gn_sock *gn)
+{
+	gn_address_t saddr = gn->src_addr ? gn->src_addr : gnif->address;
+
+	memset(guc_h, 0, sizeof(*guc_h));
+	guc_h->sn = cpu_to_be16(gn_if_next_sn(gnif));
+	gn_fill_sopv(gnif, &guc_h->sopv, saddr);
+	guc_h->depv.addr = dest_addr;
+	if (depv && depv->tst) {
+		guc_h->depv.tst = depv->tst;
+		guc_h->depv.lat = depv->lat;
+		guc_h->depv.lon = depv->lon;
+	} else {
+		guc_h->depv.tst = htonl(0);
+		guc_h->depv.lat = htonl(0);
+		guc_h->depv.lon = htonl(0);
+	}
+	return 0;
+}
+
+static int gn_fill_gxc_header(struct gn_gxc_header *gxc_h,
+			      struct gn_iface *gnif, struct gn_sock *gn)
+{
+	gn_address_t saddr = gn->src_addr ? gn->src_addr : gnif->address;
+
+	WARN_ON_ONCE(gn->scope.scope_type != GN_SCOPE_GEOGRAPHICAL &&
+		     gn->scope.scope_type != GN_SCOPE_GEOGRAPHICAL_ANYCAST);
+
+	memset(gxc_h, 0, sizeof(*gxc_h));
+	gxc_h->sn = cpu_to_be16(gn_if_next_sn(gnif));
+	gn_fill_sopv(gnif, &gxc_h->sopv, saddr);
+
+	gxc_h->gap_lat = cpu_to_be32(gn->scope.geo_scope.coord.lat);
+	gxc_h->gap_lon = cpu_to_be32(gn->scope.geo_scope.coord.lon);
+	gxc_h->angle = cpu_to_be16(gn->scope.geo_scope.angle);
+	switch (gn->scope.geo_scope.shape) {
+	case GN_SHAPE_CIRCLE:
+		gxc_h->da = cpu_to_be16(gn->scope.geo_scope.a);
+		break;
+	case GN_SHAPE_ELLIPSE:
+	case GN_SHAPE_RECTANGLE:
+		gxc_h->da = cpu_to_be16(gn->scope.geo_scope.a);
+		gxc_h->db = cpu_to_be16(gn->scope.geo_scope.b);
+		break;
+	default:
+	case GN_SHAPE_UNSPECIFIED:
+		pr_debug("unknown shape type");
+		break;
+	}
+	return 0;
+}
+
+static int gn_fill_shb_header(struct gn_shb_header *shb_h,
+			      struct gn_iface *gnif, struct gn_sock *gn)
+{
+	gn_address_t saddr = gn->src_addr ? gn->src_addr : gnif->address;
+
+	memset(shb_h, 0, sizeof(*shb_h));
+	gn_fill_sopv(gnif, &shb_h->sopv, saddr);
+
+	// prefill media dependent data field with empty
+	shb_h->mdd = htonl(0);
+	return 0;
+}
+
+static int gn_fill_tsb_header(struct gn_tsb_header *tsb_h,
+			      struct gn_iface *gnif, struct gn_sock *gn)
+{
+	gn_address_t saddr = gn->src_addr ? gn->src_addr : gnif->address;
+
+	memset(tsb_h, 0, sizeof(*tsb_h));
+	tsb_h->sn = cpu_to_be16(gn_if_next_sn(gnif));
+	gn_fill_sopv(gnif, &tsb_h->sopv, saddr);
+	return 0;
+}
+
+/**
+ * gn_sendmsg - always called if sendmsg() is called on a socket with an
+ * address family matching AF_GN
+ * @msghdr - contains information, including the payload of the packet
+ * @len - the size of the payload
+ */
+static int gn_sendmsg(struct socket *sock, struct msghdr *msg, size_t len)
+{
+	DECLARE_SOCKADDR(struct sockaddr_gn *, usgn, msg->msg_name);
+	int packet_type, packet_subtype, btp_type;
+	struct gn_common_header *gc_h;
+	struct gn_basic_header *gb_h;
+	struct sockaddr_gn local_sgn;
+	struct gn_spv depv = { 0 };
+	u8 rhl = DEFAULT_HOP_LIMIT;
+	int flags = msg->msg_flags;
+	struct btp_header *btp_h;
+	struct net_device *dev;
+	struct gn_iface *gnif;
+	struct sk_buff *skb;
+	struct gn_sock *gn;
+	int err = -EINVAL;
+	struct sock *sk;
+	u32 eh_size;
+	void *gp_h;
+	u32 size;
+
+	sk = sock->sk;
+	gn = gn_sk(sk);
+
+	if (flags & ~(MSG_DONTWAIT | MSG_CMSG_COMPAT)) {
+		pr_debug("unsupported sendmsg flags");
+		return -EINVAL;
+	}
+
+	if (len > GN_MAXSZ) {
+		pr_warn("message too long (got %zu, maximum %d)", len,
+			GN_MAXSZ);
+		return -EMSGSIZE;
+	}
+	lock_sock(sk);
+	if (usgn) {
+		err = -EBUSY;
+		if (sock_flag(sk, SOCK_ZAPPED)) {
+			pr_debug("socket zapped and autobind not implemented");
+			if (gn_autobind(sk) < 0)
+				goto out;
+		}
+		err = -EINVAL;
+		if (msg->msg_namelen < sizeof(*usgn) ||
+		    usgn->sgn_family != AF_GN) {
+			pr_debug("incorrect address family\n");
+			goto out;
+		}
+		//appletalk makes another check here
+	} else {
+		err = -ENOTCONN;
+		if (sk->sk_state != TCP_ESTABLISHED)
+			goto out;
+		usgn = &local_sgn;
+		usgn->sgn_family = AF_GN;
+		usgn->sgn_port = gn->dst_port;
+		usgn->sgn_addr = gn->dst_addr;
+	}
+	/*
+	 * Before the packet can be send, we have determine if the gn_address of
+	 * the source is bound to an interface. The interface provides the device
+	 * for sending the packet.
+	 */
+	gnif = gn_find_interface(gn->src_addr);
+
+	if (!gnif) {
+		pr_debug("Could not find an interface\n");
+		err = -EFAULT;
+		goto out;
+	}
+	dev = gnif->dev;
+
+	packet_subtype = CH_HST_UNSPECIFIED;
+	if (usgn->sgn_addr != 0 && usgn->sgn_addr != GNADDR_BROADCAST &&
+	    gn->scope.scope_type == GN_SCOPE_UNSPECIFIED) {
+		packet_type = CH_HT_GUC;
+	} else {
+		switch (gn->scope.scope_type) {
+		case GN_SCOPE_GEOGRAPHICAL:
+			packet_type = CH_HT_GBC;
+			packet_subtype =
+				gn_encode_shape(gn->scope.geo_scope.shape);
+			break;
+		case GN_SCOPE_GEOGRAPHICAL_ANYCAST:
+			packet_type = CH_HT_GAC;
+			packet_subtype =
+				gn_encode_shape(gn->scope.geo_scope.shape);
+			break;
+		case GN_SCOPE_TOPOLOGICAL:
+			packet_type = CH_HT_TSB;
+			if (gn->scope.topo_hops <= 1)
+				packet_subtype = CH_HST_TSB_SINGLE_HOP;
+			else
+				packet_subtype = CH_HST_TSB_MULTI_HOP;
+			break;
+		case GN_SCOPE_UNSPECIFIED:
+			packet_type = CH_HT_TSB;
+			packet_subtype = CH_HST_TSB_SINGLE_HOP;
+			break;
+		default:
+			err = -EINVAL;
+			goto out;
+		}
+	}
+
+	switch (gn->protocol) {
+	case GN_PROTO_BTP_A:
+		btp_type = CH_NH_BTPA;
+		break;
+	case GN_PROTO_BTP_B:
+		btp_type = CH_NH_BTPB;
+		break;
+	default:
+		err = -EINVAL;
+		goto out;
+	}
+
+	// Determine extended (header type specific) header size
+	switch (packet_type) {
+	case CH_HT_GUC:
+		eh_size = sizeof(struct gn_guc_header);
+		break;
+	case CH_HT_GAC:
+	case CH_HT_GBC:
+		eh_size = sizeof(struct gn_gxc_header);
+		break;
+	case CH_HT_TSB:
+		if (packet_subtype == CH_HST_TSB_SINGLE_HOP) {
+			eh_size = sizeof(struct gn_shb_header);
+		} else if (packet_subtype == CH_HST_TSB_MULTI_HOP) {
+			eh_size = sizeof(struct gn_tsb_header);
+		} else {
+			pr_debug("internal error");
+			err = -EINVAL;
+			goto out;
+		}
+		break;
+	default:
+		pr_debug("internal error");
+		err = -EINVAL;
+		goto out;
+	}
+
+	/*
+	 * Find out, how much memory has to be allocated for the packet and
+	 * allocate it.
+	 */
+	size = gn_dl->header_length;
+	size += dev->hard_header_len;
+	size += sizeof(struct gn_basic_header);
+	size += sizeof(struct gn_common_header);
+	size += eh_size;
+	size += sizeof(struct btp_header);
+	size += len;
+
+	release_sock(sk);
+	skb = sock_alloc_send_skb(sk, size, (flags & MSG_DONTWAIT), &err);
+	lock_sock(sk);
+	if (!skb) {
+		err = -ENOMEM;
+		goto out;
+	}
+	/*
+	 * Reserve memory in the socketbuffer for datalink, hardware, our headers
+	 */
+	skb_reserve(skb, gn_dl->header_length);
+	skb_reserve(skb, dev->hard_header_len);
+	skb_reserve(skb, sizeof(struct gn_basic_header));
+	skb_reserve(skb, sizeof(struct gn_common_header));
+	skb_reserve(skb, eh_size);
+	skb_reserve(skb, sizeof(struct btp_header));
+
+	skb->dev = dev;
+	/*
+	 * Copy userdata from socket into the payload of the packet
+	 */
+	err = memcpy_from_msg(skb_put(skb, len), msg, len);
+	if (err) {
+		pr_debug("could not extract from msg\n");
+		kfree_skb(skb);
+		err = -EFAULT;
+		goto out;
+	}
+
+	/*
+	 * Get positions of headers and fill them
+	 */
+	btp_h = skb_push(skb, sizeof(struct btp_header));
+	gp_h = skb_push(skb, eh_size);
+	gc_h = skb_push(skb, sizeof(struct gn_common_header));
+	gb_h = skb_push(skb, sizeof(struct gn_basic_header));
+
+	switch (btp_type) {
+	case CH_NH_BTPA:
+		btp_h->src_port = htons(gn->src_port);
+		btp_h->dst_port = htons(usgn->sgn_port);
+		break;
+	case CH_NH_BTPB:
+		btp_h->dst_port = htons(usgn->sgn_port);
+		btp_h->src_port = 0;
+		break;
+	default:
+		pr_debug("internal error");
+		kfree_skb(skb);
+		err = -EINVAL;
+		goto out;
+	}
+
+	switch (packet_type) {
+	case CH_HT_GUC:
+		gn_fill_depv(&depv, usgn->sgn_addr);
+		gn_fill_guc_header((struct gn_guc_header *)gp_h, gnif,
+				   usgn->sgn_addr, &depv, gn);
+		break;
+	case CH_HT_GAC:
+	case CH_HT_GBC:
+		gn_fill_gxc_header((struct gn_gxc_header *)gp_h, gnif, gn);
+		break;
+	case CH_HT_TSB:
+		if (packet_subtype == CH_HST_TSB_SINGLE_HOP) {
+			rhl = 1;
+			gn_fill_shb_header((struct gn_shb_header *)gp_h, gnif,
+					   gn);
+		} else if (packet_subtype == CH_HST_TSB_MULTI_HOP) {
+			//at this point it is safe to assume that topological scope is used
+			rhl = gn->scope.topo_hops ?: DEFAULT_HOP_LIMIT;
+			gn_fill_tsb_header((struct gn_tsb_header *)gp_h, gnif,
+					   gn);
+		} else {
+			pr_debug("internal error");
+			kfree_skb(skb);
+			err = -EINVAL;
+			goto out;
+		}
+		break;
+	}
+
+	/* Note: BTP header type (A/B) is determined by socket protocol */
+	gn_fill_bh_ch(gnif, (struct gn_header *)gb_h, packet_type,
+		      packet_subtype, rhl, btp_type,
+		      htons(len + sizeof(struct btp_header)));
+
+	/* Note: DEPV is populated during location service queue processing */
+	if (packet_type == CH_HT_GUC) {
+		u8 ll_address[ETH_ALEN];
+		int queue_rc = gn_ls_queue(usgn->sgn_addr, skb);
+
+		switch (queue_rc) {
+		case GN_QUEUE_LS_STALE:
+			// We need to perform a LS request
+			gn_location_service_req(gnif, gn->src_addr,
+						usgn->sgn_addr);
+			break;
+		case GN_QUEUE_LS_PENDING:
+			// LS request is pending, we're done
+			break;
+		case GN_QUEUE_DIRECT:
+			/* Destination in LocTE; resolve MAC or greedy next-hop */
+			if (gn_query_ll_nexthop(gnif, usgn->sgn_addr,
+						ll_address))
+				gn_dl->request(gn_dl, skb, dev->broadcast);
+			else
+				gn_dl->request(gn_dl, skb, ll_address);
+			break;
+		case GN_QUEUE_ERROR:
+		default:
+			kfree_skb(skb);
+			err = -ENOMEM;
+			goto out;
+		}
+	} else {
+		// Not a unicast packet
+		gn_dl->request(gn_dl, skb, dev->broadcast);
+	}
+
+	/* Destination position vector routing handled by location service queue */
+
+	err = len;
+out:
+	release_sock(sk);
+	return err;
+}
+
+/**
+ * gn_recvmsg - called, when recv() is called on a socket with AF = AF_GN.
+ * Copies the data of a received packet into msg.
+ * @sock - socket, on which recv() was called
+ * @msg - buffer used to retrieve packetdata
+ * @size - size of msg
+ * @flags - flags for receiving
+ *
+ */
+
+static int gn_recvmsg(struct socket *sock, struct msghdr *msg, size_t size,
+		      int flags)
+{
+	struct sock *sk = sock->sk;
+	struct gn_header *gh;
+	struct sk_buff *skb;
+	u16 copied = 0;
+	int err = 0;
+	u32 offset;
+
+	/* It is necessary to find the actual length of the payload, which,
+	 * in case of an unsecured package, resides in the commonheader and
+	 * still has to be calculated without the length of the btp-header
+	 * (4byte).
+	 * Furthermore, the datapointer probably has to be set to the
+	 * beginning of the actual payload
+	 */
+	skb = skb_recv_datagram(sk, flags & MSG_DONTWAIT, &err);
+	lock_sock(sk);
+	if (!skb)
+		goto out;
+
+	/* Note: Socket validation checks performed during bind/connect */
+	gh = (struct gn_header *)skb_network_header(skb);
+
+	copied = be16_to_cpu(gh->gc_h.pl);
+	offset = skb->transport_header - skb->network_header +
+		 sizeof(struct btp_header);
+	copied -= sizeof(struct btp_header);
+
+	/* Handle truncated datagram reception when user buffer is smaller than
+	 * payload */
+	if (copied > size) {
+		copied = size;
+		msg->msg_flags |= MSG_TRUNC;
+	}
+
+	err = skb_copy_datagram_msg(skb, offset, msg, copied);
+
+	skb_free_datagram(sk, skb);
+
+out:
+	release_sock(sk);
+	return err ?: copied;
+}
+
+/*
+ * Geonetworking timer callbacks
+ */
+static int gn_send_beacon(struct gn_iface *gnif)
+{
+	/* Note: Media dependent procedures (e.g. ITS-G5 DCC / DCC Access)
+	 * are evaluated here */
+	struct gn_beacon_header *gbe_h;
+	struct gn_common_header *gc_h;
+	struct gn_basic_header *gb_h;
+	struct sk_buff *skb;
+	unsigned int size;
+
+	size = gn_dl->header_length;
+	size += gnif->dev->hard_header_len;
+	size += sizeof(struct gn_basic_header);
+	size += sizeof(struct gn_common_header);
+	size += sizeof(struct gn_beacon_header);
+
+	skb = netdev_alloc_skb(gnif->dev, size);
+	if (!skb)
+		goto out;
+
+	skb_reserve(skb, size);
+	skb->dev = gnif->dev;
+
+	gbe_h = skb_push(skb, sizeof(struct gn_beacon_header));
+	gc_h = skb_push(skb, sizeof(struct gn_common_header));
+	gb_h = skb_push(skb, sizeof(struct gn_basic_header));
+
+	gn_fill_bh_ch_nopayload(gnif, (struct gn_header *)gb_h, CH_HT_BEACON,
+				CH_HST_BEACON, 1);
+
+	gn_fill_sopv(gnif, &gbe_h->sopv, gnif->address);
+
+	gn_dl->request(gn_dl, skb, gnif->dev->broadcast);
+
+out:
+	return 0;
+}
+
+static void gn_send_beacons(struct timer_list *tl)
+{
+	struct gn_iface *gnif;
+	bool empty = true;
+
+	rcu_read_lock();
+	hlist_for_each_entry_rcu(gnif, &gn_interfaces, hnode) {
+		gn_send_beacon(gnif);
+		empty = false;
+	}
+	rcu_read_unlock();
+
+	if (!empty)
+		mod_timer(tl, jiffies + msecs_to_jiffies(
+						GN_BEACON_RETRANSMIT_TIME));
+}
+
+static void gn_activate_beacon(void)
+{
+	mod_timer(&gn_beacon_timer,
+		  jiffies + msecs_to_jiffies(GN_BEACON_RETRANSMIT_TIME));
+}
+
+static int validate_geo_scope(struct gn_geo_scope *scope)
+{
+	if (scope->angle > 360)
+		return -EINVAL;
+
+	if (scope->a == 0)
+		return -EINVAL;
+
+	switch (scope->shape) {
+	case GN_SHAPE_CIRCLE:
+		return scope->b != 0 ? -EINVAL : 0;
+	case GN_SHAPE_RECTANGLE:
+	case GN_SHAPE_ELLIPSE:
+		return 0;
+	default:
+	case GN_SHAPE_UNSPECIFIED:
+		return -EINVAL;
+	}
+	return 0;
+}
+
+static int validate_scope(struct gn_scope *scope)
+{
+	if (scope->scope_type < GN_SCOPE_TOPOLOGICAL ||
+	    scope->scope_type > GN_SCOPE_MAX)
+		return -EINVAL;
+	switch (scope->scope_type) {
+	case GN_SCOPE_TOPOLOGICAL:
+		return 0;
+	case GN_SCOPE_GEOGRAPHICAL:
+	case GN_SCOPE_GEOGRAPHICAL_ANYCAST:
+		return validate_geo_scope(&scope->geo_scope);
+	default:
+		return -EINVAL;
+	}
+	return 0;
+}
+
+static int gn_setsockopt(struct socket *sock, int level, int optname,
+			 sockptr_t optval, unsigned int optlen)
+{
+	struct sock *sk = sock->sk;
+	int rc = -ENOPROTOOPT;
+	struct gn_scope opt;
+	struct gn_sock *gn;
+
+	gn = gn_sk(sk);
+
+	if (level != SOL_GN || optname != GN_SCOPE)
+		goto out;
+
+	rc = -EINVAL;
+	if (optlen < sizeof(struct gn_scope))
+		goto out;
+
+	rc = -EFAULT;
+	if (copy_from_sockptr(&opt, optval, sizeof(struct gn_scope)))
+		goto out;
+
+	rc = validate_scope(&opt);
+	if (rc < 0)
+		goto out;
+
+	lock_sock(sk);
+	memcpy(&gn->scope, &opt, sizeof(struct gn_scope));
+	release_sock(sk);
+
+	rc = 0;
+out:
+	return rc;
+}
+
+static int gn_getsockopt(struct socket *sock, int level, int optname,
+			 char __user *optval, int __user *optlen)
+{
+	int len, rc = -ENOPROTOOPT;
+	struct sock *sk = sock->sk;
+	struct gn_sock *gn;
+
+	gn = gn_sk(sk);
+
+	if (level != SOL_GN || optname != GN_SCOPE)
+		goto out;
+
+	rc = -EFAULT;
+	if (get_user(len, optlen))
+		goto out;
+
+	rc = -EINVAL;
+	if (len < 0)
+		goto out;
+
+	len = min_t(unsigned int, len, sizeof(struct gn_scope));
+
+	rc = -EFAULT;
+	if (put_user(len, optlen))
+		goto out;
+
+	lock_sock(sk);
+	rc = copy_to_user(optval, &gn->scope, len) ? -EFAULT : 0;
+	release_sock(sk);
+out:
+	return rc;
+}
+
+/*
+ * validate a gn_position coming from userspace
+ */
+int gn_validate_pos(struct gn_position *pos)
+{
+	if (pos->tst.tv_sec < 0 || pos->tst.tv_nsec < 0 ||
+	    pos->tst.tv_nsec >= NSEC_PER_SEC)
+		return -EINVAL;
+	return 0;
+}
+
+static int gn_ioctl(struct socket *sock, unsigned int cmd, unsigned long arg)
+{
+	void __user *argp = (void __user *)arg;
+	struct sock *sk = sock->sk;
+	int rc = -ENOIOCTLCMD;
+
+	switch (cmd) {
+	/* Protocol layer */
+	case TIOCOUTQ: {
+		long amount = sk->sk_sndbuf - sk_wmem_alloc_get(sk);
+
+		if (amount < 0)
+			amount = 0;
+		rc = put_user(amount, (int __user *)argp);
+		break;
+	}
+	case TIOCINQ: {
+		/*
+		 * These two are safe on a single CPU system as only
+		 * user tasks fiddle here
+		 */
+		struct sk_buff *skb = skb_peek(&sk->sk_receive_queue);
+		long amount = 0;
+
+		if (skb)
+			amount = skb->len - sizeof(struct gn_header);
+		rc = put_user(amount, (int __user *)argp);
+		break;
+	}
+	case SIOCGSTAMP_OLD:
+	case SIOCGSTAMP_NEW:
+	case SIOCGSTAMPNS_OLD:
+	case SIOCGSTAMPNS_NEW:
+		break;
+	}
+
+	return rc;
+}
+
+#ifdef CONFIG_COMPAT
+static int gn_compat_ioctl(struct socket *sock, unsigned int cmd,
+			   unsigned long arg)
+{
+	/* All GeoNetworking ioctl commands (TIOCOUTQ, TIOCINQ, SIOCGIFADDR, etc.)
+	 * and struct gn_position (using struct __kernel_timespec) are 64-bit
+	 * clean and compat-safe.
+	 */
+	return gn_ioctl(sock, cmd, arg);
+}
+#endif
+
+static const struct net_proto_family gn_family_ops = {
+	.family = PF_GN,
+	.create = gn_create,
+	.owner = THIS_MODULE,
+};
+
+static const struct proto_ops gn_dgram_ops = {
+	.family = PF_GN,
+	.owner = THIS_MODULE,
+	.release = gn_release,
+	.bind = gn_bind,
+	.connect = gn_connect,
+	.socketpair = sock_no_socketpair,
+	.accept = sock_no_accept,
+	.getname = sock_no_getname,
+	.poll = datagram_poll,
+	.ioctl = gn_ioctl,
+#ifdef CONFIG_COMPAT
+	.compat_ioctl = gn_compat_ioctl,
+#endif
+	.listen = sock_no_listen,
+	.shutdown = sock_no_shutdown,
+	.setsockopt = gn_setsockopt,
+	.getsockopt = gn_getsockopt,
+	.sendmsg = gn_sendmsg,
+	.recvmsg = gn_recvmsg,
+	.mmap = sock_no_mmap,
+};
+
+static struct notifier_block gn_notifier = {
+	.notifier_call = gn_device_event,
+};
+
+static struct packet_type gn_packet_type __read_mostly = {
+	.type = cpu_to_be16(ETH_P_GN),
+	.func = gn_rcv,
+};
+
+/*
+ * SNAP-ID for Geonetworking 0x8947
+ * Note: SNAP header format uses big-endian 0x8947 per ETSI EN 302 636-4-1
+ */
+static unsigned char gn_snap_id[] = { 0x00, 0x00, 0x00, 0x89, 0x47 };
+
+/* Called by proto.c on kernel start up */
+static int __init gn_init(void)
+{
+	int rc;
+
+	timer_setup(&gn_beacon_timer, gn_send_beacons, 0);
+
+	rc = proto_register(&gn_proto, 0);
+	if (rc)
+		return rc;
+
+	rc = sock_register(&gn_family_ops);
+	if (rc)
+		goto out_proto;
+
+	gn_dl = register_snap_client(gn_snap_id, gn_rcv);
+	if (!gn_dl) {
+		pr_warn("Unable to register GeoNetworking with SNAP.\n");
+		rc = -ENOMEM;
+		goto out_snap;
+	}
+
+	dev_add_pack(&gn_packet_type);
+
+	rc = register_netdevice_notifier(&gn_notifier);
+	if (rc)
+		goto out_dev;
+
+#ifdef CONFIG_SYSCTL
+	rc = gn_register_sysctl();
+	if (rc)
+		goto out_nd;
+#endif
+
+	rc = gn_netlink_init();
+	if (rc)
+		goto out_sysctl;
+
+	return 0;
+
+out_sysctl:
+#ifdef CONFIG_SYSCTL
+	gn_unregister_sysctl();
+#endif
+out_nd:
+	unregister_netdevice_notifier(&gn_notifier);
+out_dev:
+	dev_remove_pack(&gn_packet_type);
+	unregister_snap_client(gn_dl);
+out_snap:
+	sock_unregister(PF_GN);
+out_proto:
+	proto_unregister(&gn_proto);
+	return rc;
+}
+module_init(gn_init);
+
+static void __exit gn_exit(void)
+{
+	gn_netlink_exit();
+
+#ifdef CONFIG_SYSCTL
+	gn_unregister_sysctl();
+#endif /* CONFIG_SYSCTL */
+
+	timer_shutdown_sync(&gn_beacon_timer);
+
+	unregister_netdevice_notifier(&gn_notifier);
+	dev_remove_pack(&gn_packet_type);
+	unregister_snap_client(gn_dl);
+	sock_unregister(PF_GN);
+	proto_unregister(&gn_proto);
+
+	gn_interfaces_clear();
+	gn_routing_exit();
+	rcu_barrier();
+}
+module_exit(gn_exit);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Mr Noname <email.here@domain");
+MODULE_DESCRIPTION("GeoNetworking protocol");
+MODULE_ALIAS_NETPROTO(PF_GN);
diff --git a/net/gn/gn_routing.c b/net/gn/gn_routing.c
new file mode 100644
index 000000000000..7d8df7165ecb
--- /dev/null
+++ b/net/gn/gn_routing.c
@@ -0,0 +1,646 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": %s: " fmt, __func__
+#include <linux/types.h>
+#include <linux/bitfield.h>
+#include <linux/hashtable.h>
+#include <linux/spinlock.h>
+#include <linux/skbuff.h>
+#include <net/datalink.h>
+
+#include <linux/gn_routing.h>
+#include <linux/gn.h>
+
+extern struct datalink_proto *gn_dl;
+
+static DEFINE_HASHTABLE(gn_loc_t, 3);
+static DEFINE_SPINLOCK(gn_loc_t_lock);
+
+// ###### values in meters
+#define EARTH_RADIUS 6378388ULL
+#define PI 31415926ULL // * 10^7
+#define METER_PER_LON 111317ULL
+#define RAD_PER_DEGREE 174533ULL // * 10^7 - same as long and lat from PV
+
+#define GN_LT_JIFFIES msecs_to_jiffies(GN_LOC_TE_LIFETIME)
+#define GN_TST_VALID(tst) \
+	time_before(jiffies, (unsigned long)(tst) + GN_LT_JIFFIES)
+
+/* GeoNetworking routing */
+
+static u16 *gn_dpd_find(struct gn_dpd_buf *buf, u16 sn)
+{
+	int i = 0;
+
+	for (; i < GN_DPL_SIZE; i++) {
+		if (buf->sn[i] == sn)
+			return &buf->sn[i];
+	}
+	return NULL;
+}
+
+static void gn_dpd_insert(struct gn_dpd_buf *buf, u16 sn)
+{
+	buf->sn[buf->head] = sn;
+	buf->head = (buf->head + 1) % GN_DPL_SIZE;
+}
+
+static u32 pdr(u32 old_pdr, u32 delta)
+{
+	if (!delta)
+		delta = 1;
+	return (GN_MAX_PDR_EMA_BETA * old_pdr) / 100 +
+	       (((100 - GN_MAX_PDR_EMA_BETA) * (100 / delta)) / 100);
+}
+
+// table is * 1000 | p1 * 100 entry
+static const int cos_table[] = {
+	100000, 99995,	99980,	99955,	99920,	99875,	99820,	99755,	99680,
+	99595,	99500,	99396,	99281,	99156,	99022,	98877,	98723,	98558,
+	98384,	98200,	98007,	97803,	97590,	97367,	97134,	96891,	96639,
+	96377,	96106,	95824,	95534,	95233,	94924,	94604,	94275,	93937,
+	93590,	93233,	92866,	92491,	92106,	91712,	91309,	90897,	90475,
+	90045,	89605,	89157,	88699,	88233,	87758,	87274,	86782,	86281,
+	85771,	85252,	84726,	84190,	83646,	83094,	82534,	81965,	81388,
+	80803,	80210,	79608,	78999,	78382,	77757,	77125,	76484,	75836,
+	75181,	74517,	73847,	73169,	72484,	71791,	71091,	70385,	69671,
+	68950,	68222,	67488,	66746,	65998,	65244,	64483,	63715,	62941,
+	62161,	61375,	60582,	59783,	58979,	58168,	57352,	56530,	55702,
+	54869,	54030,	53186,	52337,	51482,	50622,	49757,	48887,	48012,
+	47133,	46249,	45360,	44466,	43568,	42666,	41759,	40849,	39934,
+	39015,	38092,	37166,	36236,	35302,	34365,	33424,	32480,	31532,
+	30582,	29628,	28672,	27712,	26750,	25785,	24818,	23848,	22875,
+	21901,	20924,	19945,	18964,	17981,	16997,	16010,	15023,	14033,
+	13042,	12050,	11057,	10063,	9067,	8071,	7074,	6076,	5077,
+	4079,	3079,	2079,	1080,	0,	-920,	-1920,	-2920,	-3919,
+	-4918,	-5917,	-6915,	-7912,	-8909,	-9904,	-10899, -11892, -12884,
+	-13875, -14865, -15853, -16840, -17825, -18808, -19789, -20768, -21745,
+	-22720, -23693, -24663, -25631, -26596, -27559, -28519, -29476, -30430,
+	-31381, -32329, -33274, -34215, -35153, -36087, -37018, -37945, -38868,
+	-39788, -40703, -41615, -42522, -43425, -44323, -45218, -46107, -46992,
+	-47873, -48748, -49619, -50485, -51345, -52201, -53051, -53896, -54736,
+	-55570, -56399, -57221, -58039, -58850, -59656, -60455, -61249, -62036,
+	-62817, -63592, -64361, -65123, -65879, -66628, -67370, -68106, -68834,
+	-69556, -70271, -70979, -71680, -72374, -73060, -73739, -74411, -75075,
+	-75732, -76382, -77023, -77657, -78283, -78901, -79512, -80114, -80709,
+	-81295, -81873, -82444, -83005, -83559, -84104, -84641, -85169, -85689,
+	-86200, -86703, -87197, -87682, -88158, -88626, -89085, -89534, -89975,
+	-90407, -90830, -91244, -91648, -92044, -92430, -92807, -93175, -93533,
+	-93883, -94222, -94553, -94873, -95185, -95486, -95779, -96061, -96334,
+	-96598, -96852, -97096, -97330, -97555, -97770, -97975, -98170, -98356,
+	-98531, -98697, -98853, -98999, -99135, -99262, -99378, -99484, -99581,
+	-99667, -99744, -99810, -99867, -99914, -99950, -99977, -99993, -100000
+};
+
+/* icos() - look up in cos_table for a rad value.
+ * @rad : the rad value.* 10^7
+ *
+ * Return : the cosine value * 100000
+ */
+static int icos(__s64 rad)
+{
+	size_t idx;
+
+	if (rad < 0)
+		rad = -rad;
+
+	if (rad >= 2 * PI)
+		rad %= (2 * PI);
+
+	if (rad > PI)
+		rad = 2 * PI - rad;
+
+	idx = rad / 100000;
+	if (idx >= ARRAY_SIZE(cos_table))
+		idx = ARRAY_SIZE(cos_table) - 1;
+
+	return cos_table[idx];
+}
+
+/* degree_to_rad() - convert a degree value to a rad value.
+ * @a : the degree value as 1/10 micro degree (10^7).
+ *
+ * Return : the rad value * 10^7.
+ */
+static __s64 degree_to_rad(__s64 a)
+{
+	return (((RAD_PER_DEGREE * a) / 10000000ULL)) % (PI * 2ULL);
+}
+
+/* diff() - calculate the difference beween a and b.
+ * @a : value a.
+ * @b : value b.
+ *
+ * Return : the difference
+ */
+static __s32 diff(__s32 a, __s32 b)
+{
+	__s32 r = a - b;
+
+	return r < 0 ? r * -1 : r;
+}
+
+/* get_distance() - calculate the distance of to position vectors
+ * center/self: positionvectors, whose distance is calculated
+ * @x : after execute includes the meter on X-axes.
+ * @y : after execute includes the meter on Y-axes.
+ *
+ * the calculation based on pythagoras.
+ */
+static struct gn_coord gn_coord_diff(struct gn_coord lhs, struct gn_coord rhs)
+{
+	struct gn_coord c;
+	s32 lat;
+
+	lat = degree_to_rad((lhs.lat + rhs.lat) / 2);
+
+	c.lat = (METER_PER_LON * icos(lat) * diff(rhs.lon, lhs.lon)) /
+		(10000000ULL * 100000ULL);
+	c.lon = (METER_PER_LON * diff(rhs.lat, lhs.lat)) / 10000000ULL;
+	return c;
+}
+
+static inline struct gn_coord pv_to_coord(struct gn_lpv *pv)
+{
+	struct gn_coord c = {
+		.lat = be32_to_cpu(pv->lat),
+		.lon = be32_to_cpu(pv->lon),
+	};
+	return c;
+}
+
+static inline u64 dist(struct gn_coord c)
+{
+	return c.lat * c.lat + c.lon * c.lon;
+}
+
+/* gn_F() - decides if self is inside or at the border of the geographical area.
+ * @center: The position vector of the Package sender.
+ * @self: The own position vector.
+ * @t : The Type of geographical area.
+ * @r : The radius of area. Only use for circle.
+ * @a : The width of area. Only use for rectangel and elipse.
+ * @b : The height of area. Only use for rectangel and elipse.
+ * @angel : ???
+ *
+ * Return:  if the result is > 0 self is inside area.
+			if the result is 0 self is on border of area.
+			otherwise self is outside of area.
+ */
+__s64 gn_F(struct gn_coord self, struct gn_geo_scope scope)
+{
+	struct gn_coord coord_diff = gn_coord_diff(self, scope.coord);
+	s32 a2, b2, x2, y2;
+	s64 result = -1;
+	/* Note: Scope angle rotation for non-circular geographical areas */
+	a2 = scope.a * scope.a;
+	b2 = scope.b * scope.b;
+	x2 = coord_diff.lat * coord_diff.lat;
+	y2 = coord_diff.lon * coord_diff.lon;
+
+	switch (scope.shape) {
+	case GN_SHAPE_CIRCLE:
+		result = a2 - (x2 + y2);
+		break;
+	case GN_SHAPE_RECTANGLE:
+		result = a2 - x2;
+		if (result > (b2 - y2))
+			result = b2 - y2;
+		break;
+	case GN_SHAPE_ELLIPSE:
+		if (scope.a == 0 || scope.b == 0) {
+			pr_debug("internal error: input out of range");
+			break;
+		}
+		result = 1000;
+		result -= (y2 * 1000) / a2;
+		result -= (x2 * 1000) / b2;
+		break;
+	}
+
+	return result;
+}
+
+static int greedy_forward(struct gn_iface *gnif, u8 *addr, struct gn_lpv *depv)
+{
+	struct gn_coord dest_coord = pv_to_coord(depv);
+	u64 min_dist, curr_dist, ego_dist;
+	u8 *found_addr = NULL;
+	struct loc_te *curr;
+	int bkt, rc;
+
+	ego_dist = dist(gn_coord_diff(dest_coord, gnif->pos.coord));
+	min_dist = ego_dist;
+	spin_lock_bh(&gn_loc_t_lock);
+	hash_for_each(gn_loc_t, bkt, curr, hnode) {
+		if (curr->is_neighbour) {
+			curr_dist = dist(gn_coord_diff(dest_coord,
+						       pv_to_coord(&curr->pv)));
+			if (curr_dist < min_dist) {
+				min_dist = curr_dist;
+				found_addr = curr->ll_address;
+			}
+		}
+	}
+	/* Note: Traffic class and store-carry-forward evaluation for next-hop selection */
+	if (found_addr) {
+		ether_addr_copy(addr, found_addr);
+		rc = GN_FORWARD_NEXT_HOP;
+	} else {
+		rc = GN_FORWARD_BROADCAST;
+	}
+	spin_unlock_bh(&gn_loc_t_lock);
+
+	return rc;
+}
+
+int gn_gxc_forward(struct gn_iface *gnif, s64 f, u8 *addr, struct gn_lpv *depv)
+{
+	if (f >= 0) {
+		// Area forwarding
+		switch (GN_AREA_FORWARDING) {
+		case GN_AREA_FORWARDING_SIMPLE:
+			return GN_FORWARD_BROADCAST;
+		default:
+			pr_warn("non-simple area forwarding not implemented");
+			return GN_FORWARD_BROADCAST;
+		}
+	} else {
+		// Non-area forwarding
+		switch (GN_NON_AREA_FORWARDING) {
+		case GN_NON_AREA_FORWARDING_GREEDY:
+			return greedy_forward(gnif, addr, depv);
+		default:
+			return GN_FORWARD_BROADCAST;
+		}
+	}
+}
+
+static void debug_loc_te(void)
+{
+	struct hlist_node *tmp;
+	struct loc_te *entry;
+	int bucket;
+
+	spin_lock_bh(&gn_loc_t_lock);
+	if (hash_empty(gn_loc_t)) {
+		spin_unlock_bh(&gn_loc_t_lock);
+		return;
+	}
+
+	pr_debug("Printing location table\n");
+	hash_for_each_safe(gn_loc_t, bucket, tmp, entry, hnode) {
+		pr_debug(
+			"LOC_TE(%p) tst=%x addr=%llx ll_addr=%llx is_neighbour=%x ls_pending=%x\n",
+			entry, entry->tst_addr, be64_to_cpu(entry->addr),
+			ether_addr_to_u64(entry->ll_address),
+			entry->is_neighbour, entry->ls_pending);
+	}
+	spin_unlock_bh(&gn_loc_t_lock);
+}
+
+static void gn_prune(void)
+{
+	struct hlist_node *tmp;
+	struct loc_te *entry;
+	int bucket;
+
+	spin_lock_bh(&gn_loc_t_lock);
+	hash_for_each_safe(gn_loc_t, bucket, tmp, entry, hnode) {
+		if (!GN_TST_VALID(entry->tst_addr)) {
+			pr_debug("pruning entry addr=%llx\n", entry->addr);
+			skb_queue_purge(&entry->lsb);
+			hash_del(&entry->hnode);
+			kfree(entry);
+		}
+	}
+	spin_unlock_bh(&gn_loc_t_lock);
+}
+
+/* update_location_table() - update location table
+ * @pv: the position vector which indicate an entry.
+ *
+ * Return: 0 on success, negative errno on error, -EALREADY if packet is duplicate.
+ *
+ * Update an entry, which indicated by @spv. If no entry found its will be add a new one.
+ * And all entries will be check with the update function.
+ */
+int gn_update_location_table(struct gn_lpv *pv, bool make_neighbour,
+			     const u8 *ll_address, const __be16 *sn)
+{
+	/* ETSI EN 302 636-4-1 Clause C.2: Update PV only when incoming PV timestamp is newer */
+	struct loc_te *entry;
+	bool found = false;
+
+	if (gn_find_interface(pv->addr))
+		return -EINVAL;
+
+	spin_lock_bh(&gn_loc_t_lock);
+	hash_for_each_possible(gn_loc_t, entry, hnode, pv->addr) {
+		if (entry->addr != pv->addr)
+			continue;
+
+		found = true;
+
+		pr_debug("updating entry addr=%llx\n", pv->addr);
+
+		entry->pdr = pdr(entry->pdr,
+				 jiffies_to_msecs(jiffies) -
+					 jiffies_to_msecs(entry->tst_addr));
+		entry->tst_addr = jiffies;
+		if (make_neighbour) {
+			if (!ll_address) {
+				spin_unlock_bh(&gn_loc_t_lock);
+				return -EINVAL;
+			}
+			ether_addr_copy(entry->ll_address, ll_address);
+			entry->is_neighbour = true;
+		}
+		memcpy(&entry->pv, pv, sizeof(*pv));
+		if (sn) {
+			// Perform DPD
+			if (gn_dpd_find(&entry->dpl, be16_to_cpu(*sn))) {
+				pr_debug("received duplicate packet\n");
+				spin_unlock_bh(&gn_loc_t_lock);
+				return -EALREADY;
+			}
+			gn_dpd_insert(&entry->dpl, be16_to_cpu(*sn));
+		}
+		break;
+	}
+
+	if (!found) {
+		entry = kzalloc_obj(*entry, GFP_ATOMIC);
+		if (!entry) {
+			spin_unlock_bh(&gn_loc_t_lock);
+			return -ENOMEM;
+		}
+		pr_debug("adding entry addr=%llx\n", pv->addr);
+		entry->addr = pv->addr;
+		entry->tst_addr = jiffies;
+		entry->is_neighbour = make_neighbour;
+		memcpy(&entry->pv, pv, sizeof(*pv));
+		skb_queue_head_init(&entry->lsb);
+		if (make_neighbour) {
+			if (!ll_address) {
+				kfree(entry);
+				spin_unlock_bh(&gn_loc_t_lock);
+				return -EINVAL;
+			}
+			ether_addr_copy(entry->ll_address, ll_address);
+		}
+		if (sn)
+			gn_dpd_insert(&entry->dpl, be16_to_cpu(*sn));
+
+		hash_add(gn_loc_t, &entry->hnode, entry->addr);
+	}
+	spin_unlock_bh(&gn_loc_t_lock);
+
+	gn_prune();
+
+	debug_loc_te();
+
+	return 0;
+}
+
+static void __ls_queue(struct sk_buff_head *q, struct sk_buff *skb)
+{
+	u32 qlen = skb_queue_len(q);
+	struct sk_buff *curr;
+
+	while (qlen-- > GN_LSB_SIZE) {
+		curr = skb_dequeue(q);
+		if (curr)
+			kfree_skb(curr);
+	}
+
+	skb_queue_tail(q, skb);
+}
+
+/* ls_queue() - queue packet for delivery if destination is not a neighbour
+ * @dest_addr: destination address
+ * @skb: the packet
+ *
+ * If the destination address is not a known neighbour ẃith recent activity,
+ * queue the packet
+ *
+ * Return: GN_QUEUE_DIRECT if the destination is a neighbour,
+ * GN_QUEUE_LS_PENDING if the destination is unknown, but a LS request is
+ * pending and the packet is queued,
+ * GN_QUEUE_LS_STALE if the destination is unknown or its entry is stale and
+ * we should send a LS query, GN_QUEUE_ERROR on error
+ */
+int gn_ls_queue(gn_address_t dest_addr, struct sk_buff *skb)
+{
+	struct loc_te *entry;
+	int rc = -ENOENT;
+
+	spin_lock_bh(&gn_loc_t_lock);
+	hash_for_each_possible(gn_loc_t, entry, hnode, dest_addr) {
+		if (entry->addr != dest_addr)
+			continue;
+
+		if (GN_TST_VALID(entry->tst_addr)) {
+			// Entry is valid, no need for LS
+			rc = GN_QUEUE_DIRECT;
+		} else if (entry->ls_pending == 1) {
+			// Entry is stale, but we already sent a LS request
+			rc = GN_QUEUE_LS_PENDING;
+			__ls_queue(&entry->lsb, skb);
+		} else {
+			// Entry is stale, perform LS request
+			rc = GN_QUEUE_LS_STALE;
+
+			entry->ls_pending = 1;
+			__ls_queue(&entry->lsb, skb);
+		}
+
+		break;
+	}
+	if (rc == -ENOENT) {
+		entry = kzalloc_obj(*entry, GFP_ATOMIC);
+		if (!entry) {
+			spin_unlock_bh(&gn_loc_t_lock);
+			return -ENOMEM;
+		}
+
+		rc = GN_QUEUE_LS_STALE;
+		entry->addr = dest_addr;
+		entry->ls_pending = 1;
+		skb_queue_head_init(&entry->lsb);
+		__ls_queue(&entry->lsb, skb);
+
+		hash_add(gn_loc_t, &entry->hnode, entry->addr);
+	}
+	spin_unlock_bh(&gn_loc_t_lock);
+
+	return rc;
+}
+
+void gn_ls_flush(gn_address_t dest_addr)
+{
+	struct sk_buff *tmp_skb;
+	u8 ll_address[ETH_ALEN];
+	struct loc_te *entry;
+	bool has_mac = false;
+
+	spin_lock_bh(&gn_loc_t_lock);
+	hash_for_each_possible(gn_loc_t, entry, hnode, dest_addr) {
+		if (entry->addr != dest_addr)
+			continue;
+		if (!entry->ls_pending)
+			break;
+
+		if (entry->is_neighbour &&
+		    !is_zero_ether_addr(entry->ll_address)) {
+			ether_addr_copy(ll_address, entry->ll_address);
+			has_mac = true;
+		}
+
+		while ((tmp_skb = skb_dequeue(&entry->lsb)) != NULL) {
+			struct gn_header *gh =
+				(struct gn_header *)skb_network_header(tmp_skb);
+			struct gn_iface *gnif =
+				gn_find_interface_by_dev(tmp_skb->dev);
+
+			/* Populate DEPV for queued GeoUnicast packets when location is
+			   resolved */
+			if (gh->gc_h.ht == CH_HT_GUC) {
+				gh->guc_h.depv.tst = entry->pv.tst;
+				gh->guc_h.depv.lat = entry->pv.lat;
+				gh->guc_h.depv.lon = entry->pv.lon;
+			}
+
+			if (has_mac)
+				gn_dl->request(gn_dl, tmp_skb, ll_address);
+			else if (gnif && !gn_query_ll_nexthop(gnif, dest_addr,
+							      ll_address))
+				gn_dl->request(gn_dl, tmp_skb, ll_address);
+			else
+				gn_dl->request(gn_dl, tmp_skb,
+					       tmp_skb->dev->broadcast);
+		}
+		entry->ls_pending = 0;
+		break;
+	}
+	spin_unlock_bh(&gn_loc_t_lock);
+}
+
+int gn_query_ll_address(gn_address_t query_addr, u8 *ll_address)
+{
+	struct loc_te *entry;
+	int rc = -ENOENT;
+
+	spin_lock_bh(&gn_loc_t_lock);
+	hash_for_each_possible(gn_loc_t, entry, hnode, query_addr) {
+		if (entry->addr != query_addr)
+			continue;
+		if (!entry->is_neighbour || !GN_TST_VALID(entry->tst_addr))
+			break;
+		if (is_zero_ether_addr(entry->ll_address))
+			break;
+
+		rc = 0;
+		ether_addr_copy(ll_address, entry->ll_address);
+		break;
+	}
+	spin_unlock_bh(&gn_loc_t_lock);
+
+	return rc;
+}
+
+/**
+ * gn_query_ll_nexthop - Query linklayer address or next-hop for GUC forwarding
+ * @gnif: Local GeoNetworking interface sending the packet
+ * @query_addr: Destination GeoNetworking address
+ * @ll_address: Buffer to receive the linklayer (MAC) address
+ *
+ * If query_addr is a direct 1-hop neighbor, resolves directly to its MAC
+ * address.
+ * If query_addr is a multi-hop destination in LocTE, runs greedy forwarding
+ * to select the best next-hop neighbor toward the destination.
+ *
+ * Return: 0 if linklayer address resolved, negative errno if broadcast needed.
+ */
+int gn_query_ll_nexthop(struct gn_iface *gnif, gn_address_t query_addr,
+			u8 *ll_address)
+{
+	bool is_neighbor = false;
+	struct gn_lpv target_pv;
+	struct loc_te *entry;
+	bool found = false;
+
+	spin_lock_bh(&gn_loc_t_lock);
+	hash_for_each_possible(gn_loc_t, entry, hnode, query_addr) {
+		if (entry->addr != query_addr)
+			continue;
+		if (!GN_TST_VALID(entry->tst_addr))
+			break;
+		if (entry->is_neighbour &&
+		    !is_zero_ether_addr(entry->ll_address)) {
+			ether_addr_copy(ll_address, entry->ll_address);
+			is_neighbor = true;
+		} else {
+			target_pv = entry->pv;
+		}
+		found = true;
+		break;
+	}
+	spin_unlock_bh(&gn_loc_t_lock);
+
+	if (!found)
+		return -ENOENT;
+	if (is_neighbor)
+		return 0;
+
+	return (greedy_forward(gnif, ll_address, &target_pv) ==
+		GN_FORWARD_NEXT_HOP) ?
+		       0 :
+		       -EHOSTUNREACH;
+}
+
+/**
+ * gn_fill_depv - Populate Destination Position Vector (DEPV) from Location Table
+ * @depv: Pointer to gn_spv struct to populate
+ * @dest_addr: GeoNetworking address of the destination
+ *
+ * Return: 0 if valid destination position found in LocTE,
+ * negative error code otherwise.
+ */
+int gn_fill_depv(struct gn_spv *depv, gn_address_t dest_addr)
+{
+	struct loc_te *entry;
+	int rc = -ENOENT;
+
+	spin_lock_bh(&gn_loc_t_lock);
+	hash_for_each_possible(gn_loc_t, entry, hnode, dest_addr) {
+		if (entry->addr != dest_addr)
+			continue;
+		if (GN_TST_VALID(entry->tst_addr)) {
+			depv->addr = entry->pv.addr;
+			depv->tst = entry->pv.tst;
+			depv->lat = entry->pv.lat;
+			depv->lon = entry->pv.lon;
+			rc = 0;
+		}
+		break;
+	}
+	spin_unlock_bh(&gn_loc_t_lock);
+
+	return rc;
+}
+
+void gn_routing_exit(void)
+{
+	struct hlist_node *tmp;
+	struct loc_te *entry;
+	int bucket;
+
+	spin_lock_bh(&gn_loc_t_lock);
+	hash_for_each_safe(gn_loc_t, bucket, tmp, entry, hnode) {
+		skb_queue_purge(&entry->lsb);
+		hash_del(&entry->hnode);
+		kfree(entry);
+	}
+	spin_unlock_bh(&gn_loc_t_lock);
+}
diff --git a/net/gn/sysctl_net_gn.c b/net/gn/sysctl_net_gn.c
new file mode 100644
index 000000000000..12bb2cb64135
--- /dev/null
+++ b/net/gn/sysctl_net_gn.c
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * sysctl_net_gn.c: sysctl interface to net GeoNetworking subsystem
+ */
+
+#include <linux/sysctl.h>
+#include <net/sock.h>
+#include <linux/gn.h>
+
+static int dummy_var = 1234;
+
+static struct ctl_table gn_table[] = { {
+	.procname = "gn-dummy-var",
+	.data = &dummy_var,
+	.maxlen = sizeof(int),
+	.mode = 0644,
+	.proc_handler = proc_dointvec,
+} };
+
+static struct ctl_table_header *gn_table_header;
+
+int __init gn_register_sysctl(void)
+{
+	gn_table_header = register_net_sysctl(&init_net, "net/gn", gn_table);
+	if (!gn_table_header)
+		return -ENOMEM;
+	return 0;
+}
+
+void gn_unregister_sysctl(void)
+{
+	unregister_net_sysctl_table(gn_table_header);
+}
diff --git a/security/selinux/hooks.c b/security/selinux/hooks.c
index 1a713d96206f..38876505cec3 100644
--- a/security/selinux/hooks.c
+++ b/security/selinux/hooks.c
@@ -1318,7 +1318,10 @@ static inline u16 socket_type_to_security_class(int family, int type, int protoc
 			return SECCLASS_XDP_SOCKET;
 		case PF_MCTP:
 			return SECCLASS_MCTP_SOCKET;
-#if PF_MAX > 46
+		case PF_GN:
+			return SECCLASS_GN_SOCKET;
+
+#if PF_MAX > 47
 #error New address family defined, please update this function.
 #endif
 		}
diff --git a/security/selinux/include/classmap.h b/security/selinux/include/classmap.h
index 90cb61b16425..dfa07a586060 100644
--- a/security/selinux/include/classmap.h
+++ b/security/selinux/include/classmap.h
@@ -174,6 +174,7 @@ const struct security_class_mapping secclass_map[] = {
 	    "map_create_as", "prog_load_as", NULL } },
 	{ "xdp_socket", { COMMON_SOCK_PERMS, NULL } },
 	{ "mctp_socket", { COMMON_SOCK_PERMS, NULL } },
+	{ "gn_socket",	{ COMMON_SOCK_PERMS, NULL } },
 	{ "perf_event",
 	  { "open", "cpu", "kernel", "tracepoint", "read", "write", NULL } },
 	{ "anon_inode", { COMMON_FILE_PERMS, NULL } },
@@ -187,7 +188,7 @@ const struct security_class_mapping secclass_map[] = {
 #ifdef __KERNEL__ /* avoid this check when building host programs */
 #include <linux/socket.h>
 
-#if PF_MAX > 46
+#if PF_MAX > 47
 #error New address family defined, please update secclass_map.
 #endif
 #endif
-- 
2.55.0


^ permalink raw reply related

* [RFC PATCH net-next v0.1 0/1] add GeoNetworking protocol
From: Simon Dietz @ 2026-07-18 21:00 UTC (permalink / raw)
  To: netdev
  Cc: andrew+netdev, davem, edumazet, johannes, kuniyu, linux-wireless,
	simon.dietz, dietz23838

Implement the GeoNetworking / ETSI ITS-G5 ('net/gn') protocol which
is based on 802.11p wifi and used for vehicle2x applications. It is
standardized by the ETSI and used by some car manufacturers
(especially in europe). It enables ad-hoc, multi-hop geographical
communication and routing among vehicles (and road- or railside
infrastructure).

Most work of this implementation has been done by the bachelor
project 2018/2019 of the operating systems and middleware group of
the Hasso Plattner Institute, University of Potsdam, which the author
was part of.

The code [1] is published under the GPL v2 at this location:
https://gitlab.com/hpi-potsdam/osm/g5-on-linux/linux-geonetworking/

This patch set is not merge-ready, yet. E.g. network namespaces are
not supported at all, documentation is missing, ...

Purpose of this RFC is to determine if GeoNetworking support in the
linux kernel is desirable and if so what steps would have to be done
next.

In the original implementation new ioctl calls have been used. For the
removal of them and changing their functionality to use netlink (and
only this change) help of AI has been used as stated in the commit.

Changes since v0:
* replace ioctl interface changes with netlink
* remove profcs
* remove magic value in unix/ETSI epoch conversion
* add _HLEN macros
* remove inline from static functions
* remove commented out code
* change default log level to debug
* adhere to line limit of 80

Changes in v0:
* rebase to current-net next
* fix (most) of coding style violations
* fix SPDX-License Identifiers
* use kzalloc_obj instead of kzalloc
* change __attribute__((packed)) to __packed
* (partially?) fix locking
* adhere to kernel error code conventions
* add various bounds, 0, NULL, ... checks
* use has_for_each_safe
* refactor forward_packet helper functions
* document -EEXIST return on existing dev
* add gn_fill_depv and ls_flush logic
* add nexthop query logic
* replace timespec64 with uapi conform __kernel_timespec
* fix default hop limit

Detailed explaination of the changes:

net: gn: remove procfs

Remove any procfs usage for debugging purposes. Procfs has been used
during the development of the gn module for passing gps data from the
user space to the kernel. This behaviour has been changed to ioctl.
Procfs has only been kept for debugging reasons and is removed hereby.

net: gn: remove magic value in unix/ETSI epoc conversion

ETSI uses a different epoch than the unix default 1970-01-01 00:00:00.
For epoch conversion a magic value has been used, which now is replaced
by a constant.

net: gn: add _HLEN macros

Add _HLEN macros and use them to shorten code lines.

net: gn: remove inline from static functions
net: gn: remove commented out code
net: gn: change default log level to debug
net: gn: enforce line lenght limit of 80

For an explaination of changes in v0 compared to the original code base
[1] consult the patch series v0.

Greetings,
Simon

PS: The commit message date is off, because I squashed the commits.

Simon Dietz (1):
  net: add GeoNetworking protocol

 include/linux/gn.h                  |  363 +++++
 include/linux/gn_routing.h          |   68 +
 include/linux/socket.h              |    6 +-
 include/uapi/linux/gn.h             |   84 ++
 include/uapi/linux/if_ether.h       |    1 +
 include/uapi/linux/if_link.h        |   10 +
 net/Kconfig                         |    2 +
 net/Makefile                        |    1 +
 net/gn/Kconfig                      |    4 +
 net/gn/Makefile                     |    7 +
 net/gn/gn_netlink.c                 |  266 ++++
 net/gn/gn_prot.c                    | 1908 +++++++++++++++++++++++++++
 net/gn/gn_routing.c                 |  646 +++++++++
 net/gn/sysctl_net_gn.c              |   33 +
 security/selinux/hooks.c            |    5 +-
 security/selinux/include/classmap.h |    3 +-
 16 files changed, 3403 insertions(+), 4 deletions(-)
 create mode 100644 include/linux/gn.h
 create mode 100644 include/linux/gn_routing.h
 create mode 100644 include/uapi/linux/gn.h
 create mode 100644 net/gn/Kconfig
 create mode 100644 net/gn/Makefile
 create mode 100644 net/gn/gn_netlink.c
 create mode 100644 net/gn/gn_prot.c
 create mode 100644 net/gn/gn_routing.c
 create mode 100644 net/gn/sysctl_net_gn.c

-- 
2.55.0


^ permalink raw reply

* Re: [RFC PATCH v1 0/9] wifi: rtw88: add RTL8723B/RTL8723BS support
From: Peter Robinson @ 2026-07-18 17:38 UTC (permalink / raw)
  To: luka.gejak, linux-wireless; +Cc: pkshih, straube.linux

Hi Luka,

> RTL8723BS predates rtw88 and uses the available Realtek vendor firmware
> (version 41), whose H2C/C2H and association behavior differs from the
> firmware used by the existing rtw88 8723-family devices. Supporting it
> therefore needs both the common RTL8723B chip implementation and an
> 8723BS-specific SDIO path for firmware commands, coexistence, power,
> association, and TX/RX handling.

Over all the driver works quite well on the Pine64+ and Pine64-LTS
against a 7.1.3 kernel that I tested it on. I get the following errors
in dmesg, they don't appear to be detrimental to the operation with
some basic testing:

rtw88_8723bs mmc0:0001:1: Firmware version 41.0.0, H2C version 0
rtw88_8723bs mmc0:0001:1: firmware failed to leave lps state
rtw88_8723bs mmc0:0001:1: failed to get tx report from firmware
rtw88_8723bs mmc0:0001:1: failed to get tx report from firmware
rtw88_8723bs mmc0:0001:1: failed to get tx report from firmware
rtw88_8723bs mmc0:0001:1: failed to get tx report from firmware
rtw88_8723bs mmc0:0001:1: failed to get tx report from firmware
rtw88_8723bs mmc0:0001:1: failed to get tx report from firmware
rtw88_8723bs mmc0:0001:1: firmware failed to leave lps state
rtw88_8723bs mmc0:0001:1: failed to get tx report from firmware
rtw88_8723bs mmc0:0001:1: firmware failed to leave lps state
rtw88_8723bs mmc0:0001:1: firmware failed to leave lps state
rtw88_8723bs mmc0:0001:1: firmware failed to leave lps state
rtw88_8723bs mmc0:0001:1: failed to get tx report from firmware

Overall for the series you can add:
Tested-by: Peter Robinson <pbrobinson@gmail.com>
Pine64+ / Pine64-LTS on Fedora 44 with a 7.1.x kernel

> The driver requests this firmware file:
>
>  rtw88/rtw8723b_fw.bin
>
> It is not included in this kernel series. I would appreciate guidance
> from Realtek and the linux-firmware maintainers on whether the existing
> LICENCE.rtlwifi_firmware.txt redistribution terms cover this version 41
> blob, or whether Realtek should submit the firmware directly.

There's precedent for including the realtek firmware in linux-firmware
like this, you should include in the notes where you got the firmware
from and how it was extracted so it's verifiable, as well as cc:ing
Ping-Ke Shih who will then ACK/review it. That's been done for a
number of the realtek firmware that were extracted from official
realtek windows drivers.

I am happy to see this, I had started to play around with a similar
patchset but still had a way to go and little time to move it on.

Cheers,
Peter

^ permalink raw reply

* Re: [regression] rtw89_8852be: silent RX stalls since v7.1; reverting bda294ed0ed0 appears to fix it
From: Armin Breiteneder @ 2026-07-18 14:56 UTC (permalink / raw)
  To: Bitterblue Smith; +Cc: Ping-Ke Shih, o-Hao Huang, linux-wireless, linux-kernel
In-Reply-To: <4d8ac018-ce77-4e30-9596-bbc28d18c18f@gmail.com>

Perfect!
Thank you for the fast response!


Am 18.07.2026 um 13:55 schrieb Bitterblue Smith <rtl8821cerfe2@gmail.com>:

On 18/07/2026 01:48, Armin Breiteneder wrote:
> Hi,
> 
> I am hitting a reproducible WiFi regression with rtw89_8852be (RTL8852BE, 10ec:b852, CV:1 RFE:1, firmware 0.29.29.18) on Fedora 44.
> 
> Good: 7.0.14-201.fc44
> Bad:  7.1.3-200.fc44, and v7.1 vanilla
> 
> Reverting bda294ed0ed0 ("wifi: rtw89: Drop malformed AMPDU frames with abnormal PN") on top of v7.1 appears to fix it -- the stalls are gone.
> 
> Symptom: every few minutes the RX data path stalls silently for 1-7 minutes at a time. The radio link stays perfectly healthy -- still associated, signal unchanged (~-62 dBm), "tx failed" and "tx retries" both 0 (the AP ACKs every frame we send), "beacon loss" 0 -- and the station's rx_packets counter keeps climbing at ~23 frames/s. Yet almost nothing is delivered to the network stack. The kernel logs nothing at all. Then it recovers on its own, without re-association, and the whole backlog is delivered in one burst.
> 
> Quantified over one 54-second stall (mac80211 station counter vs. the napi_gro_receive tracepoint):
>   frames received and counted from the AP : ~1230
>   frames delivered to the network stack   :  ~211
>   frames withheld                         : ~1000
>   delivered in one interval on recovery   : +5070
> 
> Everything that does get through during the stall is broadcast/multicast only (ARP, IPv6 multicast, vendor ethertypes) -- exactly the traffic that bypasses Block-Ack reordering.
> 
> That fits the suspected commit: dropping a frame inside an A-MPDU leaves a hole in the sequence numbers, so the RX reorder buffer waits for something that never arrives, everything behind it piles up, and it is only released once a timeout fires. I have not verified this in the code beyond reading the commit, so please treat the mechanism as a hypothesis -- the revert result is the solid part.
> 
> The AP is a FRITZ!Box 4040 on channel 36 (5180 MHz, VHT80), non-DFS.
> 
> Already ruled out:
> - Powersave: disabled (NetworkManager wifi.powersave=2, confirmed via iw).
> - ASPM: disable_clkreq=y disable_aspm_l1=y disable_aspm_l1ss=y disable_ps_mode=y
>   all set and confirmed active. The stall still occurs -- note this is the
>   workaround that reportedly fixes the CachyOS case below; it does not help here.
> - Signal / range / DFS: signal constant across the stall, channel is non-DFS.
> - TX path: txf=0 and txr=0 throughout.
> - Stable patches: "git log v7.1..v7.1.3 -- net/mac80211/ drivers/net/wireless/
>   realtek/rtw89/" is empty, so this came in with the v7.1 merge window.
> 
> I could not find a way to trigger the stall on demand, so I could not bisect; the suspect was found by reading the v7.0..v7.1 commits touching the RX/AMPDU path and running a targeted revert test.
> 
> A regression with the same version boundary was reported against the CachyOS kernel: https://github.com/CachyOS/linux-cachyos/issues/921
> 
> Fedora bug: https://bugzilla.redhat.com/show_bug.cgi?id=2501974
> 
> Happy to run a debug build or test patches -- I have the affected hardware and the failure shows up within minutes of normal use, so turnaround is fast.
> 
> #regzbot introduced: bda294ed0ed0
> 
> Thanks,
> Armin
> 
> 

Hi!

This commit from 7.2 should fix it: https://github.com/torvalds/linux/commit/63ccdfac8677387dfdbd9d4336089e9823280704

It's queued for 7.1.x as well.

^ permalink raw reply

* [PATCH] wifi: rtw88: disable ASPM and deep PS on ASUS TUF Gaming A15 FA506II
From: Mihail Dimoski @ 2026-07-18 12:40 UTC (permalink / raw)
  To: Ping-Ke Shih; +Cc: linux-wireless, Mihail Dimoski

The RTL8822CE on the ASUS TUF Gaming A15 FA506II wedges during normal
use. The driver watchdog toggles PCIe ASPM while leaving power save;
the DBI read of the ASPM link-config register fails with -EIO, the PCIe
link becomes unstable, and the device drops off the bus, taking Wi-Fi
down until a cold power cycle:

  rtw88_8822ce 0000:03:00.0: failed to read ASPM, ret=-5
  rtw88_8822ce 0000:03:00.0: firmware failed to leave lps state
  rtw88_8822ce 0000:03:00.0: mac power on failed

This is the same platform ASPM inter-operability problem already
handled for other machines through rtw_pci_quirks[]. Disabling PCI
ASPM and deep power save on this model stops the failure. Add a DMI
quirk so the workaround is applied automatically.

Signed-off-by: Mihail Dimoski <mihaildimoski@gmail.com>
---
 drivers/net/wireless/realtek/rtw88/pci.c | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/drivers/net/wireless/realtek/rtw88/pci.c b/drivers/net/wireless/realtek/rtw88/pci.c
index a304672..2c4f123 100644
--- a/drivers/net/wireless/realtek/rtw88/pci.c
+++ b/drivers/net/wireless/realtek/rtw88/pci.c
@@ -1779,6 +1779,16 @@ static const struct dmi_system_id rtw_pci_quirks[] = {
 		.driver_data = (void *)(BIT(QUIRK_DIS_CAP_PCI_ASPM) |
 					BIT(QUIRK_DIS_CAP_LPS_DEEP)),
 	},
+	{
+		.callback = rtw_pci_disable_caps,
+		.ident = "ASUS TUF Gaming A15 FA506II",
+		.matches = {
+			DMI_MATCH(DMI_SYS_VENDOR, "ASUSTeK COMPUTER INC."),
+			DMI_MATCH(DMI_BOARD_NAME, "FA506II"),
+		},
+		.driver_data = (void *)(BIT(QUIRK_DIS_CAP_PCI_ASPM) |
+					BIT(QUIRK_DIS_CAP_LPS_DEEP)),
+	},
 	{}
 };
 
-- 
2.53.0


^ permalink raw reply related

* Re: [regression] rtw89_8852be: silent RX stalls since v7.1; reverting bda294ed0ed0 appears to fix it
From: Bitterblue Smith @ 2026-07-18 11:55 UTC (permalink / raw)
  To: Armin Breiteneder, Ping-Ke Shih; +Cc: o-Hao Huang, linux-wireless, linux-kernel
In-Reply-To: <0f6d921f-1026-4d9a-bad5-7e5b9485c4a2@breiteneder.me>

On 18/07/2026 01:48, Armin Breiteneder wrote:
> Hi,
> 
> I am hitting a reproducible WiFi regression with rtw89_8852be (RTL8852BE, 10ec:b852, CV:1 RFE:1, firmware 0.29.29.18) on Fedora 44.
> 
> Good: 7.0.14-201.fc44
> Bad:  7.1.3-200.fc44, and v7.1 vanilla
> 
> Reverting bda294ed0ed0 ("wifi: rtw89: Drop malformed AMPDU frames with abnormal PN") on top of v7.1 appears to fix it -- the stalls are gone.
> 
> Symptom: every few minutes the RX data path stalls silently for 1-7 minutes at a time. The radio link stays perfectly healthy -- still associated, signal unchanged (~-62 dBm), "tx failed" and "tx retries" both 0 (the AP ACKs every frame we send), "beacon loss" 0 -- and the station's rx_packets counter keeps climbing at ~23 frames/s. Yet almost nothing is delivered to the network stack. The kernel logs nothing at all. Then it recovers on its own, without re-association, and the whole backlog is delivered in one burst.
> 
> Quantified over one 54-second stall (mac80211 station counter vs. the napi_gro_receive tracepoint):
>   frames received and counted from the AP : ~1230
>   frames delivered to the network stack   :  ~211
>   frames withheld                         : ~1000
>   delivered in one interval on recovery   : +5070
> 
> Everything that does get through during the stall is broadcast/multicast only (ARP, IPv6 multicast, vendor ethertypes) -- exactly the traffic that bypasses Block-Ack reordering.
> 
> That fits the suspected commit: dropping a frame inside an A-MPDU leaves a hole in the sequence numbers, so the RX reorder buffer waits for something that never arrives, everything behind it piles up, and it is only released once a timeout fires. I have not verified this in the code beyond reading the commit, so please treat the mechanism as a hypothesis -- the revert result is the solid part.
> 
> The AP is a FRITZ!Box 4040 on channel 36 (5180 MHz, VHT80), non-DFS.
> 
> Already ruled out:
> - Powersave: disabled (NetworkManager wifi.powersave=2, confirmed via iw).
> - ASPM: disable_clkreq=y disable_aspm_l1=y disable_aspm_l1ss=y disable_ps_mode=y
>   all set and confirmed active. The stall still occurs -- note this is the
>   workaround that reportedly fixes the CachyOS case below; it does not help here.
> - Signal / range / DFS: signal constant across the stall, channel is non-DFS.
> - TX path: txf=0 and txr=0 throughout.
> - Stable patches: "git log v7.1..v7.1.3 -- net/mac80211/ drivers/net/wireless/
>   realtek/rtw89/" is empty, so this came in with the v7.1 merge window.
> 
> I could not find a way to trigger the stall on demand, so I could not bisect; the suspect was found by reading the v7.0..v7.1 commits touching the RX/AMPDU path and running a targeted revert test.
> 
> A regression with the same version boundary was reported against the CachyOS kernel: https://github.com/CachyOS/linux-cachyos/issues/921
> 
> Fedora bug: https://bugzilla.redhat.com/show_bug.cgi?id=2501974
> 
> Happy to run a debug build or test patches -- I have the affected hardware and the failure shows up within minutes of normal use, so turnaround is fast.
> 
> #regzbot introduced: bda294ed0ed0
> 
> Thanks,
> Armin
> 
> 

Hi!

This commit from 7.2 should fix it: https://github.com/torvalds/linux/commit/63ccdfac8677387dfdbd9d4336089e9823280704

It's queued for 7.1.x as well.

^ permalink raw reply

* [PATCH v3 2/2] wifi: ath12k: release QMI handle on late init failures
From: Guangshuo Li @ 2026-07-18  7:40 UTC (permalink / raw)
  To: Jeff Johnson, Pradeep Kumar Chitrapu, Vasanthakumar Thiagarajan,
	Sathishkumar Muruganandam, Anilkumar Kolli, Ganesh Sesetti,
	Baochen Qiang, linux-wireless, ath11k, linux-kernel, ath12k
  Cc: Guangshuo Li
In-Reply-To: <20260718074026.3085688-1-lgs201920130244@gmail.com>

ath12k_qmi_init_service() initializes the QMI handle before allocating
the QMI event workqueue and registering the service lookup.

If either of these later initialization steps fails, the function
returns without releasing the initialized QMI handle. Since the failed
device is skipped by the hardware group rollback path, the handle is
leaked.

Release the QMI handle on the late failure paths.

ath12k_qmi_deinit_service() uses ab->qmi.ab to determine whether QMI
service initialization completed successfully. Set it only after all
initialization steps succeed.

Fixes: 088a099690e4 ("wifi: ath12k: fix error handling in creating hardware group")
Signed-off-by: Guangshuo Li <lgs201920130244@gmail.com>
---
 drivers/net/wireless/ath/ath12k/qmi.c | 13 ++++++++++---
 1 file changed, 10 insertions(+), 3 deletions(-)

diff --git a/drivers/net/wireless/ath/ath12k/qmi.c b/drivers/net/wireless/ath/ath12k/qmi.c
index fd762b5d7bb5..692f1b2c2031 100644
--- a/drivers/net/wireless/ath/ath12k/qmi.c
+++ b/drivers/net/wireless/ath/ath12k/qmi.c
@@ -4041,7 +4041,6 @@ int ath12k_qmi_init_service(struct ath12k_base *ab)
 
 	memset(&ab->qmi.target, 0, sizeof(struct target_info));
 	memset(&ab->qmi.target_mem, 0, sizeof(struct target_mem_chunk));
-	ab->qmi.ab = ab;
 
 	ab->qmi.target_mem_mode = ab->target_mem_mode;
 	ret = qmi_handle_init(&ab->qmi.handle, ATH12K_QMI_RESP_LEN_MAX,
@@ -4054,7 +4053,8 @@ int ath12k_qmi_init_service(struct ath12k_base *ab)
 	ab->qmi.event_wq = alloc_ordered_workqueue("ath12k_qmi_driver_event", 0);
 	if (!ab->qmi.event_wq) {
 		ath12k_err(ab, "failed to allocate workqueue\n");
-		return -EFAULT;
+		ret = -EFAULT;
+		goto err_release_qmi_handle;
 	}
 
 	INIT_LIST_HEAD(&ab->qmi.event_list);
@@ -4067,9 +4067,16 @@ int ath12k_qmi_init_service(struct ath12k_base *ab)
 	if (ret < 0) {
 		ath12k_warn(ab, "failed to add qmi lookup\n");
 		destroy_workqueue(ab->qmi.event_wq);
-		return ret;
+		goto err_release_qmi_handle;
 	}
 
+	ab->qmi.ab = ab;
+
+	return ret;
+
+err_release_qmi_handle:
+	qmi_handle_release(&ab->qmi.handle);
+
 	return ret;
 }
 
-- 
2.43.0


^ permalink raw reply related


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox