From aff1abb9d43f4dbcef904a5161d5dce55b1f4112 Mon Sep 17 00:00:00 2001 From: Victor Westerhuis Date: Fri, 7 Feb 2025 11:43:57 +0100 Subject: [PATCH] Fix parsing of abstract Unix socket addresses These start with a null byte, don't end with a null byte and can contain embedded null bytes. --- src/unix/nlas.rs | 79 ++++++++++++++++++++++++++++++++++++-------- src/unix/response.rs | 4 +-- src/unix/tests.rs | 71 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 135 insertions(+), 19 deletions(-) diff --git a/src/unix/nlas.rs b/src/unix/nlas.rs index bfbb33a..05f4dcb 100644 --- a/src/unix/nlas.rs +++ b/src/unix/nlas.rs @@ -1,22 +1,79 @@ // SPDX-License-Identifier: MIT +use std::{ + ffi::{CStr, OsString}, + os::unix::ffi::{OsStrExt, OsStringExt}, +}; + use anyhow::Context; use byteorder::{ByteOrder, NativeEndian}; use netlink_packet_utils::{ buffer, nla::{self, DefaultNla, NlaBuffer}, - parsers::{parse_string, parse_u32, parse_u8}, + parsers::{parse_u32, parse_u8}, traits::{Emitable, Parseable}, DecodeError, }; use crate::constants::*; +#[derive(Debug, Eq, PartialEq, Clone)] +pub enum UnixDiagName { + /// Filesystem pathname to which the socket was bound. + Pathname(OsString), + /// Abstract socket address to which the socket was bound. + Abstract(Vec), +} + +impl> Parseable for UnixDiagName { + fn parse(buf: &T) -> Result { + let buf = buf.as_ref(); + if let Some((0, address)) = buf.split_first() { + Ok(UnixDiagName::Abstract(address.to_owned())) + } else { + Ok(UnixDiagName::Pathname(OsString::from_vec( + CStr::from_bytes_with_nul(buf) + .context("pathname is not null-terminated")? + .to_owned() + .into(), + ))) + } + } +} + +impl Emitable for UnixDiagName { + fn buffer_len(&self) -> usize { + match self { + UnixDiagName::Pathname(pathname) => pathname.len() + 1, + UnixDiagName::Abstract(address) => address.len() + 1, + } + } + + fn emit(&self, buffer: &mut [u8]) { + match self { + UnixDiagName::Pathname(pathname) => { + let (last, first) = buffer + .split_last_mut() + .expect("buffer should not be empty"); + first.copy_from_slice(pathname.as_bytes()); + *last = 0; + } + UnixDiagName::Abstract(address) => { + let (first, last) = buffer + .split_first_mut() + .expect("buffer should not be empty"); + *first = 0; + last.copy_from_slice(address); + } + } + } +} + #[derive(Debug, Eq, PartialEq, Clone)] pub enum Nla { - /// Path to which the socket was bound. This attribute is known as + /// Name to which the socket was bound. This attribute is known as /// `UNIX_DIAG_NAME` in the kernel. - Name(String), + Name(UnixDiagName), /// VFS information for this socket. This attribute is known as /// `UNIX_DIAG_VFS` in the kernel. Vfs(Vfs), @@ -246,8 +303,7 @@ impl nla::Nla for Nla { fn value_len(&self) -> usize { use self::Nla::*; match *self { - // +1 because we need to append a null byte - Name(ref s) => s.as_bytes().len() + 1, + Name(ref s) => s.buffer_len(), Vfs(_) => VFS_LEN, Peer(_) => 4, PendingConnections(ref v) => 4 * v.len(), @@ -261,10 +317,7 @@ impl nla::Nla for Nla { fn emit_value(&self, buffer: &mut [u8]) { use self::Nla::*; match *self { - Name(ref s) => { - buffer[..s.len()].copy_from_slice(s.as_bytes()); - buffer[s.len()] = 0; - } + Name(ref s) => s.emit(buffer), Vfs(ref value) => value.emit(buffer), Peer(value) => NativeEndian::write_u32(buffer, value), PendingConnections(ref values) => { @@ -301,10 +354,10 @@ impl<'a, T: AsRef<[u8]> + ?Sized> Parseable> for Nla { fn parse(buf: &NlaBuffer<&'a T>) -> Result { let payload = buf.value(); Ok(match buf.kind() { - UNIX_DIAG_NAME => { - let err = "invalid UNIX_DIAG_NAME value"; - Self::Name(parse_string(payload).context(err)?) - } + UNIX_DIAG_NAME => Self::Name( + UnixDiagName::parse(&payload) + .context("invalid UNIX_DIAG_NAME value")?, + ), UNIX_DIAG_VFS => { let err = "invalid UNIX_DIAG_VFS value"; let buf = VfsBuffer::new_checked(payload).context(err)?; diff --git a/src/unix/response.rs b/src/unix/response.rs index e5eb8d8..fad9bf4 100644 --- a/src/unix/response.rs +++ b/src/unix/response.rs @@ -13,7 +13,7 @@ use smallvec::SmallVec; use crate::{ constants::*, - unix::nlas::{MemInfo, Nla}, + unix::nlas::{MemInfo, Nla, UnixDiagName}, }; pub const UNIX_RESPONSE_HEADER_LEN: usize = 16; @@ -92,7 +92,7 @@ impl UnixResponse { }) } - pub fn name(&self) -> Option<&String> { + pub fn name(&self) -> Option<&UnixDiagName> { self.nlas.iter().find_map(|nla| { if let Nla::Name(name) = nla { Some(name) diff --git a/src/unix/tests.rs b/src/unix/tests.rs index 39b28ea..127b5f4 100644 --- a/src/unix/tests.rs +++ b/src/unix/tests.rs @@ -5,8 +5,9 @@ use netlink_packet_utils::traits::{Emitable, Parseable}; use crate::{ constants::*, unix::{ - nlas::Nla, ShowFlags, StateFlags, UnixRequest, UnixResponse, - UnixResponseBuffer, UnixResponseHeader, + nlas::{Nla, UnixDiagName}, + ShowFlags, StateFlags, UnixRequest, UnixResponse, UnixResponseBuffer, + UnixResponseHeader, }, }; @@ -39,7 +40,7 @@ lazy_static! { cookie: [0xa0, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], }, nlas: smallvec![ - Nla::Name("/tmp/.ICE-unix/1151".to_string()), + Nla::Name(UnixDiagName::Pathname("/tmp/.ICE-unix/1151".into())), Nla::ReceiveQueueLength(0, 128), Nla::Shutdown(0), ] @@ -101,7 +102,7 @@ lazy_static! { cookie: [0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] }, nlas: smallvec![ - Nla::Name("/run/user/1000/bus".to_string()), + Nla::Name(UnixDiagName::Pathname("/run/user/1000/bus".into())), Nla::Peer(31062), Nla::ReceiveQueueLength(0, 0), Nla::Shutdown(0), @@ -165,3 +166,65 @@ fn emit_socket_info() { SOCKET_INFO.emit(&mut buf); assert_eq!(&buf[..], &SOCKET_INFO_BUF[..]); } + +lazy_static! { + static ref ABSTRACT_ADDRESS: UnixResponse = UnixResponse { + header: UnixResponseHeader { + kind: SOCK_STREAM, + state: TCP_LISTEN, + inode: 20238, + cookie: [0xa0, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + }, + nlas: smallvec![ + Nla::Name(UnixDiagName::Abstract( + "1c1440c5f5e2a52e/bus/systemd/\0/bus-api-user".into() + )), + Nla::ReceiveQueueLength(0, 128), + Nla::Shutdown(0), + ] + }; +} + +#[rustfmt::skip] +static ABSTRACT_ADDRESS_BUF: [u8; 84] = [ + 0x01, // family: AF_UNIX + 0x01, // type: SOCK_STREAM + 0x0a, // state: TCP_LISTEN + 0x00, // padding + 0x0e, 0x4f, 0x00, 0x00, // inode number + 0xa0, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // cookie + + // NLAs + 0x30, 0x00, // length: 48 + 0x00, 0x00, // type: UNIX_DIAG_NAME + // value: \01c1440c5f5e2a52e/bus/systemd/\0/bus-api-user + 0x00, 0x31, 0x63, 0x31, 0x34, 0x34, 0x30, 0x63, 0x35, 0x66, 0x35, 0x65, 0x32, 0x61, 0x35, 0x32, 0x65, 0x2f, 0x62, 0x75, 0x73, 0x2f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x64, 0x2f, 0x00, 0x2f, 0x62, 0x75, 0x73, 0x2d, 0x61, 0x70, 0x69, 0x2d, 0x75, 0x73, 0x65, 0x72, + + 0x0c, 0x00, // length: 12 + 0x04, 0x00, // type: UNIX_DIAG_RQLEN + // value: ReceiveQueueLength(0, 128) + 0x00, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x00, 0x00, + + 0x05, 0x00, // length: 5 + 0x06, 0x00, // type: UNIX_DIAG_SHUTDOWN + 0x00, // value: 0 + 0x00, 0x00, 0x00 // padding +]; + +#[test] +fn parse_abstract_address() { + let parsed = UnixResponse::parse( + &UnixResponseBuffer::new_checked(&&ABSTRACT_ADDRESS_BUF[..]).unwrap(), + ) + .unwrap(); + assert_eq!(parsed, *ABSTRACT_ADDRESS); +} + +#[test] +fn emit_abstract_address() { + assert_eq!(ABSTRACT_ADDRESS.buffer_len(), 84); + let mut buf = vec![0xff; ABSTRACT_ADDRESS.buffer_len()]; + ABSTRACT_ADDRESS.emit(&mut buf); + assert_eq!(&buf[..], &ABSTRACT_ADDRESS_BUF[..]); +}