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:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user