descriptor marked as invalid, tx_ring_empty_descs

Nahla <[email protected]> Fri, 09 Jan 2026 21:42:35 +0000
Newsgroups org.kernel.vger.xdp-newbies
Message-ID <ZalFnB0f9nzP17TjeXcgI8_2dj1pDZrTMxeeQVi5v93LkbdsrMXwWl6OdWRS8vHHINVAZNL2VjKN3ko9KKkCnGtlJyCYwYgu_TVKcRdN9g4=@protonmail.com>
Hello everyone,

I'm currently trying to figure out how to use an AF_XDP socket without rely=
ing on libxdp as I want to learn how it works under the hood. I'm having an=
 issue where the packet is seemingly getting swallowed up somewhere in the =
stack and I'm not sure why. I write a packet to the UMEM buffer's 0th chunk=
, fill out the 0th entry in the tx ring, and then increment the producer. H=
owever I don't see a packet come out at all at the end of that process and =
checking XDP_STATISTICS indicates that tx_ring_empty_descs was incremented =
by 1.

As it stands I'm creating a UMEM buffer with 4096 chunks, each being 4096 b=
ytes long, and when registering it I use no flags, headroom, or tx_metadata=
_len. There is one of each type of ring, and they all have a size of 512. W=
hen binding the socket I put it into XDP_COPY mode, bind it to the 0th queu=
e, and pass 0 for the UMEM fd. The actual code showing the write and send o=
peration is given below, thought it is written in Rust. I heavily commented=
 for the sake of anyone who might not know Rust. All types such as Umem are=
 more or less just thin wrappers around memory returned by libc/syscalls.

pub=C2=A0fn=C2=A0send(&mut=C2=A0self,=C2=A0data:=C2=A0&[u8],=C2=A0chunk_idx=
:=C2=A0usize)=C2=A0{
    // write data to the nth chunk of the umem buffer
=C2=A0 =C2=A0 self.umem[(Umem::CHUNK_SIZE=C2=A0*=C2=A0chunk_idx)..(Umem::CH=
UNK_SIZE=C2=A0*=C2=A0(chunk_idx+1))]
=C2=A0 =C2=A0 =C2=A0 =C2=A0 .copy_within(0..data.len(),=C2=A00);

=C2=A0 =C2=A0 // get the last known value of producer without interacting w=
ith the atomic's address
=C2=A0 =C2=A0 let=C2=A0entry_idx=C2=A0=3D=C2=A0self.tx.cached_prod as=C2=
=A0usize;
=C2=A0 =C2=A0 // get a reference to the entry indexed by the cached produce=
r
=C2=A0 =C2=A0 let=C2=A0tx_entry=C2=A0=3D=C2=A0self.tx.data.get_mut(entry_id=
x).expect("Failed to get first umem chunk");

=C2=A0 =C2=A0 // set the entry's addr field to the index nth chunk
=C2=A0 =C2=A0 tx_entry.addr =3D=C2=A0(Umem::CHUNK_SIZE=C2=A0*=C2=A0chunk_id=
x)=C2=A0as=C2=A0_;
=C2=A0 =C2=A0 // set the length to be that of the data we just wrote to ume=
m
=C2=A0 =C2=A0 tx_entry.len =3D=C2=A0data.len()=C2=A0as=C2=A0_;
=C2=A0 =C2=A0 // no flags/options
=C2=A0 =C2=A0 tx_entry.options =3D=C2=A00;

=C2=A0 =C2=A0 // store/release to the producer the value of cached_prod + 1
=C2=A0 =C2=A0 self.tx.producer.store(self.tx.cached_prod +=C2=A01,=C2=A0Ord=
ering::Release);
=C2=A0 =C2=A0 // updated cached_prod
=C2=A0 =C2=A0 self.tx.cached_prod +=3D=C2=A01;

=C2=A0 =C2=A0 // sendto call to let the kernel know we have something ready=
 to go out
=C2=A0 =C2=A0 unsafe=C2=A0{
=C2=A0 =C2=A0 =C2=A0 =C2=A0 let=C2=A0ret=C2=A0=3D=C2=A0libc::sendto(
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 self.fd.as_raw_fd(),
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 std::ptr::null(),
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 0,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 libc::MSG_DONTWAIT,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 std::ptr::null(),
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 0
=C2=A0 =C2=A0 =C2=A0 =C2=A0 );

=C2=A0 =C2=A0 =C2=A0 =C2=A0 // print the errno error string
=C2=A0 =C2=A0 =C2=A0 =C2=A0 if=C2=A0ret=C2=A0<=C2=A00=C2=A0{
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 println!("sendto issue: {}",=
=C2=A0ioError::last_os_error());
=C2=A0 =C2=A0 =C2=A0 =C2=A0 }
=C2=A0 =C2=A0 }
}

I have tried running this in XDP_ZEROCOPY mode as well with an Intel NIC us=
ing the igb driver, the end result is that the sendto call returns EINVAL r=
ather than marking it as an invalid descriptor. When running in XDP_COPY mo=
de sendto returns 0 and the behavior described above occurs.

Best,
Nahla