diff --git a/aya/src/programs/tc.rs b/aya/src/programs/tc.rs index 5e54a946..5c70f40a 100644 --- a/aya/src/programs/tc.rs +++ b/aya/src/programs/tc.rs @@ -24,8 +24,8 @@ use crate::{ sys::{ BpfLinkCreateArgs, LinkTarget, NetlinkError, ProgQueryTarget, SyscallError, bpf_link_create, bpf_link_get_info_by_fd, bpf_link_update, bpf_prog_get_fd_by_id, - netlink_find_filter_with_name, netlink_qdisc_add_clsact, netlink_qdisc_attach, - netlink_qdisc_detach, + netlink_clsact_qdisc_exists, netlink_find_filter_with_name, netlink_qdisc_add_clsact, + netlink_qdisc_attach, netlink_qdisc_detach, }, util::{KernelVersion, ifindex_from_ifname, tc_handler_make}, }; @@ -602,3 +602,9 @@ fn qdisc_detach_program_fast( Ok(()) } + +/// Check if the `clasct` qdisc exists on the given interface. +pub fn clsact_qdisc_exists(if_name: &str) -> Result { + let if_index = ifindex_from_ifname(if_name)?; + unsafe { netlink_clsact_qdisc_exists(if_index as i32).map_err(TcError::NetlinkError) } +} diff --git a/aya/src/sys/netlink.rs b/aya/src/sys/netlink.rs index 02ae5ee7..d6a8fc28 100644 --- a/aya/src/sys/netlink.rs +++ b/aya/src/sys/netlink.rs @@ -15,8 +15,8 @@ use libc::{ AF_NETLINK, AF_UNSPEC, ETH_P_ALL, IFF_UP, IFLA_XDP, NETLINK_CAP_ACK, NETLINK_EXT_ACK, NETLINK_ROUTE, NLA_ALIGNTO, NLA_F_NESTED, NLA_TYPE_MASK, NLM_F_ACK, NLM_F_CREATE, NLM_F_DUMP, NLM_F_ECHO, NLM_F_EXCL, NLM_F_MULTI, NLM_F_REQUEST, NLMSG_DONE, NLMSG_ERROR, RTM_DELTFILTER, - RTM_GETTFILTER, RTM_NEWQDISC, RTM_NEWTFILTER, RTM_SETLINK, SOCK_RAW, SOL_NETLINK, getsockname, - nlattr, nlmsgerr, nlmsghdr, recv, send, setsockopt, sockaddr_nl, socket, + RTM_GETQDISC, RTM_GETTFILTER, RTM_NEWQDISC, RTM_NEWTFILTER, RTM_SETLINK, SOCK_RAW, SOL_NETLINK, + getsockname, nlattr, nlmsgerr, nlmsghdr, recv, send, setsockopt, sockaddr_nl, socket, }; use thiserror::Error; @@ -105,6 +105,47 @@ pub(crate) unsafe fn netlink_set_xdp_fd( Ok(()) } +pub(crate) unsafe fn netlink_clsact_qdisc_exists(if_index: i32) -> Result { + let sock = NetlinkSocket::open()?; + + let mut req = unsafe { mem::zeroed::() }; + + let nlmsg_len = mem::size_of::() + mem::size_of::(); + + req.header = nlmsghdr { + nlmsg_len: nlmsg_len as u32, + nlmsg_flags: (NLM_F_REQUEST | NLM_F_ACK | NLM_F_DUMP) as u16, + nlmsg_type: RTM_GETQDISC, + nlmsg_pid: 0, + nlmsg_seq: 1, + }; + req.tc_info.tcm_family = AF_UNSPEC as u8; + + sock.send(&bytes_of(&req)[..req.header.nlmsg_len as usize])?; + + for msg in sock.recv()? { + if msg.header.nlmsg_type != RTM_NEWQDISC { + continue; + } + + let tc_msg = unsafe { ptr::read_unaligned(msg.data.as_ptr() as *const tcmsg) }; + if tc_msg.tcm_ifindex != if_index { + continue; + } + + let attrs = parse_attrs(&msg.data[mem::size_of::()..]) + .map_err(|e| NetlinkError(NetlinkErrorInternal::NlAttrError(e)))?; + + if let Some(opts) = attrs.get(&(TCA_KIND as u16)) { + if opts.data == b"clsact\0" { + return Ok(true); + } + } + } + + Ok(false) +} + pub(crate) unsafe fn netlink_qdisc_add_clsact(if_index: i32) -> Result<(), NetlinkError> { let sock = NetlinkSocket::open()?; @@ -341,7 +382,13 @@ struct Request { struct TcRequest { header: nlmsghdr, tc_info: tcmsg, - attrs: [u8; 64], + // The buffer for attributes should be sized to hold at least 256 bytes, + // based on `CLS_BPF_NAME_LEN = 256` from the kernel: + // https://github.com/torvalds/linux/blob/02aee814/net/sched/cls_bpf.c#L28 + // We currently use around ~30 bytes of attributes in addition to name. + // Rather than picking a "right sized buffer" for the payload (which is of + // varying length anyway) we use the next largest power of 2. + attrs: [u8; 512], } struct NetlinkSocket { diff --git a/test/integration-ebpf/Cargo.toml b/test/integration-ebpf/Cargo.toml index b0573882..508cad87 100644 --- a/test/integration-ebpf/Cargo.toml +++ b/test/integration-ebpf/Cargo.toml @@ -91,3 +91,11 @@ path = "src/xdp_sec.rs" [[bin]] name = "uprobe_cookie" path = "src/uprobe_cookie.rs" + +[[bin]] +name = "tc_name_limit" +path = "src/tc_name_limit.rs" + +[[bin]] +name = "tc_name_limit_exceeded" +path = "src/tc_name_limit_exceeded.rs" diff --git a/test/integration-ebpf/src/tc_name_limit.rs b/test/integration-ebpf/src/tc_name_limit.rs new file mode 100644 index 00000000..39afca7a --- /dev/null +++ b/test/integration-ebpf/src/tc_name_limit.rs @@ -0,0 +1,22 @@ +#![no_std] +#![no_main] + +use aya_ebpf::{macros::classifier, programs::TcContext}; + +// A function with a 256-byte-long name (all 'a's) to be used as the name of +// the ebpf program. This name must match the name passed to userspace side +// of the program (i.e. test/integration-test/src/tests/load.rs). +// 256 is the maximum length allowed by the kernel, so this test should pass. +// https://github.com/torvalds/linux/blob/02aee814/net/sched/cls_bpf.c#L28 +#[classifier] +pub fn aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa( + _ctx: TcContext, +) -> i32 { + 0 +} + +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/test/integration-ebpf/src/tc_name_limit_exceeded.rs b/test/integration-ebpf/src/tc_name_limit_exceeded.rs new file mode 100644 index 00000000..ec92a71d --- /dev/null +++ b/test/integration-ebpf/src/tc_name_limit_exceeded.rs @@ -0,0 +1,22 @@ +#![no_std] +#![no_main] + +use aya_ebpf::{macros::classifier, programs::TcContext}; + +// A function with a 257-byte-long name (all 'a's) to be used as the name of +// the ebpf program. This name must match the name passed to userspace side +// of the program (i.e. test/integration-test/src/tests/load.rs). +// 256 is the maximum length allowed by the kernel, so this test should fail. +// https://github.com/torvalds/linux/blob/02aee814/net/sched/cls_bpf.c#L28 +#[classifier] +pub fn aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa( + _ctx: TcContext, +) -> i32 { + 0 +} + +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/test/integration-test/src/lib.rs b/test/integration-test/src/lib.rs index f7cc30c6..56bb9d91 100644 --- a/test/integration-test/src/lib.rs +++ b/test/integration-test/src/lib.rs @@ -55,6 +55,8 @@ bpf_file!( TWO_PROGS => "two_progs", XDP_SEC => "xdp_sec", UPROBE_COOKIE => "uprobe_cookie", + TC_NAME_LIMIT_TEST => "tc_name_limit", + TC_NAME_LIMIT_EXCEEDED_TEST => "tc_name_limit_exceeded", ); #[cfg(test)] diff --git a/test/integration-test/src/tests/load.rs b/test/integration-test/src/tests/load.rs index 984d12a4..328fc792 100644 --- a/test/integration-test/src/tests/load.rs +++ b/test/integration-test/src/tests/load.rs @@ -1,12 +1,15 @@ use std::{convert::TryInto as _, fs::remove_file, path::Path, thread, time::Duration}; +use assert_matches::assert_matches; use aya::{ Ebpf, maps::Array, programs::{ - FlowDissector, KProbe, TracePoint, UProbe, Xdp, XdpFlags, + FlowDissector, KProbe, ProgramError, SchedClassifier, TcAttachType, TcError, TracePoint, + UProbe, Xdp, XdpFlags, links::{FdLink, PinnedLink}, loaded_links, loaded_programs, + tc::{clsact_qdisc_exists, qdisc_add_clsact}, }, util::KernelVersion, }; @@ -605,3 +608,51 @@ fn pin_lifecycle_uprobe() { // Make sure the function isn't optimized out. uprobe_function(); } + +#[test] +fn tc_name_limit() { + let clsact_exists = clsact_qdisc_exists("lo").unwrap(); + if !clsact_exists { + qdisc_add_clsact("lo").unwrap(); + } + + let mut bpf = Ebpf::load(crate::TC_NAME_LIMIT_TEST).unwrap(); + + let long_name = "a".repeat(256); + + let program: &mut SchedClassifier = bpf + .program_mut(long_name.as_str()) + .unwrap() + .try_into() + .unwrap(); + program.load().unwrap(); + program.attach("lo", TcAttachType::Ingress).unwrap(); +} + +#[test] +fn tc_name_limit_exceeded() { + let clsact_exists = clsact_qdisc_exists("lo").unwrap(); + if !clsact_exists { + qdisc_add_clsact("lo").unwrap(); + } + + let mut bpf = Ebpf::load(crate::TC_NAME_LIMIT_EXCEEDED_TEST).unwrap(); + + let long_name = "a".repeat(257); + + let program: &mut SchedClassifier = bpf + .program_mut(long_name.as_str()) + .unwrap() + .try_into() + .unwrap(); + program.load().unwrap(); + + assert_matches!( + program.attach("lo", TcAttachType::Ingress), + Err(ProgramError::TcError(TcError::NetlinkError())) => { + // An invalid argument error (EINVAL) with code 22 should occur. + // The invalid argument is the tc program name which is too long. + assert_eq!(io_error.raw_os_error(), Some(22)) + } + ); +} diff --git a/xtask/public-api/aya.txt b/xtask/public-api/aya.txt index a0d9277f..85b185ea 100644 --- a/xtask/public-api/aya.txt +++ b/xtask/public-api/aya.txt @@ -6477,6 +6477,7 @@ impl core::borrow::BorrowMut for aya::programs::tc::SchedClassifierLinkId pub fn aya::programs::tc::SchedClassifierLinkId::borrow_mut(&mut self) -> &mut T impl core::convert::From for aya::programs::tc::SchedClassifierLinkId pub fn aya::programs::tc::SchedClassifierLinkId::from(t: T) -> T +pub fn aya::programs::tc::clsact_qdisc_exists(if_name: &str) -> core::result::Result pub fn aya::programs::tc::qdisc_add_clsact(if_name: &str) -> core::result::Result<(), aya::programs::tc::TcError> pub fn aya::programs::tc::qdisc_detach_program(if_name: &str, attach_type: aya::programs::tc::TcAttachType, name: &str) -> core::result::Result<(), aya::programs::tc::TcError> pub mod aya::programs::tp_btf @@ -10429,4 +10430,4 @@ impl aya::Pod for [T; N] pub fn aya::features() -> &'static aya_obj::obj::Features pub type aya::Bpf = aya::Ebpf pub type aya::BpfError = aya::EbpfError -pub type aya::BpfLoader<'a> = aya::EbpfLoader<'a> +pub type aya::BpfLoader<'a> = aya::EbpfLoader<'a> \ No newline at end of file