Expand linux-kpi wireless and networking scaffolding

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-17 00:03:08 +01:00
parent 7126a76f54
commit e2d2fd138c
25 changed files with 1655 additions and 42 deletions
@@ -1,7 +1,7 @@
#ifndef _LINUX_ATOMIC_H
#define _LINUX_ATOMIC_H
#include <linux/types.h>
#include "types.h"
typedef struct {
volatile int counter;
@@ -26,6 +26,8 @@
#define ERANGE 34
#define ENOSYS 38
#define ENODATA 61
#define ENOSPC 28
#define ENOTCONN 107
#define ENOTSUP 95
#define ETIMEDOUT 110
@@ -7,14 +7,21 @@
#define IEEE80211_NUM_ACS 4
struct ieee80211_channel {
u32 band;
u16 center_freq;
u16 hw_value;
u32 flags;
s8 max_power;
s8 max_reg_power;
s8 max_antenna_gain;
bool beacon_found;
};
struct ieee80211_rate {
u32 flags;
u16 bitrate;
u16 hw_value;
u16 hw_value_short;
};
#endif
@@ -14,8 +14,9 @@ extern int irqs_disabled(void);
static inline int in_interrupt(void) { return irqs_disabled(); }
static inline int in_irq(void) { return irqs_disabled(); }
#define disable_irq_nosync(irq) ((void)(irq))
#define enable_irq(irq) ((void)(irq))
extern void disable_irq(unsigned int irq);
extern void disable_irq_nosync(unsigned int irq);
extern void enable_irq(unsigned int irq);
#define IRQF_NO_SUSPEND 0x0000U
#define IRQF_FORCE_RESUME 0x0000U
@@ -1,8 +1,8 @@
#ifndef _LINUX_KERNEL_H
#define _LINUX_KERNEL_H
#include <linux/compiler.h>
#include <linux/types.h>
#include "compiler.h"
#include "types.h"
#include <stddef.h>
#include <stdio.h>
#include <unistd.h>
@@ -20,6 +20,13 @@ struct net_device {
size_t __priv_alloc_align;
};
struct napi_struct {
int (*poll)(struct napi_struct *napi, int budget);
struct net_device *dev;
int state;
int weight;
};
extern struct net_device *alloc_netdev_mqs(size_t sizeof_priv,
const char *name,
unsigned char name_assign_type,
@@ -32,5 +39,13 @@ extern void unregister_netdev(struct net_device *dev);
extern void netif_carrier_on(struct net_device *dev);
extern void netif_carrier_off(struct net_device *dev);
extern int netif_carrier_ok(const struct net_device *dev);
extern void netif_napi_add(struct net_device *dev, struct napi_struct *napi,
int (*poll)(struct napi_struct *napi, int budget), int weight);
extern void napi_schedule(struct napi_struct *napi);
extern int napi_complete_done(struct napi_struct *napi, int work_done);
extern void netif_tx_wake_queue(struct net_device *dev, u16 queue_idx);
extern void netif_tx_stop_queue(struct net_device *dev, u16 queue_idx);
extern void netif_device_attach(struct net_device *dev);
extern void netif_device_detach(struct net_device *dev);
#endif
@@ -9,6 +9,12 @@ enum nl80211_iftype {
NL80211_IFTYPE_MONITOR = 6,
};
enum nl80211_band {
NL80211_BAND_2GHZ = 0,
NL80211_BAND_5GHZ = 1,
NL80211_BAND_6GHZ = 2,
};
enum nl80211_commands {
NL80211_CMD_UNSPEC = 0,
NL80211_CMD_TRIGGER_SCAN = 33,
@@ -23,7 +23,7 @@ struct pci_device_id {
u32 device;
u32 subvendor;
u32 subdevice;
u32 class;
u32 class_code;
u32 class_mask;
unsigned long driver_data;
};
@@ -52,6 +52,12 @@ struct pci_driver {
void (*shutdown)(struct pci_dev *dev);
};
struct msix_entry {
u32 vector;
u16 entry;
u16 _pad;
};
extern int pci_enable_device(struct pci_dev *dev);
extern void pci_disable_device(struct pci_dev *dev);
extern void pci_set_master(struct pci_dev *dev);
@@ -60,6 +66,8 @@ extern void pci_free_irq_vectors(struct pci_dev *dev);
extern int pci_irq_vector(struct pci_dev *dev, unsigned int nr);
extern int pci_enable_msi(struct pci_dev *dev);
extern void pci_disable_msi(struct pci_dev *dev);
extern int pci_enable_msix_range(struct pci_dev *dev, struct msix_entry *entries, int minvec, int maxvec);
extern void pci_disable_msix(struct pci_dev *dev);
extern void *pci_iomap(struct pci_dev *dev, unsigned int bar, size_t max_len);
extern void pci_iounmap(struct pci_dev *dev, void *addr, size_t size);
@@ -70,6 +78,20 @@ extern int pci_write_config_dword(struct pci_dev *dev, unsigned int offset, u32
extern u64 pci_resource_start(struct pci_dev *dev, unsigned int bar);
extern u64 pci_resource_len(struct pci_dev *dev, unsigned int bar);
extern u64 pci_get_quirk_flags(struct pci_dev *dev);
extern bool pci_has_quirk(struct pci_dev *dev, u64 flag);
#define PCI_QUIRK_NO_MSI (1ULL << 0)
#define PCI_QUIRK_NO_MSIX (1ULL << 1)
#define PCI_QUIRK_FORCE_LEGACY (1ULL << 2)
#define PCI_QUIRK_NO_PM (1ULL << 3)
#define PCI_QUIRK_NO_D3COLD (1ULL << 4)
#define PCI_QUIRK_NO_ASPM (1ULL << 5)
#define PCI_QUIRK_NEED_IOMMU (1ULL << 6)
#define PCI_QUIRK_DMA_32BIT_ONLY (1ULL << 8)
#define PCI_QUIRK_NEED_FIRMWARE (1ULL << 11)
#define PCI_QUIRK_DISABLE_ACCEL (1ULL << 12)
extern int pci_register_driver(struct pci_driver *drv);
extern void pci_unregister_driver(struct pci_driver *drv);
@@ -17,6 +17,7 @@ struct sk_buff_head {
struct sk_buff *next;
struct sk_buff *prev;
u32 qlen;
unsigned char lock;
};
extern struct sk_buff *alloc_skb(unsigned int size, gfp_t gfp_mask);
@@ -32,10 +33,14 @@ extern void skb_queue_head_init(struct sk_buff_head *list);
extern void skb_queue_tail(struct sk_buff_head *list, struct sk_buff *newsk);
extern struct sk_buff *skb_dequeue(struct sk_buff_head *list);
extern void skb_queue_purge(struct sk_buff_head *list);
extern struct sk_buff *skb_peek(const struct sk_buff_head *list);
extern u32 skb_queue_len(const struct sk_buff_head *list);
extern int skb_queue_empty(const struct sk_buff_head *list);
extern struct sk_buff *__netdev_alloc_skb(struct net_device *dev, u32 length, gfp_t gfp_mask);
#define dev_alloc_skb(length) __netdev_alloc_skb(NULL, length, GFP_KERNEL)
extern struct sk_buff *skb_copy(const struct sk_buff *src, gfp_t gfp);
extern struct sk_buff *skb_clone(const struct sk_buff *skb, gfp_t gfp);
extern void skb_set_network_header(struct sk_buff *skb, int offset);
extern void skb_reset_mac_header(struct sk_buff *skb);
#endif
@@ -1,7 +1,7 @@
#ifndef _LINUX_SLAB_H
#define _LINUX_SLAB_H
#include <linux/types.h>
#include "types.h"
#include <stddef.h>
#define GFP_KERNEL 0U
@@ -19,7 +19,11 @@ static inline void init_waitqueue_head(struct wait_queue_head *wq)
do { while (!(condition)) { __asm__ volatile("pause"); } } while(0)
#define wait_event_timeout(wq, condition, timeout) \
({ (void)(wq); (condition) ? 1 : 0; })
({ (void)(wq); unsigned long _deadline = jiffies + (timeout); \
int _ret = 0; \
while (!(_ret = !!(condition)) && time_before(jiffies, _deadline)) { \
__asm__ volatile("pause"); \
} _ret; })
#define wait_event_interruptible(wq, condition) \
({ (void)(wq); (condition) ? 0 : -512; })
@@ -51,6 +51,17 @@ struct key_params {
const u8 *key;
u8 key_len;
u32 cipher;
u8 key_idx;
};
struct cfg80211_bss {
u8 bssid[6];
struct ieee80211_channel *channel;
s16 signal;
u16 capability;
u16 beacon_interval;
const u8 *ies;
size_t ies_len;
};
struct cfg80211_connect_params {
@@ -99,9 +110,11 @@ extern void cfg80211_connect_bss(struct net_device *dev,
u16 status,
gfp_t gfp);
extern void cfg80211_new_sta(struct net_device *dev, const u8 *mac_addr,
struct station_parameters *params, gfp_t gfp);
struct station_parameters *params, gfp_t gfp);
extern void cfg80211_rx_mgmt(struct wireless_dev *wdev, u32 freq, int sig_dbm,
const u8 *buf, size_t len, gfp_t gfp);
const u8 *buf, size_t len, gfp_t gfp);
extern void cfg80211_mgmt_tx_status(struct wireless_dev *wdev, u64 cookie,
const u8 *buf, size_t len, bool ack, gfp_t gfp);
extern void cfg80211_sched_scan_results(struct wiphy *wiphy, u64 reqid);
extern void cfg80211_ready_on_channel(struct wireless_dev *wdev,
u64 cookie,
@@ -109,5 +122,19 @@ extern void cfg80211_ready_on_channel(struct wireless_dev *wdev,
u32 chan_type,
u32 duration,
gfp_t gfp);
extern u32 ieee80211_channel_to_frequency(u32 chan, u32 band);
extern u32 ieee80211_frequency_to_channel(u32 freq);
extern struct cfg80211_bss *cfg80211_inform_bss(struct wiphy *wiphy,
struct wireless_dev *wdev,
u32 freq,
const u8 *bssid,
u64 tsf,
u16 capability,
u16 beacon_interval,
const u8 *ie,
size_t ielen,
int signal,
gfp_t gfp);
extern void cfg80211_put_bss(struct cfg80211_bss *bss);
#endif
@@ -9,6 +9,7 @@
struct ieee80211_hw {
struct wiphy *wiphy;
const struct ieee80211_ops *ops;
void *priv;
int registered;
u32 extra_tx_headroom;
@@ -32,6 +33,25 @@ struct ieee80211_bss_conf {
bool assoc;
u16 aid;
u16 beacon_int;
u8 bssid[6];
u32 basic_rates;
u8 bandwidth;
struct {
u32 center_freq;
u16 band;
void *channel;
} chandef;
};
struct ieee80211_rx_status {
u16 freq;
u32 band;
s8 signal;
s8 noise;
u8 rate_idx;
u32 flag;
u8 antenna;
u32 rx_flags;
};
enum ieee80211_sta_state {
@@ -62,8 +82,13 @@ struct ieee80211_ops {
int (*set_key)(struct ieee80211_hw *hw, enum set_key_cmd cmd,
struct ieee80211_vif *vif, struct ieee80211_sta *sta,
struct key_params *key);
int (*ampdu_action)(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
struct ieee80211_sta *sta, u16 action, u16 tid, u16 ssn);
void (*sw_scan_start)(struct ieee80211_hw *hw, struct ieee80211_vif *vif, const u8 *mac_addr);
void (*sw_scan_complete)(struct ieee80211_hw *hw, struct ieee80211_vif *vif);
u64 (*prepare_multicast)(struct ieee80211_hw *hw, void *mc_list);
void (*configure_filter)(struct ieee80211_hw *hw, u32 changed_flags,
u32 *total_flags, u64 multicast);
int (*sched_scan_start)(struct ieee80211_hw *hw, struct ieee80211_vif *vif, void *req);
void (*sched_scan_stop)(struct ieee80211_hw *hw, struct ieee80211_vif *vif);
};
@@ -87,6 +112,11 @@ extern void ieee80211_scan_completed(struct ieee80211_hw *hw, bool aborted);
extern void ieee80211_connection_loss(struct ieee80211_vif *vif);
extern int ieee80211_start_tx_ba_session(struct ieee80211_sta *sta, u16 tid, u16 timeout);
extern int ieee80211_stop_tx_ba_session(struct ieee80211_sta *sta, u16 tid);
extern int ieee80211_sta_state(struct ieee80211_hw *hw, struct ieee80211_vif *vif,
struct ieee80211_sta *sta, u32 old_state, u32 new_state);
extern struct ieee80211_sta *ieee80211_find_sta(struct ieee80211_hw *hw, const u8 *addr);
extern void ieee80211_beacon_loss(struct ieee80211_vif *vif);
extern void ieee80211_rx_irqsafe(struct ieee80211_hw *hw, struct sk_buff *skb);
extern size_t ieee80211_rx_drain(struct ieee80211_hw *hw);
#endif
@@ -17,5 +17,7 @@ pub use rust_impl::memory;
pub use rust_impl::net;
pub use rust_impl::pci;
pub use rust_impl::sync;
pub use rust_impl::timer;
pub use rust_impl::wait;
pub use rust_impl::wireless;
pub use rust_impl::workqueue;
@@ -100,3 +100,56 @@ pub extern "C" fn devres_free_all(dev: *mut u8) {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn devm_kzalloc_and_devm_kfree_round_trip() {
let dev: *mut u8 = 0xDEAD_0000 as *mut u8;
let ptr = devm_kzalloc(dev, 64, 0);
assert!(!ptr.is_null(), "devm_kzalloc should return non-null");
let bytes = unsafe { std::slice::from_raw_parts(ptr, 64) };
assert!(
bytes.iter().all(|&b| b == 0),
"devm_kzalloc should zero memory"
);
devm_kfree(dev, ptr);
}
#[test]
fn devm_kfree_null_is_safe() {
devm_kfree(0xDEAD_0001 as *mut u8, std::ptr::null_mut());
}
#[test]
fn devres_free_all_cleans_up() {
let dev: *mut u8 = 0xBEEF_0000 as *mut u8;
let p1 = devm_kzalloc(dev, 32, 0);
let p2 = devm_kzalloc(dev, 64, 0);
assert!(!p1.is_null());
assert!(!p2.is_null());
devres_free_all(dev);
}
#[test]
fn devres_free_all_null_is_safe() {
devres_free_all(std::ptr::null_mut());
}
#[test]
fn devm_kzalloc_null_dev_returns_untracked() {
let ptr = devm_kzalloc(std::ptr::null_mut(), 32, 0);
assert!(!ptr.is_null());
crate::rust_impl::memory::kfree(ptr);
}
#[test]
fn tracked_layout_rejects_zero_size() {
assert!(tracked_layout(0, 0).is_none());
}
}
@@ -1,4 +1,5 @@
use std::alloc::{alloc_zeroed, dealloc, Layout};
use std::collections::HashMap;
use std::ffi::{c_char, c_void, CStr};
use std::ptr;
use std::sync::atomic::{fence, Ordering};
@@ -10,6 +11,7 @@ lazy_static::lazy_static! {
.ok()
.map(|fd| fd)
};
static ref DMA_MASK_TRACKER: Mutex<HashMap<usize, u64>> = Mutex::new(HashMap::new());
}
#[cfg(target_os = "redox")]
@@ -157,15 +159,38 @@ pub extern "C" fn dma_map_single(_dev: *mut u8, ptr: *mut u8, _size: usize, _dir
}
#[no_mangle]
pub extern "C" fn dma_unmap_single(_dev: *mut u8, _addr: u64, _size: usize, _dir: u32) {}
pub extern "C" fn dma_unmap_single(_dev: *mut u8, addr: u64, _size: usize, _dir: u32) {
if addr == 0 {
return;
}
log::trace!("dma_unmap_single: addr={:#x}", addr);
}
#[no_mangle]
pub extern "C" fn dma_set_mask(_dev: *mut u8, _mask: u64) -> i32 {
pub extern "C" fn dma_set_mask(dev: *mut u8, mask: u64) -> i32 {
if dev.is_null() {
return -22;
}
if let Ok(mut tracker) = DMA_MASK_TRACKER.lock() {
tracker.insert(dev as usize, mask);
}
log::debug!("dma_set_mask: dev={:#x} mask={:#x}", dev as usize, mask);
0
}
#[no_mangle]
pub extern "C" fn dma_set_coherent_mask(_dev: *mut u8, _mask: u64) -> i32 {
pub extern "C" fn dma_set_coherent_mask(dev: *mut u8, mask: u64) -> i32 {
if dev.is_null() {
return -22;
}
if let Ok(mut tracker) = DMA_MASK_TRACKER.lock() {
tracker.insert(dev as usize | 0x1, mask);
}
log::debug!(
"dma_set_coherent_mask: dev={:#x} mask={:#x}",
dev as usize,
mask
);
0
}
@@ -338,7 +363,12 @@ pub extern "C" fn dma_map_page(
}
#[no_mangle]
pub extern "C" fn dma_unmap_page(_dev: *mut u8, _addr: u64, _size: usize, _dir: u32) {}
pub extern "C" fn dma_unmap_page(_dev: *mut u8, addr: u64, _size: usize, _dir: u32) {
if addr == 0 {
return;
}
log::trace!("dma_unmap_page: addr={:#x}", addr);
}
#[no_mangle]
pub extern "C" fn dma_mapping_error(_dev: *mut u8, addr: u64) -> i32 {
@@ -45,7 +45,10 @@ where
if guard.is_none() {
*guard = Some(HashMap::new());
}
f(guard.as_mut().unwrap())
match guard.as_mut() {
Some(map) => f(map),
None => f(&mut HashMap::new()),
}
}
fn with_handles<F, R>(f: F) -> R
@@ -56,7 +59,10 @@ where
if guard.is_none() {
*guard = Some(BTreeMap::new());
}
f(guard.as_mut().unwrap())
match guard.as_mut() {
Some(map) => f(map),
None => f(&mut BTreeMap::new()),
}
}
fn next_gem_handle() -> u32 {
@@ -263,3 +269,105 @@ pub extern "C" fn drm_connector_register(_connector: *mut u8) -> i32 {
pub extern "C" fn drm_crtc_handle_vblank(_crtc: *mut u8) -> u32 {
0
}
#[cfg(test)]
mod tests {
use super::*;
fn alloc_fake_obj() -> *mut u8 {
let layout = std::alloc::Layout::from_size_align(256, 8).unwrap();
let ptr = unsafe { std::alloc::alloc(layout) };
assert!(!ptr.is_null());
ptr
}
fn free_fake_obj(ptr: *mut u8) {
let layout = std::alloc::Layout::from_size_align(256, 8).unwrap();
unsafe { std::alloc::dealloc(ptr, layout) };
}
#[test]
fn drm_gem_object_init_and_release() {
let obj = alloc_fake_obj();
let rc = drm_gem_object_init(std::ptr::null_mut(), obj, 4096);
assert_eq!(rc, 0);
drm_gem_object_release(obj);
free_fake_obj(obj);
}
#[test]
fn drm_gem_handle_create_and_lookup() {
let file: *mut u8 = 0x1100 as *mut u8;
let obj = alloc_fake_obj();
drm_gem_object_init(std::ptr::null_mut(), obj, 1024);
let mut handle: u32 = 0;
let rc = drm_gem_handle_create(file, obj, &mut handle);
assert_eq!(rc, 0, "handle_create should succeed");
assert!(handle > 0, "handle should be nonzero");
let found = drm_gem_object_lookup(file, handle);
assert_eq!(found, obj, "lookup should return the original object");
let not_found = drm_gem_object_lookup(file, 99999);
assert_eq!(
not_found,
std::ptr::null_mut(),
"invalid handle should return null"
);
drm_gem_object_release(obj);
free_fake_obj(obj);
}
#[test]
fn drm_gem_handle_delete_removes_mapping() {
let file: *mut u8 = 0x1200 as *mut u8;
let obj = alloc_fake_obj();
drm_gem_object_init(std::ptr::null_mut(), obj, 2048);
let mut handle: u32 = 0;
drm_gem_handle_create(file, obj, &mut handle);
assert!(handle > 0);
drm_gem_handle_delete(file, handle);
let found = drm_gem_object_lookup(file, handle);
assert_eq!(
found,
std::ptr::null_mut(),
"deleted handle should return null"
);
drm_gem_object_release(obj);
free_fake_obj(obj);
}
#[test]
fn drm_dev_register_and_unregister_are_callable() {
let dev: *mut u8 = 0x1300 as *mut u8;
assert_eq!(drm_dev_register(dev, 0), 0);
drm_dev_unregister(dev);
}
#[test]
fn drm_ioctl_returns_zero() {
assert_eq!(
drm_ioctl(
std::ptr::null_mut(),
0,
std::ptr::null_mut(),
std::ptr::null_mut()
),
0
);
}
#[test]
fn drm_null_pointers_are_safe() {
drm_gem_object_release(std::ptr::null_mut());
drm_dev_unregister(std::ptr::null_mut());
drm_mode_config_reset(std::ptr::null_mut());
assert_eq!(drm_connector_register(std::ptr::null_mut()), 0);
assert_eq!(drm_crtc_handle_vblank(std::ptr::null_mut()), 0);
}
}
@@ -149,3 +149,95 @@ pub extern "C" fn idr_destroy(idr: *mut Idr) {
idr_ref.map.clear();
idr_ref.next_id = 0;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn idr_alloc_and_find_round_trip() {
let mut idr = std::mem::MaybeUninit::<Idr>::uninit();
idr_init(idr.as_mut_ptr());
let ptr1: *mut u8 = 0x1000 as *mut u8;
let id1 = idr_alloc(idr.as_mut_ptr(), ptr1, 1, 0, 0);
assert!(id1 >= 1, "allocated ID should be >= start");
assert_eq!(idr_find(idr.as_mut_ptr(), id1 as u32), ptr1);
assert_eq!(idr_find(idr.as_mut_ptr(), 9999), std::ptr::null_mut());
idr_destroy(idr.as_mut_ptr());
}
#[test]
fn idr_remove_frees_slot() {
let mut idr = std::mem::MaybeUninit::<Idr>::uninit();
idr_init(idr.as_mut_ptr());
let ptr1: *mut u8 = 0x2000 as *mut u8;
let id1 = idr_alloc(idr.as_mut_ptr(), ptr1, 10, 0, 0);
assert!(id1 >= 10);
idr_remove(idr.as_mut_ptr(), id1 as u32);
assert_eq!(idr_find(idr.as_mut_ptr(), id1 as u32), std::ptr::null_mut());
idr_destroy(idr.as_mut_ptr());
}
#[test]
fn idr_alloc_with_bounded_range() {
let mut idr = std::mem::MaybeUninit::<Idr>::uninit();
idr_init(idr.as_mut_ptr());
let ptr1: *mut u8 = 0x3000 as *mut u8;
let id1 = idr_alloc(idr.as_mut_ptr(), ptr1, 5, 8, 0);
assert!(id1 >= 5 && id1 < 8, "ID should be in [5, 8)");
idr_destroy(idr.as_mut_ptr());
}
#[test]
fn idr_alloc_returns_enospc_when_full() {
let mut idr = std::mem::MaybeUninit::<Idr>::uninit();
idr_init(idr.as_mut_ptr());
let ptr1: *mut u8 = 0x4000 as *mut u8;
let id1 = idr_alloc(idr.as_mut_ptr(), ptr1, 1, 2, 0);
assert_eq!(id1, 1);
let ptr2: *mut u8 = 0x4001 as *mut u8;
let id2 = idr_alloc(idr.as_mut_ptr(), ptr2, 1, 2, 0);
assert_eq!(id2, -ENOSPC);
idr_destroy(idr.as_mut_ptr());
}
#[test]
fn idr_null_pointers_are_safe() {
assert_eq!(
idr_alloc(std::ptr::null_mut(), std::ptr::null_mut(), 1, 0, 0),
-EINVAL
);
assert_eq!(idr_find(std::ptr::null_mut(), 1), std::ptr::null_mut());
idr_remove(std::ptr::null_mut(), 1);
idr_destroy(std::ptr::null_mut());
idr_init(std::ptr::null_mut());
}
#[test]
fn idr_alloc_reuses_removed_id() {
let mut idr = std::mem::MaybeUninit::<Idr>::uninit();
idr_init(idr.as_mut_ptr());
let ptr1: *mut u8 = 0x5000 as *mut u8;
let id1 = idr_alloc(idr.as_mut_ptr(), ptr1, 1, 0, 0);
idr_remove(idr.as_mut_ptr(), id1 as u32);
let ptr2: *mut u8 = 0x5001 as *mut u8;
let id2 = idr_alloc(idr.as_mut_ptr(), ptr2, 1, 0, 0);
assert_eq!(id2, id1, "should reuse removed ID");
idr_destroy(idr.as_mut_ptr());
}
}
@@ -17,6 +17,7 @@ pub type IrqHandler = extern "C" fn(i32, *mut u8) -> u32;
struct IrqEntry {
cancel: Arc<AtomicBool>,
masked: Arc<AtomicBool>,
fd: Option<File>,
handle: Option<std::thread::JoinHandle<()>>,
}
@@ -51,7 +52,9 @@ pub extern "C" fn request_irq(
};
let cancel = Arc::new(AtomicBool::new(false));
let masked = Arc::new(AtomicBool::new(false));
let cancel_clone = Arc::clone(&cancel);
let masked_clone = Arc::clone(&masked);
let send_dev_id = SendU8Ptr(dev_id);
let handle = std::thread::spawn(move || {
@@ -69,6 +72,9 @@ pub extern "C" fn request_irq(
if cancel_clone.load(Ordering::Acquire) {
break;
}
if masked_clone.load(Ordering::Acquire) {
continue;
}
handler(irq as i32, send_dev_id.as_ptr());
}
}
@@ -77,6 +83,7 @@ pub extern "C" fn request_irq(
let entry = IrqEntry {
cancel: Arc::clone(&cancel),
masked: Arc::clone(&masked),
fd: Some(fd),
handle: Some(handle),
};
@@ -120,7 +127,102 @@ pub extern "C" fn free_irq(irq: u32, _dev_id: *mut u8) {
}
#[no_mangle]
pub extern "C" fn enable_irq(_irq: u32) {}
pub extern "C" fn disable_irq_nosync(irq: u32) {
disable_irq(irq)
}
#[no_mangle]
pub extern "C" fn disable_irq(_irq: u32) {}
pub extern "C" fn enable_irq(irq: u32) {
if let Ok(table) = IRQ_TABLE.lock() {
if let Some(entry) = table.get(&irq) {
entry.masked.store(false, Ordering::Release);
log::trace!("enable_irq: unmasked IRQ {}", irq);
}
}
}
#[no_mangle]
pub extern "C" fn disable_irq(irq: u32) {
if let Ok(table) = IRQ_TABLE.lock() {
if let Some(entry) = table.get(&irq) {
entry.masked.store(true, Ordering::Release);
log::trace!("disable_irq: masked IRQ {}", irq);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_irq_returns_error_without_scheme() {
let result = request_irq(99, test_handler, 0, std::ptr::null(), std::ptr::null_mut());
assert_eq!(
result, -22,
"request_irq should return -EINVAL without /scheme/irq/"
);
}
#[test]
fn free_irq_on_unregistered_is_safe() {
free_irq(42, std::ptr::null_mut());
}
#[test]
fn enable_irq_on_unregistered_is_noop() {
enable_irq(42);
}
#[test]
fn disable_irq_on_unregistered_is_noop() {
disable_irq(42);
}
#[test]
fn disable_irq_nosync_is_equivalent_to_disable_irq() {
// Insert a synthetic entry directly, then verify disable_irq_nosync
// sets the same masked state as disable_irq.
let cancel = Arc::new(AtomicBool::new(false));
let masked = Arc::new(AtomicBool::new(false));
let entry = IrqEntry {
cancel,
masked: Arc::clone(&masked),
fd: None,
handle: None,
};
{
let mut table = IRQ_TABLE.lock().unwrap();
table.insert(200, entry);
}
disable_irq_nosync(200);
assert!(
masked.load(Ordering::Acquire),
"disable_irq_nosync should set masked=true"
);
enable_irq(200);
assert!(
!masked.load(Ordering::Acquire),
"enable_irq should set masked=false"
);
disable_irq(200);
assert!(
masked.load(Ordering::Acquire),
"disable_irq should set masked=true"
);
{
let mut table = IRQ_TABLE.lock().unwrap();
table.remove(&200);
}
}
extern "C" fn test_handler(_irq: i32, _dev_id: *mut u8) -> u32 {
0
}
}
@@ -17,6 +17,10 @@ const EBUSY: i32 = 16;
lazy_static::lazy_static! {
static ref STA_REGISTRY: Mutex<HashMap<usize, StaRegistryEntry>> = Mutex::new(HashMap::new());
static ref BA_SESSIONS: Mutex<HashMap<usize, Vec<u16>>> = Mutex::new(HashMap::new());
static ref RX_QUEUE: Mutex<Vec<(usize, usize)>> = Mutex::new(Vec::new());
static ref HW_WDEV_MAP: Mutex<HashMap<usize, usize>> = Mutex::new(HashMap::new());
static ref RX_CALLBACKS: Mutex<HashMap<usize, usize>> = Mutex::new(HashMap::new());
static ref TX_STATS: Mutex<HashMap<usize, TxStats>> = Mutex::new(HashMap::new());
}
#[derive(Clone, Copy)]
@@ -26,6 +30,15 @@ struct StaRegistryEntry {
state: u32,
}
#[derive(Clone, Copy, Default)]
pub struct TxStats {
pub total: u64,
pub acked: u64,
pub nacked: u64,
}
pub type RxCallback = extern "C" fn(*mut Ieee80211Hw, *mut SkBuff);
#[repr(C)]
pub struct Ieee80211Ops {
pub tx: Option<extern "C" fn(*mut Ieee80211Hw, *mut SkBuff)>,
@@ -177,8 +190,37 @@ pub extern "C" fn ieee80211_free_hw(hw: *mut Ieee80211Hw) {
if hw.is_null() {
return;
}
let hw_key = hw as usize;
if let Ok(mut map) = HW_WDEV_MAP.lock() {
map.remove(&hw_key);
}
if let Ok(mut map) = RX_CALLBACKS.lock() {
map.remove(&hw_key);
}
if let Ok(mut stats_map) = TX_STATS.lock() {
stats_map.remove(&hw_key);
}
if let Ok(mut queue) = RX_QUEUE.lock() {
let mut i = 0;
while i < queue.len() {
if queue[i].0 == hw_key {
let (_, skb_key) = queue.swap_remove(i);
let skb = skb_key as *mut SkBuff;
if !skb.is_null() {
super::net::kfree_skb(skb);
}
} else {
i += 1;
}
}
}
if let Ok(mut registry) = STA_REGISTRY.lock() {
registry.retain(|_, entry| entry.hw != hw as usize);
registry.retain(|_, entry| entry.hw != hw_key);
}
unsafe {
let hw_box = Box::from_raw(hw);
@@ -230,8 +272,93 @@ pub extern "C" fn ieee80211_queue_work(_hw: *mut Ieee80211Hw, work: *mut c_void)
let _ = schedule_work(work.cast::<WorkStruct>());
}
/// Register the WirelessDev associated with an Ieee80211Hw.
/// Must be called by the driver after ieee80211_alloc_hw_nm and before scan/connect.
/// Required for ieee80211_scan_completed to find the correct wdev.
#[no_mangle]
pub extern "C" fn ieee80211_scan_completed(_hw: *mut Ieee80211Hw, _aborted: bool) {}
pub extern "C" fn ieee80211_link_hw_wdev(
hw: *mut Ieee80211Hw,
wdev: *mut super::wireless::WirelessDev,
) {
if hw.is_null() || wdev.is_null() {
return;
}
if let Ok(mut map) = HW_WDEV_MAP.lock() {
map.insert(hw as usize, wdev as usize);
}
}
/// Register a per-hw callback that receives drained RX frames.
/// When `ieee80211_rx_drain` processes a queued frame and a callback
/// is registered for the hw instance, the frame is delivered to the
/// callback instead of being logged and freed.
#[no_mangle]
pub extern "C" fn ieee80211_register_rx_handler(
hw: *mut Ieee80211Hw,
callback: Option<RxCallback>,
) {
if hw.is_null() {
return;
}
if let Ok(mut map) = RX_CALLBACKS.lock() {
match callback {
Some(cb) => {
map.insert(hw as usize, cb as usize);
}
None => {
map.remove(&(hw as usize));
}
}
}
}
/// Retrieve accumulated TX statistics for a given hw instance.
#[no_mangle]
pub extern "C" fn ieee80211_get_tx_stats(hw: *mut Ieee80211Hw) -> TxStats {
if hw.is_null() {
return TxStats::default();
}
if let Ok(stats_map) = TX_STATS.lock() {
stats_map.get(&(hw as usize)).copied().unwrap_or_default()
} else {
TxStats::default()
}
}
#[no_mangle]
pub extern "C" fn ieee80211_scan_completed(hw: *mut Ieee80211Hw, aborted: bool) {
if hw.is_null() {
return;
}
let wiphy = unsafe { (*hw).wiphy };
if wiphy.is_null() {
return;
}
let wdev_ptr = match HW_WDEV_MAP.lock() {
Ok(map) => match map.get(&(hw as usize)) {
Some(&ptr) => ptr as *mut super::wireless::WirelessDev,
None => {
log::warn!(
"ieee80211_scan_completed: no wdev registered for hw={:#x}",
hw as usize
);
return;
}
},
Err(_) => return,
};
let scan_info = super::wireless::Cfg80211ScanInfo { aborted };
let mut scan_request = super::wireless::Cfg80211ScanRequest {
wiphy,
wdev: wdev_ptr,
n_ssids: 0,
n_channels: 0,
};
super::wireless::cfg80211_scan_done(&mut scan_request, &scan_info);
}
#[no_mangle]
pub extern "C" fn ieee80211_connection_loss(vif: *mut Ieee80211Vif) {
@@ -291,6 +418,129 @@ pub extern "C" fn ieee80211_rx_irqsafe(hw: *mut Ieee80211Hw, skb: *mut SkBuff) {
if hw.is_null() || skb.is_null() {
return;
}
let hw_key = hw as usize;
let skb_key = skb as usize;
if let Ok(mut queue) = RX_QUEUE.lock() {
queue.push((hw_key, skb_key));
log::trace!(
"ieee80211_rx_irqsafe: queued frame hw={:#x} skb={:#x} queue_len={}",
hw_key,
skb_key,
queue.len()
);
} else {
log::warn!("ieee80211_rx_irqsafe: failed to lock RX queue, dropping frame");
super::net::kfree_skb(skb);
}
}
/// Drain and consume all queued RX frames for a specific hw instance.
/// If an RX handler has been registered via ieee80211_register_rx_handler,
/// frames are delivered to that handler. Otherwise frames are classified,
/// logged, and freed.
#[no_mangle]
pub extern "C" fn ieee80211_rx_drain(hw: *mut Ieee80211Hw) -> usize {
if hw.is_null() {
return 0;
}
let hw_key = hw as usize;
let rx_callback = if let Ok(map) = RX_CALLBACKS.lock() {
map.get(&hw_key).copied()
} else {
None
};
let Ok(mut queue) = RX_QUEUE.lock() else {
return 0;
};
let mut drained = 0usize;
let mut i = 0;
while i < queue.len() {
if queue[i].0 == hw_key {
let (_, skb_key) = queue.swap_remove(i);
let skb = skb_key as *mut SkBuff;
if !skb.is_null() {
if let Some(cb) = rx_callback {
let callback: RxCallback = unsafe { std::mem::transmute(cb) };
callback(hw, skb);
} else {
let skb_ref = unsafe { &mut *skb };
let frame_type = extract_frame_type(skb_ref);
let frame_len = skb_ref.len;
match frame_type {
FrameType::Management(subtype) => {
log::debug!(
"rx_drain: mgmt subtype={} len={} hw={:#x}",
subtype,
frame_len,
hw_key
);
}
FrameType::Data => {
log::debug!("rx_drain: data frame len={} hw={:#x}", frame_len, hw_key);
}
FrameType::Control(subtype) => {
log::trace!(
"rx_drain: ctrl subtype={} len={} hw={:#x}",
subtype,
frame_len,
hw_key
);
}
FrameType::Unknown => {
log::trace!(
"rx_drain: unknown frame len={} hw={:#x}",
frame_len,
hw_key
);
}
}
super::net::kfree_skb(skb);
}
}
drained += 1;
} else {
i += 1;
}
}
if drained > 0 {
log::debug!(
"ieee80211_rx_drain: hw={:#x} drained {} frames",
hw_key,
drained
);
}
drained
}
enum FrameType {
Management(u8),
Data,
Control(u8),
Unknown,
}
fn extract_frame_type(skb: &SkBuff) -> FrameType {
let len = (skb.len as usize).min(2);
if len < 2 {
return FrameType::Unknown;
}
let data = unsafe { std::slice::from_raw_parts(skb.data, len) };
let frame_ctl = u16::from_le_bytes([data[0], data[1]]);
let type_val = ((frame_ctl >> 2) & 0x3) as u8;
let subtype = ((frame_ctl >> 4) & 0xF) as u8;
match type_val {
0 => FrameType::Management(subtype),
1 => FrameType::Control(subtype),
2 => FrameType::Data,
_ => FrameType::Unknown,
}
}
#[no_mangle]
@@ -298,6 +548,20 @@ pub extern "C" fn ieee80211_tx_status(hw: *mut Ieee80211Hw, skb: *mut SkBuff) {
if hw.is_null() || skb.is_null() {
return;
}
let hw_key = hw as usize;
if let Ok(mut stats_map) = TX_STATS.lock() {
let stats = stats_map.entry(hw_key).or_default();
stats.total += 1;
stats.acked += 1;
}
log::trace!(
"ieee80211_tx_status: hw={:#x} skb={:#x}",
hw_key,
skb as usize
);
super::net::kfree_skb(skb);
}
#[no_mangle]
@@ -305,19 +569,53 @@ pub extern "C" fn ieee80211_get_tid(skb: *const SkBuff) -> u8 {
if skb.is_null() {
return 0;
}
unsafe {
let s = &*skb;
if s.data.is_null() || s.len < 26 {
return 0;
}
let frame_control = u16::from_le_bytes([*s.data, *s.data.add(1)]);
let subtype = (frame_control >> 4) & 0xF;
if subtype != 0x8 {
return 0;
}
let qos = (frame_control >> 7) & 0x1;
if qos == 0 {
return 0;
}
let qos_offset: usize = 24;
if (s.len as usize) < qos_offset + 2 {
return 0;
}
let tid = (*s.data.add(qos_offset)) & 0xF;
tid
}
}
0
#[repr(C)]
struct ChanDef {
center_freq: u32,
band: u16,
channel: *mut c_void,
}
#[no_mangle]
pub extern "C" fn ieee80211_chandef_create(
chandef: *mut c_void,
channel: *const super::wireless::Ieee80211Channel,
_chan_type: u32,
chan_type: u32,
) {
if chandef.is_null() || channel.is_null() {
return;
}
unsafe {
let cd = chandef.cast::<ChanDef>();
let ch = &*channel;
(*cd).center_freq = ch.center_freq as u32;
(*cd).band = ch.band as u16;
(*cd).channel = channel as *mut c_void;
let _ = chan_type;
}
}
pub const IEEE80211_STA_NOTEXIST: u32 = 0;
@@ -587,4 +885,74 @@ mod tests {
fn ieee80211_get_tid_returns_zero_for_null() {
assert_eq!(ieee80211_get_tid(ptr::null()), 0);
}
static RX_RECEIVED: AtomicI32 = AtomicI32::new(0);
extern "C" fn test_rx_callback(_hw: *mut Ieee80211Hw, skb: *mut SkBuff) {
RX_RECEIVED.fetch_add(1, Ordering::Release);
super::super::net::kfree_skb(skb);
}
#[test]
fn rx_callback_receives_drained_frames() {
let hw = ieee80211_alloc_hw_nm(0, ptr::null(), ptr::null());
assert!(!hw.is_null());
RX_RECEIVED.store(0, Ordering::Release);
ieee80211_register_rx_handler(hw, Some(test_rx_callback));
let data: [u8; 24] = [
0x88u8, 0x01, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x06, 0x05, 0x04, 0x03,
0x02, 0x01, 0xAA, 0xAA, 0x03, 0x00, 0x00, 0x00, 0x08, 0x06,
];
let skb = super::super::net::alloc_skb(128, 0);
assert!(!skb.is_null());
unsafe {
ptr::copy_nonoverlapping(data.as_ptr(), (*skb).data, data.len());
(*skb).len = data.len() as u32;
}
ieee80211_rx_irqsafe(hw, skb);
assert_eq!(ieee80211_rx_drain(hw), 1);
assert_eq!(RX_RECEIVED.load(Ordering::Acquire), 1);
ieee80211_register_rx_handler(hw, None);
let skb2 = super::super::net::alloc_skb(128, 0);
assert!(!skb2.is_null());
unsafe {
ptr::copy_nonoverlapping(data.as_ptr(), (*skb2).data, data.len());
(*skb2).len = data.len() as u32;
}
ieee80211_rx_irqsafe(hw, skb2);
assert_eq!(ieee80211_rx_drain(hw), 1);
assert_eq!(RX_RECEIVED.load(Ordering::Acquire), 1);
ieee80211_free_hw(hw);
}
#[test]
fn tx_status_tracks_statistics() {
let hw = ieee80211_alloc_hw_nm(0, ptr::null(), ptr::null());
assert!(!hw.is_null());
let stats = ieee80211_get_tx_stats(hw);
assert_eq!(stats.total, 0);
assert_eq!(stats.acked, 0);
for _ in 0..3 {
let skb = super::super::net::alloc_skb(64, 0);
assert!(!skb.is_null());
unsafe {
(*skb).len = 10;
}
ieee80211_tx_status(hw, skb);
}
let stats = ieee80211_get_tx_stats(hw);
assert_eq!(stats.total, 3);
assert_eq!(stats.acked, 3);
ieee80211_free_hw(hw);
let stats_after_free = ieee80211_get_tx_stats(hw);
assert_eq!(stats_after_free.total, 0);
}
}
@@ -128,6 +128,46 @@ fn open_current_device(dev: *mut PciDev) -> Result<PciDevice, i32> {
})
}
fn quirk_location_from_dev(dev: &PciDev) -> PciLocation {
PciLocation {
segment: 0,
bus: dev.bus,
device: dev.dev,
function: dev.func,
}
}
fn quirk_info_from_dev(dev: &PciDev) -> PciDeviceInfo {
let location = quirk_location_from_dev(dev);
let mut info = PciDeviceInfo {
location,
vendor_id: dev.vendor,
device_id: dev.device,
subsystem_vendor_id: redox_driver_sys::quirks::PCI_QUIRK_ANY_ID,
subsystem_device_id: redox_driver_sys::quirks::PCI_QUIRK_ANY_ID,
revision: dev.revision,
class_code: 0,
subclass: 0,
prog_if: 0,
header_type: 0,
irq: if dev.irq != 0 && dev.irq != u32::from(u8::MAX) {
Some(dev.irq)
} else {
None
},
bars: Vec::new(),
capabilities: Vec::new(),
};
if let Ok(mut pci) = PciDevice::open_location(&location) {
if let Ok(full_info) = pci.full_info() {
info = full_info;
}
}
info
}
fn clear_irq_vectors_for_ptr(dev_ptr: usize) {
if let Ok(mut vectors) = IRQ_VECTORS.lock() {
vectors.remove(&dev_ptr);
@@ -300,7 +340,7 @@ pub extern "C" fn pci_iomap(dev: *mut PciDev, bar: u32, max_len: usize) -> *mut
if len == 0 {
return ptr::null_mut();
}
log::warn!("pci_iomap: bar={} len={} — using heap fallback", bar, len);
log::debug!("pci_iomap: bar={} len={} — mapping via ioremap", bar, len);
super::io::ioremap(unsafe { (*dev).bars[bar as usize] }, len)
}
@@ -378,7 +418,28 @@ pub extern "C" fn pci_set_master(dev: *mut PciDev) {
if dev.is_null() {
return;
}
log::info!("pci_set_master");
let mut cmd: u32 = 0;
let rc = pci_read_config_dword(dev, 0x04, &mut cmd);
if rc != 0 {
log::warn!("pci_set_master: failed to read command register");
return;
}
if cmd & 0x04 != 0 {
return;
}
cmd = (cmd & 0x0000_FFFF) | 0x04;
let rc = pci_write_config_dword(dev, 0x04, cmd);
if rc != 0 {
log::warn!("pci_set_master: failed to write command register");
return;
}
log::info!(
"pci_set_master: enabled bus mastering (cmd={:#06x})",
cmd & 0xFFFF
);
}
#[no_mangle]
@@ -397,6 +458,21 @@ pub extern "C" fn pci_resource_len(dev: *const PciDev, bar: u32) -> u64 {
unsafe { (*dev).bar_sizes[bar as usize] }
}
#[no_mangle]
pub extern "C" fn pci_get_quirk_flags(dev: *mut PciDev) -> u64 {
if dev.is_null() {
return 0;
}
let info = quirk_info_from_dev(unsafe { &*dev });
redox_driver_sys::quirks::lookup_pci_quirks(&info).bits()
}
#[no_mangle]
pub extern "C" fn pci_has_quirk(dev: *mut PciDev, flag: u64) -> bool {
(pci_get_quirk_flags(dev) & flag) != 0
}
pub type PciDriverProbe = extern "C" fn(*mut PciDev, *const PciDeviceId) -> i32;
pub type PciDriverRemove = extern "C" fn(*mut PciDev);
@@ -169,7 +169,21 @@ pub extern "C" fn mod_timer(timer: *mut TimerList, expires: u64) -> i32 {
let data_addr = entry.data.load(Ordering::Acquire) as usize;
let entry_for_thread = entry.clone();
let handle = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(delay));
let start = std::time::Instant::now();
let total = Duration::from_millis(delay);
loop {
let elapsed = start.elapsed();
if elapsed >= total {
break;
}
if !entry_for_thread.active.load(Ordering::Acquire) {
return;
}
if entry_for_thread.generation.load(Ordering::Acquire) != generation {
return;
}
std::thread::sleep(Duration::from_millis(10));
}
if !entry_for_thread.active.load(Ordering::Acquire) {
return;
@@ -254,3 +268,157 @@ pub extern "C" fn timer_pending(timer: *const TimerList) -> i32 {
None => 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::AtomicUsize;
extern "C" fn test_timer_callback(_data: c_ulong) {}
#[test]
fn setup_timer_initializes_struct() {
let mut timer = std::mem::MaybeUninit::<TimerList>::uninit();
setup_timer(timer.as_mut_ptr(), test_timer_callback, 42);
let timer_ref = unsafe { &*timer.as_ptr() };
assert_eq!(timer_ref.expires.load(Ordering::Acquire), 0);
assert!(!timer_ref.active.load(Ordering::Acquire));
}
#[test]
fn setup_timer_null_pointer_is_safe() {
setup_timer(std::ptr::null_mut(), test_timer_callback, 0);
}
#[test]
fn mod_timer_returns_was_active() {
let mut timer = std::mem::MaybeUninit::<TimerList>::uninit();
setup_timer(timer.as_mut_ptr(), test_timer_callback, 0);
let was_active = mod_timer(timer.as_mut_ptr(), current_jiffies() + 30000);
assert_eq!(
was_active, 0,
"first mod_timer should return 0 (was inactive)"
);
let was_active = mod_timer(timer.as_mut_ptr(), current_jiffies() + 30000);
assert_eq!(
was_active, 1,
"second mod_timer should return 1 (was active)"
);
del_timer_sync(timer.as_mut_ptr());
}
#[test]
fn mod_timer_null_pointer_returns_zero() {
assert_eq!(mod_timer(std::ptr::null_mut(), 1000), 0);
}
#[test]
fn del_timer_cancels_active_timer() {
let mut timer = std::mem::MaybeUninit::<TimerList>::uninit();
setup_timer(timer.as_mut_ptr(), test_timer_callback, 0);
mod_timer(timer.as_mut_ptr(), current_jiffies() + 30000);
let result = del_timer_sync(timer.as_mut_ptr());
assert_eq!(result, 1, "del_timer should return 1 for active timer");
}
#[test]
fn del_timer_null_pointer_returns_zero() {
assert_eq!(del_timer(std::ptr::null_mut()), 0);
}
#[test]
fn del_timer_sync_cancels_long_timer() {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
extern "C" fn counting_callback(_data: c_ulong) {
COUNTER.fetch_add(1, Ordering::Relaxed);
}
let mut timer = std::mem::MaybeUninit::<TimerList>::uninit();
setup_timer(timer.as_mut_ptr(), counting_callback, 0);
COUNTER.store(0, Ordering::Relaxed);
mod_timer(timer.as_mut_ptr(), current_jiffies() + 30000);
let result = del_timer_sync(timer.as_mut_ptr());
assert_eq!(result, 1, "del_timer_sync should return 1 for active timer");
std::thread::sleep(std::time::Duration::from_millis(50));
assert_eq!(
COUNTER.load(Ordering::Relaxed),
0,
"callback should not have fired after cancel"
);
}
#[test]
fn del_timer_returns_zero_for_inactive() {
let mut timer = std::mem::MaybeUninit::<TimerList>::uninit();
setup_timer(timer.as_mut_ptr(), test_timer_callback, 0);
assert_eq!(
del_timer(timer.as_mut_ptr()),
0,
"del_timer on inactive timer should return 0"
);
}
#[test]
fn timer_pending_null_returns_zero() {
assert_eq!(timer_pending(std::ptr::null()), 0);
}
#[test]
fn timer_pending_reflects_state() {
let mut timer = std::mem::MaybeUninit::<TimerList>::uninit();
setup_timer(timer.as_mut_ptr(), test_timer_callback, 0);
assert_eq!(
timer_pending(timer.as_ptr()),
0,
"new timer should not be pending"
);
mod_timer(timer.as_mut_ptr(), current_jiffies() + 30000);
assert_eq!(
timer_pending(timer.as_ptr()),
1,
"armed timer should be pending"
);
del_timer_sync(timer.as_mut_ptr());
assert_eq!(
timer_pending(timer.as_ptr()),
0,
"cancelled timer should not be pending"
);
}
#[test]
fn mod_timer_fires_callback() {
static FIRE_COUNT: AtomicUsize = AtomicUsize::new(0);
extern "C" fn fire_callback(_data: c_ulong) {
FIRE_COUNT.fetch_add(1, Ordering::Relaxed);
}
let mut timer = std::mem::MaybeUninit::<TimerList>::uninit();
setup_timer(timer.as_mut_ptr(), fire_callback, 0);
FIRE_COUNT.store(0, Ordering::Relaxed);
mod_timer(timer.as_mut_ptr(), current_jiffies());
std::thread::sleep(std::time::Duration::from_millis(100));
assert!(
FIRE_COUNT.load(Ordering::Relaxed) >= 1,
"callback should have fired"
);
del_timer_sync(timer.as_mut_ptr());
}
}
@@ -184,3 +184,135 @@ pub extern "C" fn wait_event_timeout(
) -> i32 {
wait_event_timeout_impl(wq, || condition(), timeout_ms)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
#[derive(Copy, Clone)]
struct SendWq(*mut WaitQueueHead);
unsafe impl Send for SendWq {}
impl SendWq {
fn ptr(&self) -> *mut WaitQueueHead {
self.0
}
}
#[test]
fn init_waitqueue_head_initializes() {
let mut wq = std::mem::MaybeUninit::<WaitQueueHead>::uninit();
init_waitqueue_head(wq.as_mut_ptr());
let wq_ref = unsafe { &*wq.as_ptr() };
let guard = wq_ref.mutex.lock().unwrap();
assert!(!*guard);
}
#[test]
fn init_waitqueue_head_null_is_safe() {
init_waitqueue_head(std::ptr::null_mut());
}
#[test]
fn wake_up_null_is_safe() {
wake_up(std::ptr::null_mut());
}
#[test]
fn wait_event_unblocks_on_wake_up() {
let mut wq = std::mem::MaybeUninit::<WaitQueueHead>::uninit();
init_waitqueue_head(wq.as_mut_ptr());
let send = SendWq(wq.as_mut_ptr());
static WOKE: AtomicBool = AtomicBool::new(false);
WOKE.store(false, AtomicOrdering::Relaxed);
extern "C" fn cond_check_woke() -> bool {
WOKE.load(AtomicOrdering::Relaxed)
}
let handle = std::thread::spawn(move || {
wait_event(send.ptr(), cond_check_woke);
});
std::thread::sleep(std::time::Duration::from_millis(20));
assert!(!handle.is_finished());
WOKE.store(true, AtomicOrdering::Relaxed);
wake_up(wq.as_mut_ptr());
handle.join().expect("wait_event thread should complete");
}
#[test]
fn wait_event_returns_immediately_when_condition_true() {
let mut wq = std::mem::MaybeUninit::<WaitQueueHead>::uninit();
init_waitqueue_head(wq.as_mut_ptr());
extern "C" fn cond_true() -> bool {
true
}
let send = SendWq(wq.as_mut_ptr());
let handle = std::thread::spawn(move || {
wait_event(send.ptr(), cond_true);
});
assert!(handle.join().is_ok());
}
#[test]
fn wait_event_timeout_returns_one_when_condition_met() {
let mut wq = std::mem::MaybeUninit::<WaitQueueHead>::uninit();
init_waitqueue_head(wq.as_mut_ptr());
extern "C" fn cond_true() -> bool {
true
}
assert_eq!(wait_event_timeout(wq.as_mut_ptr(), cond_true, 100), 1);
}
#[test]
fn wait_event_timeout_returns_zero_on_timeout() {
let mut wq = std::mem::MaybeUninit::<WaitQueueHead>::uninit();
init_waitqueue_head(wq.as_mut_ptr());
extern "C" fn cond_false() -> bool {
false
}
assert_eq!(wait_event_timeout(wq.as_mut_ptr(), cond_false, 10), 0);
}
#[test]
fn wait_event_timeout_wakes_early() {
let mut wq = std::mem::MaybeUninit::<WaitQueueHead>::uninit();
init_waitqueue_head(wq.as_mut_ptr());
let send = SendWq(wq.as_mut_ptr());
static WOKE: AtomicBool = AtomicBool::new(false);
WOKE.store(false, AtomicOrdering::Relaxed);
extern "C" fn cond_check() -> bool {
WOKE.load(AtomicOrdering::Relaxed)
}
let handle = std::thread::spawn(move || wait_event_timeout(send.ptr(), cond_check, 5000));
std::thread::sleep(std::time::Duration::from_millis(20));
WOKE.store(true, AtomicOrdering::Relaxed);
wake_up(wq.as_mut_ptr());
let result = handle.join().expect("thread should complete");
assert_eq!(result, 1);
}
#[test]
fn wait_event_timeout_null_returns_zero() {
extern "C" fn cond_true() -> bool {
true
}
assert_eq!(wait_event_timeout(std::ptr::null_mut(), cond_true, 100), 0);
}
}
@@ -13,14 +13,31 @@ struct WirelessEventState {
mgmt_rx_freq: u32,
mgmt_rx_signal: i32,
mgmt_rx_len: usize,
mgmt_rx_data: Vec<u8>,
mgmt_tx_cookie: u64,
mgmt_tx_len: usize,
mgmt_tx_ack: bool,
mgmt_tx_data: Vec<u8>,
sched_scan_reqid: u64,
roc_cookie: u64,
roc_chan_freq: u16,
roc_band: u32,
roc_duration: u32,
roc_active: bool,
}
#[repr(C)]
pub struct WiphyBands {
bands: [usize; 3],
}
unsafe impl Send for WiphyBands {}
lazy_static::lazy_static! {
static ref WIRELESS_EVENTS: Mutex<HashMap<usize, WirelessEventState>> = Mutex::new(HashMap::new());
static ref BSS_REGISTRY: Mutex<Vec<Box<Cfg80211Bss>>> = Mutex::new(Vec::new());
static ref BSS_IES: Mutex<HashMap<usize, Vec<u8>>> = Mutex::new(HashMap::new());
static ref WIPY_BANDS_MAP: Mutex<HashMap<usize, WiphyBands>> = Mutex::new(HashMap::new());
}
#[repr(C)]
@@ -74,6 +91,7 @@ pub struct KeyParams {
pub key: *const u8,
pub key_len: u8,
pub cipher: u32,
pub key_idx: u8,
}
#[repr(C)]
@@ -141,8 +159,23 @@ pub extern "C" fn wiphy_free(wiphy: *mut Wiphy) {
if wiphy.is_null() {
return;
}
let wiphy_key = wiphy as usize;
if let Ok(mut events) = WIRELESS_EVENTS.lock() {
events.remove(&(wiphy as usize));
events.remove(&wiphy_key);
}
if let Ok(mut registry) = BSS_REGISTRY.lock() {
if let Ok(mut ies_map) = BSS_IES.lock() {
for entry in registry.iter() {
if entry.wiphy == wiphy_key {
let ptr = entry.as_ref() as *const Cfg80211Bss as usize;
ies_map.remove(&ptr);
}
}
}
registry.retain(|e| e.wiphy != wiphy_key);
}
if let Ok(mut bands_map) = WIPY_BANDS_MAP.lock() {
bands_map.remove(&wiphy_key);
}
unsafe {
let wiphy_box = Box::from_raw(wiphy);
@@ -304,13 +337,37 @@ pub extern "C" fn cfg80211_connect_bss(
#[no_mangle]
pub extern "C" fn cfg80211_ready_on_channel(
_wdev: *mut WirelessDev,
_cookie: u64,
_chan: *mut c_void,
wdev: *mut WirelessDev,
cookie: u64,
chan: *mut c_void,
_chan_type: u32,
_duration: u32,
duration: u32,
_gfp: u32,
) {
if wdev.is_null() {
return;
}
let key = wdev as usize;
let (freq, band) = if chan.is_null() {
(0u16, 0u32)
} else {
let ch = chan.cast::<Ieee80211Channel>();
unsafe { ((*ch).center_freq, (*ch).band) }
};
update_event_state(key, |state| {
state.roc_cookie = cookie;
state.roc_chan_freq = freq;
state.roc_band = band;
state.roc_duration = duration;
state.roc_active = true;
});
log::trace!(
"cfg80211_ready_on_channel: wdev={:#x} cookie={} freq={} duration={}",
key,
cookie,
freq,
duration
);
}
#[repr(C)]
@@ -379,6 +436,14 @@ pub extern "C" fn wiphy_bands_append(
return -22;
}
let key = wiphy as usize;
if let Ok(mut map) = WIPY_BANDS_MAP.lock() {
let entry = map
.entry(key)
.or_insert_with(|| WiphyBands { bands: [0; 3] });
entry.bands[band_idx as usize] = band as usize;
}
0
}
@@ -391,8 +456,11 @@ pub struct Cfg80211Bss {
pub beacon_interval: u16,
pub ies: *const u8,
pub ies_len: usize,
wiphy: usize,
}
unsafe impl Send for Cfg80211Bss {}
#[no_mangle]
pub extern "C" fn cfg80211_inform_bss(
wiphy: *mut Wiphy,
@@ -416,16 +484,65 @@ pub extern "C" fn cfg80211_inform_bss(
ptr::copy_nonoverlapping(bssid, bssid_bytes.as_mut_ptr(), bssid_bytes.len());
}
let Ok(mut registry) = BSS_REGISTRY.lock() else {
log::warn!("cfg80211_inform_bss: registry lock failed");
return ptr::null_mut();
};
let clamped_signal = signal.clamp(i16::MIN as i32, i16::MAX as i32) as i16;
let ies_owned: Vec<u8> = if !ies.is_null() && ies_len > 0 {
unsafe { std::slice::from_raw_parts(ies, ies_len) }.to_vec()
} else {
Vec::new()
};
for entry in registry.iter_mut() {
if entry.bssid == bssid_bytes && entry.wiphy == wiphy as usize {
entry.signal = clamped_signal;
entry.capability = capability;
entry.beacon_interval = beacon_interval;
let entry_ptr = entry.as_ref() as *const Cfg80211Bss as usize;
if let Ok(mut ies_map) = BSS_IES.lock() {
entry.ies_len = ies_owned.len();
if ies_owned.is_empty() {
entry.ies = ptr::null();
ies_map.remove(&entry_ptr);
} else {
let stored = ies_map.entry(entry_ptr).or_default();
stored.clear();
stored.extend_from_slice(&ies_owned);
entry.ies = stored.as_ptr();
}
}
return entry.as_ref() as *const Cfg80211Bss as *mut Cfg80211Bss;
}
}
let bss = Box::new(Cfg80211Bss {
bssid: bssid_bytes,
channel: ptr::null_mut(),
signal: signal.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
signal: clamped_signal,
capability,
beacon_interval,
ies,
ies_len,
ies: ptr::null(),
ies_len: 0,
wiphy: wiphy as usize,
});
Box::into_raw(bss)
registry.push(bss);
let entry = match registry.last_mut() {
Some(e) => e,
None => return ptr::null_mut(),
};
let entry_ptr = entry.as_ref() as *const Cfg80211Bss as usize;
if let Ok(mut ies_map) = BSS_IES.lock() {
if !ies_owned.is_empty() {
ies_map.insert(entry_ptr, ies_owned);
entry.ies = ies_map[&entry_ptr].as_ptr();
entry.ies_len = ies_map[&entry_ptr].len();
}
}
entry.as_ref() as *const Cfg80211Bss as *mut Cfg80211Bss
}
#[no_mangle]
@@ -433,26 +550,98 @@ pub extern "C" fn cfg80211_put_bss(bss: *mut Cfg80211Bss) {
if bss.is_null() {
return;
}
unsafe {
drop(Box::from_raw(bss));
let bss_addr = bss as usize;
let Ok(mut registry) = BSS_REGISTRY.lock() else {
log::warn!("cfg80211_put_bss: registry lock failed");
return;
};
let before = registry.len();
registry.retain(|entry| entry.as_ref() as *const Cfg80211Bss as usize != bss_addr);
let removed = before != registry.len();
if removed {
if let Ok(mut ies_map) = BSS_IES.lock() {
ies_map.remove(&bss_addr);
}
}
log::trace!(
"cfg80211_put_bss: released reference bss={:#x} (removed={})",
bss_addr,
removed
);
}
#[no_mangle]
pub extern "C" fn cfg80211_get_bss(
wiphy: *mut Wiphy,
band: u32,
_bssid: *const u8,
_ssid: *const u8,
_ssid_len: usize,
_band: u32,
bssid: *const u8,
ssid: *const u8,
ssid_len: usize,
_bss_type: u32,
_privacy: u32,
) -> *mut Cfg80211Bss {
if wiphy.is_null() || band > NL80211_BAND_6GHZ {
if wiphy.is_null() {
return ptr::null_mut();
}
let Ok(registry) = BSS_REGISTRY.lock() else {
return ptr::null_mut();
};
let want_bssid = if bssid.is_null() {
None
} else {
let mut bytes = [0u8; 6];
unsafe { ptr::copy_nonoverlapping(bssid, bytes.as_mut_ptr(), 6) };
Some(bytes)
};
let want_ssid = if ssid.is_null() || ssid_len == 0 {
None
} else {
let slice = unsafe { std::slice::from_raw_parts(ssid, ssid_len) };
Some(slice.to_vec())
};
for entry in registry.iter() {
if entry.wiphy != wiphy as usize {
continue;
}
if let Some(ref wb) = want_bssid {
if entry.bssid != *wb {
continue;
}
}
if let Some(ref ws) = want_ssid {
if entry.ies.is_null() || entry.ies_len < ws.len() + 2 {
continue;
}
unsafe {
let ies_slice = std::slice::from_raw_parts(entry.ies, entry.ies_len);
let mut offset = 0;
let mut found = false;
while offset + 2 <= ies_slice.len() {
let tag_id = ies_slice[offset];
let tag_len = ies_slice[offset + 1] as usize;
if offset + 2 + tag_len > ies_slice.len() {
break;
}
if tag_id == 0 && tag_len == ws.len() {
if &ies_slice[offset + 2..offset + 2 + tag_len] == ws.as_slice() {
found = true;
break;
}
}
offset += 2 + tag_len;
}
if !found {
continue;
}
}
}
return entry.as_ref() as *const Cfg80211Bss as *mut Cfg80211Bss;
}
ptr::null_mut()
}
@@ -492,11 +681,26 @@ pub extern "C" fn cfg80211_rx_mgmt(
return;
}
let frame_data = if !buf.is_null() && len > 0 {
unsafe { std::slice::from_raw_parts(buf, len) }.to_vec()
} else {
Vec::new()
};
update_event_state(wdev as usize, |state| {
state.mgmt_rx_freq = freq;
state.mgmt_rx_signal = sig_dbm;
state.mgmt_rx_len = len;
state.mgmt_rx_data = frame_data;
});
log::debug!(
"cfg80211_rx_mgmt: wdev={:#x} freq={} sig={} len={}",
wdev as usize,
freq,
sig_dbm,
len
);
}
#[no_mangle]
@@ -512,11 +716,26 @@ pub extern "C" fn cfg80211_mgmt_tx_status(
return;
}
let frame_data = if !buf.is_null() && len > 0 {
unsafe { std::slice::from_raw_parts(buf, len) }.to_vec()
} else {
Vec::new()
};
update_event_state(wdev as usize, |state| {
state.mgmt_tx_cookie = cookie;
state.mgmt_tx_len = len;
state.mgmt_tx_ack = ack;
state.mgmt_tx_data = frame_data;
});
log::debug!(
"cfg80211_mgmt_tx_status: wdev={:#x} cookie={} len={} ack={}",
wdev as usize,
cookie,
len,
ack
);
}
#[no_mangle]
@@ -706,8 +925,10 @@ mod tests {
.expect("wdev event state");
assert_eq!(wdev_state.mgmt_rx_freq, 2412);
assert_eq!(wdev_state.mgmt_rx_signal, -42);
assert_eq!(wdev_state.mgmt_rx_data, sta.to_vec());
assert_eq!(wdev_state.mgmt_tx_cookie, 99);
assert!(wdev_state.mgmt_tx_ack);
assert_eq!(wdev_state.mgmt_tx_data, sta.to_vec());
drop(events);
assert_eq!(ieee80211_channel_to_frequency(1, NL80211_BAND_2GHZ), 2412);
@@ -718,4 +939,64 @@ mod tests {
wiphy_free(wiphy);
free_netdev(dev);
}
#[test]
fn test_cfg80211_put_bss_removes_from_registry() {
let name = CString::new("wlan%d").expect("valid test CString");
let dev = alloc_netdev_mqs(0, name.as_ptr().cast::<u8>(), 0, None, 1, 1);
assert!(!dev.is_null());
let wiphy = wiphy_new_nm(ptr::null(), 32, ptr::null());
assert!(!wiphy.is_null());
let mut wdev = WirelessDev {
wiphy,
netdev: dev.cast::<c_void>(),
iftype: 0,
scan_in_flight: false,
scan_aborted: false,
connecting: false,
connected: false,
locally_generated: false,
last_status: 0,
last_reason: 0,
has_bssid: false,
last_bssid: [0; 6],
};
unsafe { (*dev).ieee80211_ptr = (&mut wdev as *mut WirelessDev).cast::<c_void>() };
let bssid: [u8; 6] = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff];
let ies = [1u8, 2, 3, 4];
let bss = cfg80211_inform_bss(
wiphy,
&mut wdev as *mut WirelessDev,
2412,
bssid.as_ptr(),
0,
0x0431,
100,
ies.as_ptr(),
ies.len(),
-50,
0,
);
assert!(!bss.is_null());
let found = cfg80211_get_bss(wiphy, 0, bssid.as_ptr(), ptr::null(), 0, 0, 0);
assert!(
!found.is_null(),
"BSS should be in registry after inform_bss"
);
cfg80211_put_bss(bss);
let after_put = cfg80211_get_bss(wiphy, 0, bssid.as_ptr(), ptr::null(), 0, 0, 0);
assert!(
after_put.is_null(),
"BSS should be removed from registry after put_bss"
);
wiphy_free(wiphy);
free_netdev(dev);
}
}
@@ -288,3 +288,85 @@ pub extern "C" fn flush_scheduled_work() {
};
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::AtomicUsize;
#[test]
fn alloc_and_destroy_workqueue() {
let name = b"test_wq\0";
let wq = alloc_workqueue(name.as_ptr(), 0, 2);
assert!(!wq.is_null());
destroy_workqueue(wq);
}
#[test]
fn destroy_workqueue_null_is_safe() {
destroy_workqueue(std::ptr::null_mut());
}
#[test]
fn queue_work_executes_function() {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
COUNTER.store(0, Ordering::Relaxed);
extern "C" fn work_fn(_work: *mut WorkStruct) {
COUNTER.fetch_add(1, Ordering::Relaxed);
}
let name = b"exec_wq\0";
let wq = alloc_workqueue(name.as_ptr(), 0, 2);
let mut work = WorkStruct {
func: Some(work_fn),
__opaque: [0u8; 64],
};
queue_work(wq, &mut work);
flush_workqueue(wq);
assert!(
COUNTER.load(Ordering::Relaxed) >= 1,
"work function should have executed"
);
destroy_workqueue(wq);
}
#[test]
fn queue_work_null_wq_returns_zero() {
let mut work = WorkStruct {
func: None,
__opaque: [0u8; 64],
};
assert_eq!(queue_work(std::ptr::null_mut(), &mut work), 0);
}
#[test]
fn schedule_work_uses_default_wq() {
static SCHED_COUNTER: AtomicUsize = AtomicUsize::new(0);
SCHED_COUNTER.store(0, Ordering::Relaxed);
extern "C" fn sched_fn(_work: *mut WorkStruct) {
SCHED_COUNTER.fetch_add(1, Ordering::Relaxed);
}
let mut work = WorkStruct {
func: Some(sched_fn),
__opaque: [0u8; 64],
};
schedule_work(&mut work);
flush_scheduled_work();
assert!(
SCHED_COUNTER.load(Ordering::Relaxed) >= 1,
"schedule_work should dispatch to default wq"
);
}
#[test]
fn flush_workqueue_null_is_safe() {
flush_workqueue(std::ptr::null_mut());
}
}