Skip to main content

strat9_kernel/hardware/nic/
virtio_net.rs

1//! VirtIO Network Device driver
2//!
3//! Provides network I/O via VirtIO-net protocol for QEMU/KVM environments.
4//! Implements the common [`crate::hardware::nic::NetworkDevice`] trait so
5//! this driver plugs into the unified `/dev/net/` scheme.
6//!
7//! Reference: VirtIO spec v1.2, Section 5.1 (Network Device)
8//! https://docs.oasis-open.org/virtio/virtio/v1.4/cs01/virtio-v1.4-cs01.html#x1-2700001
9
10use crate::{
11    arch::pci::{self, PciDevice},
12    hardware::{
13        nic as net,
14        virtio::{
15            common::{VirtioDevice, Virtqueue},
16            status,
17        },
18    },
19    memory::{self, PhysFrame},
20    sync::{FixedQueue, SpinLock},
21};
22use alloc::sync::Arc;
23use core::{mem, ptr, sync::atomic::Ordering};
24use endian_num::Le;
25use net_core::{NetError, NetworkDevice};
26use spin::RwLock as SpinRwLock;
27
28/// VirtIO net header size (12 bytes with MRG_RXBUF, 10 bytes without).
29/// Determined at runtime during feature negotiation.
30static NET_HDR_SIZE: core::sync::atomic::AtomicUsize =
31    core::sync::atomic::AtomicUsize::new(mem::size_of::<VirtioNetHeader>());
32const RX_FRAME_TRACK_CAPACITY: usize = 128;
33const TX_FRAME_TRACK_CAPACITY: usize = 128;
34
35/// VirtIO net device features
36pub mod features {
37    pub const VIRTIO_NET_F_CSUM: u32 = 1 << 0;
38    pub const VIRTIO_NET_F_GUEST_CSUM: u32 = 1 << 1;
39    pub const VIRTIO_NET_F_MAC: u32 = 1 << 5;
40    pub const VIRTIO_NET_F_GSO: u32 = 1 << 6;
41    pub const VIRTIO_NET_F_GUEST_TSO4: u32 = 1 << 7;
42    pub const VIRTIO_NET_F_GUEST_TSO6: u32 = 1 << 8;
43    pub const VIRTIO_NET_F_GUEST_ECN: u32 = 1 << 9;
44    pub const VIRTIO_NET_F_GUEST_UFO: u32 = 1 << 10;
45    pub const VIRTIO_NET_F_HOST_TSO4: u32 = 1 << 11;
46    pub const VIRTIO_NET_F_HOST_TSO6: u32 = 1 << 12;
47    pub const VIRTIO_NET_F_HOST_ECN: u32 = 1 << 13;
48    pub const VIRTIO_NET_F_HOST_UFO: u32 = 1 << 14;
49    pub const VIRTIO_NET_F_MRG_RXBUF: u32 = 1 << 15;
50    pub const VIRTIO_NET_F_STATUS: u32 = 1 << 16;
51    pub const VIRTIO_NET_F_CTRL_VQ: u32 = 1 << 17;
52    pub const VIRTIO_NET_F_CTRL_RX: u32 = 1 << 18;
53    pub const VIRTIO_NET_F_CTRL_VLAN: u32 = 1 << 19;
54    pub const VIRTIO_NET_F_GUEST_ANNOUNCE: u32 = 1 << 21;
55    pub const VIRTIO_NET_F_MQ: u32 = 1 << 22;
56}
57
58/// VirtIO net status flags
59pub mod net_status {
60    pub const VIRTIO_NET_S_LINK_UP: u16 = 1;
61    pub const VIRTIO_NET_S_ANNOUNCE: u16 = 2;
62}
63
64/// VirtIO net header (prepended to every packet)
65///
66/// Fields are little-endian as mandated by the VirtIO spec
67/// https://docs.oasis-open.org/virtio/virtio/v1.4/virtio-v1.4.html#x1-2810006
68#[repr(C)]
69#[derive(Debug, Clone, Copy, Default)]
70pub struct VirtioNetHeader {
71    pub flags: u8,
72    pub gso_type: u8,
73    pub hdr_len: Le<u16>,
74    pub gso_size: Le<u16>,
75    pub csum_start: Le<u16>,
76    pub csum_offset: Le<u16>,
77    pub num_buffers: Le<u16>,
78}
79
80/// VirtIO Network Device driver
81pub struct VirtioNetDevice {
82    device: VirtioDevice,
83    rx_queue: SpinLock<Virtqueue>,
84    tx_queue: SpinLock<Virtqueue>,
85    mac_address: [u8; 6],
86    pub rx_frames: SpinLock<FixedQueue<(PhysFrame, u8), RX_FRAME_TRACK_CAPACITY>>,
87    tx_frames: SpinLock<FixedQueue<(PhysFrame, u8), TX_FRAME_TRACK_CAPACITY>>,
88}
89
90// Send and Sync are safe because we use SpinLocks
91unsafe impl Send for VirtioNetDevice {}
92unsafe impl Sync for VirtioNetDevice {}
93
94/// Read 6 bytes at `offset` in the device-config window and accept them only
95/// if they look like a unicast MAC (not an all-ones/all-zeros unmapped read).
96fn read_plausible_mac(device: &VirtioDevice, offset: u16, out: &mut [u8; 6]) -> bool {
97    for i in 0..6 {
98        out[i] = device.read_reg_u8(offset + i as u16);
99    }
100    *out != [0xFF; 6] && *out != [0x00; 6] && (out[0] & 0x01) == 0
101}
102
103impl VirtioNetDevice {
104    /// Initialize a VirtIO network device from a PCI device
105    pub unsafe fn new(pci_dev: PciDevice) -> Result<Self, &'static str> {
106        log::info!("VirtIO-net: Initializing device at {:?}", pci_dev.address);
107
108        // Create VirtIO device
109        let mut device = VirtioDevice::new(pci_dev)?;
110
111        // Reset device
112        device.reset();
113
114        // Acknowledge device
115        device.add_status(status::ACKNOWLEDGE as u8);
116
117        // Indicate we know how to drive it
118        device.add_status(status::DRIVER as u8);
119
120        // Read and negotiate features
121        let device_features = device.read_device_features();
122        let needed = features::VIRTIO_NET_F_MAC | features::VIRTIO_NET_F_STATUS;
123        // VIRTIO_NET_F_MRG_RXBUF: requested so the device uses the 12-byte
124        // virtio_net_hdr_v1 layout (with num_buffers) that matches our
125        // VirtioNetHeader struct. Without it the legacy 10-byte header would
126        // shift every packet by 2 bytes, corrupting all data.
127        let desired = needed | features::VIRTIO_NET_F_MRG_RXBUF;
128        if device_features & needed != needed {
129            return Err("Device lacks mandatory MAC/STATUS features");
130        }
131        let guest_features = device_features & desired;
132        device.write_guest_features(guest_features);
133
134        // Features OK
135        device.add_status(status::FEATURES_OK as u8);
136
137        // Double-check that FEATURES_OK stuck
138        if device.get_status() & (status::FEATURES_OK as u8) == 0 {
139            return Err("Device rejected our feature set");
140        }
141
142        // Read back negotiated features to determine actual header size.
143        // With VIRTIO_NET_F_MRG_RXBUF the header is 12 bytes (virtio_net_hdr_v1);
144        // without it the legacy 10-byte header (virtio_net_hdr) is used.
145        let negotiated = device.read_device_features();
146        if negotiated & features::VIRTIO_NET_F_MRG_RXBUF != 0 {
147            NET_HDR_SIZE.store(mem::size_of::<VirtioNetHeader>(), Ordering::Release);
148        } else {
149            // Legacy 10-byte header: num_buffers field is absent.
150            NET_HDR_SIZE.store(10, Ordering::Release);
151        }
152
153        // Create virtqueues
154        // Queue 0: RX (receive)
155        // Queue 1: TX (transmit)
156        let rx_queue = Virtqueue::new(128)?;
157        let tx_queue = Virtqueue::new(128)?;
158
159        // Setup queues with device
160        device.setup_queue(0, &rx_queue);
161        device.setup_queue(1, &tx_queue);
162
163        // Diagnostics: the legacy device-config window starts at io_base+0x14.
164        // An unmapped window reads back 0xFF, which is how a wrong BAR0
165        // shows up as a corrupted MAC.
166        log::info!(
167            "VirtIO-net: io_base={:#06x} raw[0x14..0x1c]={:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} link={:#06x}",
168            device.io_base,
169            device.read_reg_u8(0x14),
170            device.read_reg_u8(0x15),
171            device.read_reg_u8(0x16),
172            device.read_reg_u8(0x17),
173            device.read_reg_u8(0x18),
174            device.read_reg_u8(0x19),
175            device.read_reg_u8(0x1a),
176            device.read_reg_u8(0x1b),
177            device.read_reg_u16(0x1a),
178        );
179
180        // Read MAC address from device config space.
181        //
182        // Legacy layout puts MAC at io_base+0x14, but a transitional device
183        // with a shifted decode window returns 0xFF there while the real MAC
184        // sits 4 bytes later. Probe both and keep the first plausible one.
185        let mut mac_address = [0u8; 6];
186        let mut mac_offset = 0x14u16;
187        if !read_plausible_mac(&device, 0x14, &mut mac_address) {
188            if read_plausible_mac(&device, 0x18, &mut mac_address) {
189                mac_offset = 0x18;
190                log::warn!(
191                    "VirtIO-net: MAC at io_base+0x18 (window shifted by 4) - transitional layout"
192                );
193                device.io_base = device.io_base.wrapping_add(4);
194            } else {
195                log::error!(
196                    "VirtIO-net: MAC unreadable at io_base+0x14 and +0x18 (all 0xFF/0x00)"
197                );
198                return Err("device config window unreadable (MAC)");
199            }
200        }
201
202        log::info!(
203            "VirtIO-net: MAC address: {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x} (offset {:#x})",
204            mac_address[0],
205            mac_address[1],
206            mac_address[2],
207            mac_address[3],
208            mac_address[4],
209            mac_address[5],
210            mac_offset
211        );
212
213        // Driver ready: only now does the device start reporting link state.
214        device.add_status(status::DRIVER_OK as u8);
215
216        log::info!(
217            "VirtIO-net: post-DRIVER_OK link status={:#06x} up={}",
218            device.read_reg_u16(0x1a),
219            device.read_reg_u16(0x1a) & net_status::VIRTIO_NET_S_LINK_UP as u16 != 0
220        );
221
222        let net_device = Self {
223            device,
224            rx_queue: SpinLock::new(rx_queue),
225            tx_queue: SpinLock::new(tx_queue),
226            mac_address,
227            rx_frames: SpinLock::new(FixedQueue::new()),
228            tx_frames: SpinLock::new(FixedQueue::new()),
229        };
230
231        // Fill RX queue with buffers
232        net_device.refill_rx_queue()?;
233
234        Ok(net_device)
235    }
236
237    /// Fill the RX queue with receive buffers
238    fn refill_rx_queue(&self) -> Result<(), &'static str> {
239        let mut rx_queue = self.rx_queue.lock();
240        let mut rx_frames = self.rx_frames.lock();
241
242        // We want to keep some buffers in the RX queue
243        let current_filled = rx_frames.len();
244        let target_filled = 64;
245        let mut added = 0usize;
246
247        if current_filled >= target_filled {
248            return Ok(());
249        }
250
251        for _ in 0..(target_filled - current_filled) {
252            // Allocate buffer for header + MTU
253            let buf_size = NET_HDR_SIZE.load(Ordering::Relaxed) + net::MTU;
254            let buf_pages = (buf_size + 4095) / 4096;
255            let buf_order = buf_pages.next_power_of_two().trailing_zeros() as u8;
256
257            let buf_frame = match crate::sync::with_irqs_disabled(|token| {
258                memory::allocate_phys_contiguous(token, buf_order)
259            }) {
260                Ok(frame) => frame,
261                Err(_) => break, // No more memory available
262            };
263
264            let buf_addr = buf_frame.start_address.as_u64();
265            let virt_addr = crate::memory::phys_to_virt(buf_addr);
266
267            // Zero the buffer (header needs to be zeroed mostly)
268            unsafe {
269                ptr::write_bytes(virt_addr as *mut u8, 0, buf_size);
270            }
271
272            // Add buffer to RX queue (device Writable)
273            match rx_queue.add_buffer(&[(buf_addr, buf_size as u32, true)]) {
274                Ok(_) => {
275                    if rx_frames.push_back((buf_frame, buf_order)).is_err() {
276                        crate::sync::with_irqs_disabled(|token| {
277                            memory::free_phys_contiguous(token, buf_frame, buf_order);
278                        });
279                        break;
280                    }
281                    added += 1;
282                }
283                Err(_) => {
284                    // Queue full, free the buffer
285                    crate::sync::with_irqs_disabled(|token| {
286                        memory::free_phys_contiguous(token, buf_frame, buf_order);
287                    });
288                    break;
289                }
290            }
291        }
292
293        // Notify device about new RX buffers
294        if rx_queue.should_notify() {
295            self.device.notify_queue(0);
296        }
297
298        if rx_frames.is_empty() && current_filled == 0 && added == 0 {
299            return Err("Failed to allocate RX buffers");
300        }
301
302        Ok(())
303    }
304
305    /// Read link status from device
306    fn read_link_status(&self) -> u16 {
307        // Status is at offset 6 in device-specific config (offset 20 + 6 = 26)
308        self.device.read_reg_u16(26)
309    }
310}
311
312impl NetworkDevice for VirtioNetDevice {
313    /// Performs the name operation.
314    fn name(&self) -> &str {
315        "virtio-net"
316    }
317
318    /// Performs the receive operation.
319    fn receive(&self, buf: &mut [u8]) -> Result<usize, NetError> {
320        let mut rx_queue = self.rx_queue.lock();
321
322        // Check if there's a used buffer
323        if !rx_queue.has_used() {
324            return Err(NetError::NoPacket);
325        }
326
327        // Claim the backing frame BEFORE consuming the used entry: the used
328        // ring and rx_frames are two parallel FIFOs, so the n-th consumed
329        // descriptor owns the n-th frame. Consuming first would free a
330        // descriptor with no buffer to match it.
331        let (frame, order) = match self.rx_frames.lock().pop_front() {
332            Some(f) => f,
333            None => {
334                // Descriptors are still queued but no buffer tracks them:
335                // do not consume the used entry, or the rings desynchronise.
336                log::warn!("[vtnet] rx: used entry without tracking frame");
337                return Err(NetError::NotReady);
338            }
339        };
340
341        let hdr_size = NET_HDR_SIZE.load(Ordering::Relaxed);
342        let (token, len) = match rx_queue.get_used() {
343            Some(v) => v,
344            None => {
345                crate::sync::with_irqs_disabled(|t| {
346                    memory::free_phys_contiguous(t, frame, order);
347                });
348                return Err(NetError::NoPacket);
349            }
350        };
351
352        let _desc_index = token as usize;
353        let _desc_table = rx_queue.desc_area(); // Physical address
354
355        let buf_addr = frame.start_address.as_u64();
356        let virt_addr = crate::memory::phys_to_virt(buf_addr);
357
358        let header_ptr = virt_addr as *const VirtioNetHeader;
359        let data_ptr = (virt_addr + hdr_size as u64) as *const u8;
360
361        let header = unsafe { ptr::read(header_ptr) };
362        let packet_len = (len as usize).saturating_sub(hdr_size);
363
364        log::trace!(
365            "[vtnet] rx: token={} len={} pkt={} flags={}",
366            token,
367            len,
368            packet_len,
369            header.flags,
370        );
371
372        if buf.len() < packet_len {
373            // Buffer too small, packet lost
374            crate::sync::with_irqs_disabled(|token| {
375                memory::free_phys_contiguous(token, frame, order);
376            });
377            drop(rx_queue);
378            // We still need to refill.
379            let _ = self.refill_rx_queue();
380            return Err(NetError::BufferTooSmall);
381        }
382
383        // Copy packet data
384        if packet_len > 0 {
385            unsafe {
386                ptr::copy_nonoverlapping(data_ptr, buf.as_mut_ptr(), packet_len);
387            }
388        }
389
390        // Free the frame
391        crate::sync::with_irqs_disabled(|token| {
392            memory::free_phys_contiguous(token, frame, order);
393        });
394        drop(rx_queue);
395
396        // Refill RX queue
397        let _ = self.refill_rx_queue();
398
399        Ok(packet_len)
400    }
401
402    /// Performs the transmit operation.
403    fn transmit(&self, buf: &[u8]) -> Result<(), NetError> {
404        if buf.len() > net::MTU {
405            return Err(NetError::BufferTooSmall);
406        }
407
408        // Allocate TX buffer (header + data)
409        let buf_size = NET_HDR_SIZE.load(Ordering::Relaxed) + buf.len();
410        let buf_pages = (buf_size + 4095) / 4096;
411        let buf_order = buf_pages.next_power_of_two().trailing_zeros() as u8;
412
413        let buf_frame = crate::sync::with_irqs_disabled(|token| {
414            memory::allocate_phys_contiguous(token, buf_order)
415        })
416        .map_err(|_| NetError::NotReady)?;
417
418        let buf_addr = buf_frame.start_address.as_u64();
419        let virt_addr = crate::memory::phys_to_virt(buf_addr);
420
421        let header_ptr = virt_addr as *mut VirtioNetHeader;
422        let data_ptr = (virt_addr + NET_HDR_SIZE.load(Ordering::Relaxed) as u64) as *mut u8;
423
424        // Write header
425        unsafe {
426            ptr::write(header_ptr, VirtioNetHeader::default());
427            ptr::copy_nonoverlapping(buf.as_ptr(), data_ptr, buf.len());
428        }
429
430        // Submit to TX queue
431        let mut tx_queue = self.tx_queue.lock();
432
433        // Reclaim completed TX buffers before submitting.
434        // The used ring is FIFO so draining here frees exactly the frames
435        // that were pushed earlier in the same order.
436        while let Some((_token, _len)) = tx_queue.get_used() {
437            if let Some((_frame, order)) = self.tx_frames.lock().pop_front() {
438                crate::sync::with_irqs_disabled(|token| {
439                    memory::free_phys_contiguous(token, _frame, order);
440                });
441            }
442        }
443
444        let head = tx_queue
445            .add_buffer(&[(buf_addr, buf_size as u32, false)]) // Device Readable
446            .map_err(|_| {
447                // Free buffer if queue is full
448                crate::sync::with_irqs_disabled(|token| {
449                    memory::free_phys_contiguous(token, buf_frame, buf_order);
450                });
451                NetError::TxQueueFull
452            })?;
453
454        if let Err(_) = self.tx_frames.lock().push_back((buf_frame, buf_order)) {
455            // Tracking queue full : free the frame we just submitted
456            // (the descriptor is already in the available ring but the
457            // device hasn't seen it yet; get_used will reclaim it later
458            // and we won't be able to free it.  This is a safety net.)
459            crate::sync::with_irqs_disabled(|token| {
460                memory::free_phys_contiguous(token, buf_frame, buf_order);
461            });
462        }
463
464        log::trace!("[vtnet] tx: submit {} bytes @ {:#x}", buf_size, buf_addr);
465
466        if tx_queue.should_notify() {
467            self.device.notify_queue(1);
468        }
469        drop(tx_queue);
470
471        Ok(())
472    }
473
474    /// Performs the mac address operation.
475    fn mac_address(&self) -> [u8; 6] {
476        self.mac_address
477    }
478
479    /// Performs the link up operation.
480    fn link_up(&self) -> bool {
481        let status = self.read_link_status();
482        status & net_status::VIRTIO_NET_S_LINK_UP != 0
483    }
484
485    /// Ack the PCI/virtio ISR so the IRQ line can fire again.
486    /// RX/TX ring draining is done by `nic::handle_interrupt()`.
487    fn handle_interrupt(&self) {
488        if self.device.read_isr_status() == 0 {
489            return;
490        }
491        self.device.ack_interrupt();
492    }
493
494    /// Reclaim completed TX buffers and top up RX when polled without IRQ.
495    fn poll(&self) {
496        loop {
497            let used = {
498                let mut tx_queue = self.tx_queue.lock();
499                tx_queue.get_used()
500            };
501            let Some((_token, _len)) = used else {
502                break;
503            };
504            if let Some((_frame, order)) = self.tx_frames.lock().pop_front() {
505                crate::sync::with_irqs_disabled(|token| {
506                    memory::free_phys_contiguous(token, _frame, order);
507                });
508            }
509        }
510        let _ = self.refill_rx_queue();
511    }
512}
513
514/// Global VirtIO network device
515static VIRTIO_NET: SpinRwLock<Option<Arc<VirtioNetDevice>>> = SpinRwLock::new(None);
516
517/// Initialize VirtIO network device and register it in the global net registry.
518///
519/// Idempotent: `hardware::init()` and the component `nic_init` both call this;
520/// a second probe would reset the same PCI device and corrupt the first handle.
521pub fn init() {
522    if VIRTIO_NET.read().is_some() {
523        return;
524    }
525
526    log::info!("VirtIO-net: Scanning for devices...");
527
528    // Prefer strict class-based probe (network/ethernet), with fallback to
529    // vendor+device for odd firmware/virtual setups.
530    let pci_dev = match pci::probe_first(pci::ProbeCriteria {
531        vendor_id: Some(pci::vendor::VIRTIO),
532        device_id: Some(pci::device::VIRTIO_NET),
533        class_code: Some(pci::class::NETWORK),
534        subclass: Some(pci::net_subclass::ETHERNET),
535        prog_if: None,
536    })
537    .or_else(|| pci::find_virtio_device(pci::device::VIRTIO_NET))
538    {
539        Some(dev) => dev,
540        None => {
541            log::warn!("VirtIO-net: No network device found");
542            return;
543        }
544    };
545
546    // Enable MSI-X => MSI => INTx before the device value is moved into
547    // VirtioNetDevice::new(). Without this the N2 data plane never sees
548    // RX/TX and DHCP can never complete.
549    // msi::probe_and_enable takes hardware::pci_client::PciDevice (same as e1000).
550    let client_dev = crate::hardware::pci_client::PciDevice {
551        address: crate::hardware::pci_client::PciAddress::new(
552            pci_dev.address.bus,
553            pci_dev.address.device,
554            pci_dev.address.function,
555        ),
556        vendor_id: pci_dev.vendor_id,
557        device_id: pci_dev.device_id,
558        class_code: pci_dev.class_code,
559        subclass: pci_dev.subclass,
560        prog_if: pci_dev.prog_if,
561        revision: pci_dev.revision,
562        header_type: pci_dev.header_type,
563        interrupt_line: pci_dev.interrupt_line,
564        interrupt_pin: pci_dev.interrupt_pin,
565    };
566    let (irq, vector) = crate::arch::msi::probe_and_enable(&client_dev, true);
567    let msi_active =
568        (client_dev.read_config_u16(pci::config::COMMAND) & pci::command::INTERRUPT_DISABLE) != 0;
569
570    // Retry once like the e1000 driver: a stale INTERRUPT_DISABLE or a
571    // missing bus-master bit from firmware makes the first attempt read back
572    // an unmapped device-config window.
573    let mut device = None;
574    for attempt in 1..=2u32 {
575        match unsafe { VirtioNetDevice::new(pci_dev) } {
576            Ok(dev) => {
577                log::info!("VirtIO-net: init ok on attempt {}", attempt);
578                device = Some(dev);
579                break;
580            }
581            Err(e) => {
582                log::warn!("VirtIO-net: init attempt {} failed: {}", attempt, e);
583                if attempt == 1 {
584                    let mut cmd = client_dev.read_config_u16(pci::config::COMMAND);
585                    cmd |= pci::command::BUS_MASTER | pci::command::IO_SPACE;
586                    cmd &= !pci::command::INTERRUPT_DISABLE;
587                    client_dev.write_config_u16(pci::config::COMMAND, cmd);
588                    log::info!(
589                        "VirtIO-net: reprogrammed PCI COMMAND={:#06x} (bus_master|io_space, intx enabled)",
590                        cmd
591                    );
592                }
593            }
594        }
595    }
596
597    match device {
598        Some(device) => {
599            let arc = Arc::new(device);
600            *VIRTIO_NET.write() = Some(arc.clone());
601            let iface = net::register_device(arc.clone());
602
603            // link_up() reads device config on every call, so the value
604            // logged here reflects the post-negotiation state.
605            log::info!(
606                "[VirtIO-net] {}: link_up={} status_raw={:#06x}",
607                iface,
608                arc.link_up(),
609                arc.read_link_status()
610            );
611
612            if msi_active {
613                // MSI delivers to the vector programmed by probe_and_enable.
614                // When interrupt_line is 0/0xFF that vector is still valid (≥ 0x20).
615                crate::arch::idt::register_nic_irq(vector);
616                let irq_for_eoi = if irq != 0 && irq != 0xFF { irq } else { vector };
617                net::set_nic_device(arc, irq_for_eoi);
618                log::info!(
619                    "[VirtIO-net] {}: MSI/MSI-X active on vector {:#x}",
620                    iface,
621                    vector
622                );
623            } else if irq == 0 || irq == 0xFF {
624                log::warn!(
625                    "[VirtIO-net] {}: no valid IRQ line, running in polling mode",
626                    iface
627                );
628            } else {
629                crate::arch::ioapic::route_nic_irq(irq, vector);
630                log::info!(
631                    "[VirtIO-net] {}: INTx IRQ {} => vector {:#x}",
632                    iface,
633                    irq,
634                    vector
635                );
636                crate::arch::idt::register_nic_irq(irq);
637                net::set_nic_device(arc, irq);
638            }
639        }
640        None => {
641            log::error!("VirtIO-net: failed to initialize device after retries");
642        }
643    }
644}
645
646/// Get the VirtIO network device instance (if present).
647pub fn get_device() -> Option<Arc<VirtioNetDevice>> {
648    VIRTIO_NET.read().clone()
649}