blob: 16eb73e9c30bad5775f23d515334259807c730dd [file] [log] [blame]
// Copyright 2025 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <arpa/inet.h>
#include <endian.h>
#include <errno.h>
#include <fcntl.h>
#include <net/if.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
#include <format>
#include <thread>
#include <fbl/unique_fd.h>
#include <gtest/gtest.h>
#include <linux/capability.h>
#include <linux/fib_rules.h>
#include <linux/genetlink.h>
#include <linux/if_link.h>
#include <linux/if_tun.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <linux/sockios.h>
#include "src/starnix/tests/syscalls/cpp/capabilities_helper.h"
#include "src/starnix/tests/syscalls/cpp/syscall_matchers.h"
#include "src/starnix/tests/syscalls/cpp/test_helper.h"
namespace {
int GetNlaMessageParseErrorCode() {
// TODO(https://fxbug.dev/450959280): NLA parsing errors produce different
// error codes in Starnix, causing divergence from Linux behavior.
if (test_helper::IsStarnix()) {
return -EINVAL;
}
return -ERANGE;
}
template <int PROTOCOL>
class NetlinkTest : public ::testing::Test {
public:
void SetUp() override {
nl_sock_.reset(socket(AF_NETLINK, SOCK_RAW, PROTOCOL));
ASSERT_TRUE(nl_sock_.is_valid()) << strerror(errno);
}
ssize_t SendMsg(test_helper::NetlinkEncoder& encoder) const {
iovec iov = {};
encoder.Finalize(iov);
struct msghdr header = {};
header.msg_iov = &iov;
header.msg_iovlen = 1;
return sendmsg(nl_sock_.get(), &header, 0);
}
void SetStrictCheck() {
// Starnix is always in strict mode, we're skipping the set.
// TODO(https://fxbug.dev/456508664): Implement NETLINK_GET_STRICT_CHK in
// starnix and remove this check.
if (!test_helper::IsStarnix()) {
int val = 1;
EXPECT_THAT(
setsockopt(nl_sock_.get(), SOL_NETLINK, NETLINK_GET_STRICT_CHK, &val, sizeof(val)),
SyscallSucceeds());
}
}
fbl::unique_fd nl_sock_;
};
class NetlinkRouteTest : public NetlinkTest<NETLINK_ROUTE> {
public:
void CheckNetlinkAlive() {
// Check netlink is still there and responding to observe any kernel panics
// happening on the netlink thread.
test_helper::NetlinkEncoder encoder(RTM_GETLINK, NLM_F_REQUEST | NLM_F_DUMP);
struct ifinfomsg ifi = {
.ifi_family = AF_UNSPEC,
.ifi_type = 0,
.ifi_index = 0,
.ifi_flags = 0,
.ifi_change = 0,
};
encoder.Write(ifi);
ASSERT_THAT(SendMsg(encoder), SyscallSucceeds());
char msg_buf_[4096];
ssize_t len = recv(nl_sock_.get(), msg_buf_, sizeof(msg_buf_), 0);
ASSERT_GT(len, 0) << strerror(errno);
const nlmsghdr* nlmsg = reinterpret_cast<const nlmsghdr*>(msg_buf_);
ASSERT_TRUE(MY_NLMSG_OK(nlmsg, len));
// Accept either NEWLINK or DONE from the system. If there are no links
// installed, then DONE is seen instead of NEWLINK.
ASSERT_THAT(nlmsg->nlmsg_type,
testing::AnyOf(testing::Eq(RTM_NEWLINK), testing::Eq(NLMSG_DONE)));
}
};
class NetlinkGenericTest : public NetlinkTest<NETLINK_GENERIC> {
public:
std::optional<uint16_t> GetFamily(test_helper::NetlinkEncoder& encoder,
const std::string_view& family_name) {
encoder.StartMessage(GENL_ID_CTRL, NLM_F_REQUEST);
const uint32_t sequence = encoder.sequence();
encoder.BeginGenetlinkHeader(CTRL_CMD_GETFAMILY);
encoder.BeginNla(CTRL_ATTR_FAMILY_NAME);
encoder.WriteString(family_name);
encoder.EndNla();
iovec iov = {};
encoder.Finalize(iov);
struct msghdr header = {};
header.msg_iov = &iov;
header.msg_iovlen = 1;
if (static_cast<size_t>(sendmsg(nl_sock_.get(), &header, 0)) != iov.iov_len) {
ADD_FAILURE() << "sendmsg failed" << strerror(errno);
return std::nullopt;
};
struct {
nlmsghdr hdr;
genlmsghdr genl;
// Family ID
nlattr id_attr;
uint16_t id;
} input;
iov.iov_len = sizeof(input);
iov.iov_base = &input;
ssize_t received = recvmsg(nl_sock_.get(), &header, MSG_TRUNC);
if (received < 0) {
ADD_FAILURE() << "recvmsg failed " << strerror(errno);
return std::nullopt;
}
if (static_cast<size_t>(received) < sizeof(input)) {
ADD_FAILURE() << " short received message " << received;
return std::nullopt;
}
if (input.hdr.nlmsg_seq != sequence) {
ADD_FAILURE() << " unexpected message sequence, " << input.hdr.nlmsg_seq << " want "
<< sequence;
return std::nullopt;
}
return input.id;
}
};
// Regression test for Syzkaller netlink crash in https://fxbug.dev/390646804.
//
// Crash was caused by the RTM_NEWPREFIX message being unimplemented and falling
// through a panic.
TEST_F(NetlinkRouteTest, RtmNewPrefixDoesntCrash) {
test_helper::NetlinkEncoder encoder(RTM_NEWPREFIX, 0);
struct prefixmsg pfx = {};
encoder.Write(pfx);
ASSERT_THAT(SendMsg(encoder), SyscallSucceeds());
CheckNetlinkAlive();
}
TEST_F(NetlinkRouteTest, NoCapabilities) {
if (!test_helper::HasCapability(CAP_NET_ADMIN)) {
GTEST_SKIP() << "Needs CAP_NET_ADMIN";
}
test_helper::UnsetCapabilityEffective(CAP_NET_ADMIN);
test_helper::NetlinkEncoder encoder(RTM_NEWRULE,
NLM_F_REQUEST | NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK);
// FIB Rule Header.
fib_rule_hdr frh = {
.family = AF_INET, // IPv4 rule
.dst_len = 0, // Match any destination
.src_len = 32, // Match a /32 source address
.table = RT_TABLE_UNSPEC, // We will specify the table in an attribute
.action = FR_ACT_TO_TBL, // Action is to jump to a new table
};
encoder.Write(frh);
// Add Attribute: Source Address (FRA_SRC)
in_addr src_addr;
inet_pton(AF_INET, "192.0.2.1", &src_addr);
encoder.AddRtAttr(FRA_SRC, src_addr);
// Add Attribute: Table ID (FRA_TABLE)
uint32_t table_id = 100;
encoder.AddRtAttr(FRA_TABLE, table_id);
// Send the message.
ASSERT_THAT(SendMsg(encoder), SyscallSucceeds());
// Receive and verify the reply.
char recv_buf[4096];
ssize_t result;
ASSERT_GT(result = recv(nl_sock_.get(), &recv_buf, sizeof(recv_buf), 0), 0) << strerror(errno);
// Verify that we've received exactly one message that contains the `EPERM`
// error.
nlmsghdr* nlmsg = reinterpret_cast<nlmsghdr*>(recv_buf);
uint32_t len = static_cast<uint32_t>(result);
ASSERT_TRUE(NLMSG_OK(nlmsg, len));
EXPECT_EQ(nlmsg->nlmsg_type, NLMSG_ERROR);
nlmsgerr* err_data = reinterpret_cast<nlmsgerr*>(NLMSG_DATA(nlmsg));
EXPECT_EQ(err_data->error, -EPERM);
nlmsg = NLMSG_NEXT(nlmsg, len);
EXPECT_FALSE(NLMSG_OK(nlmsg, len));
// Restore the capability.
test_helper::SetCapabilityEffective(CAP_NET_ADMIN);
}
void SetLoopbackIfAddr(in_addr_t addr) {
constexpr char kLoopbackIfName[] = "lo";
fbl::unique_fd fd;
ASSERT_TRUE(fd = fbl::unique_fd(socket(AF_INET, SOCK_DGRAM, 0))) << strerror(errno);
ifreq ifr;
*(reinterpret_cast<sockaddr_in*>(&ifr.ifr_addr)) = sockaddr_in{
.sin_family = AF_INET,
.sin_addr = {.s_addr = addr},
};
strncpy(ifr.ifr_name, kLoopbackIfName, IFNAMSIZ);
ASSERT_EQ(ioctl(fd.get(), SIOCSIFADDR, &ifr), 0) << strerror(errno);
}
TEST(RouteNetlinkSocket, AddDropMulticastGroup) {
// TODO(https://fxbug.dev/317285180) don't skip on baseline
if (!test_helper::HasSysAdmin()) {
GTEST_SKIP() << "Not running with sysadmin capabilities, skipping suite.";
}
fbl::unique_fd nlsock(socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE));
ASSERT_TRUE(nlsock) << strerror(errno);
struct sockaddr_nl addr = {};
addr.nl_family = AF_NETLINK;
struct sockaddr* sa = reinterpret_cast<struct sockaddr*>(&addr);
ASSERT_EQ(bind(nlsock.get(), sa, sizeof(addr)), 0) << strerror(errno);
int group = RTNLGRP_IPV4_IFADDR;
ASSERT_EQ(setsockopt(nlsock.get(), SOL_NETLINK, NETLINK_ADD_MEMBERSHIP, &group, sizeof(group)), 0)
<< strerror(errno);
ASSERT_NO_FATAL_FAILURE(SetLoopbackIfAddr(inet_addr("127.0.0.2")));
sleep(1);
char buf[4096] = {};
// Should observe 2 messages (removing old address, adding new address)
// because we're in the corresponding multicast group.
ssize_t len = recv(nlsock.get(), buf, sizeof(buf), 0);
ASSERT_GT(len, 0) << strerror(errno);
nlmsghdr* nlmsg = reinterpret_cast<nlmsghdr*>(buf);
ASSERT_TRUE(MY_NLMSG_OK(nlmsg, len));
ASSERT_EQ(nlmsg->nlmsg_type, RTM_DELADDR);
rtmsg* rtm = reinterpret_cast<rtmsg*>(NLMSG_DATA(nlmsg));
ASSERT_EQ(rtm->rtm_family, AF_INET);
nlmsg = NLMSG_NEXT(nlmsg, len);
if (MY_NLMSG_OK(nlmsg, len)) {
// The next message is already in the buffer, so we don't need to do
// anything here.
} else {
// Need to receive again.
len = recv(nlsock.get(), buf, sizeof(buf), 0);
ASSERT_GT(len, 0) << strerror(errno);
nlmsg = reinterpret_cast<nlmsghdr*>(buf);
ASSERT_TRUE(MY_NLMSG_OK(nlmsg, len));
}
// Assert that the content of the second message indicates the new loopback
// address being added.
ASSERT_EQ(nlmsg->nlmsg_type, RTM_NEWADDR);
rtm = reinterpret_cast<rtmsg*>(NLMSG_DATA(nlmsg));
ASSERT_EQ(rtm->rtm_family, AF_INET);
// Now we should have run out of messages.
nlmsg = NLMSG_NEXT(nlmsg, len);
ASSERT_FALSE(MY_NLMSG_OK(nlmsg, len));
// Drop the multicast group membership so that we won't get notified about
// further address changes.
ASSERT_EQ(setsockopt(nlsock.get(), SOL_NETLINK, NETLINK_DROP_MEMBERSHIP, &group, sizeof(group)),
0)
<< strerror(errno);
// Restore the usual loopback address.
ASSERT_NO_FATAL_FAILURE(SetLoopbackIfAddr(inet_addr("127.0.0.1")));
// Should not observe a message because we're not in any multicast group.
ASSERT_EQ(recv(nlsock.get(), buf, sizeof(buf), MSG_DONTWAIT), -1);
ASSERT_EQ(errno, EAGAIN);
}
struct RouteNetlinkSocketNewAddrParam {
uint8_t family;
const char* addr;
uint8_t prefix;
bool expect_subnet_route;
};
class RouteNetlinkSocketNewAddr : public testing::TestWithParam<RouteNetlinkSocketNewAddrParam> {
public:
void SetUp() override {
// TODO(https://fxbug.dev/317285180) don't skip on baseline
if (!test_helper::HasSysAdmin()) {
GTEST_SKIP() << "Not running with sysadmin capabilities, skipping suite.";
}
const char* dev_tun;
if (test_helper::IsStarnix()) {
dev_tun = "/dev/tun";
} else {
dev_tun = "/dev/net/tun";
}
tun_ = fbl::unique_fd(open(dev_tun, O_RDWR));
ifreq ifr{};
ifr.ifr_flags = IFF_NO_PI | IFF_TUN;
strncpy(ifr.ifr_name, kTestIfName, IFNAMSIZ);
auto result = ioctl(tun_.get(), TUNSETIFF, &ifr);
ASSERT_EQ(result, 0) << strerror(errno);
auto s = fbl::unique_fd(socket(AF_INET, SOCK_DGRAM, 0));
ifr.ifr_flags = 0;
ASSERT_EQ(ioctl(s.get(), SIOCGIFFLAGS, &ifr), 0) << strerror(errno);
ifr.ifr_flags |= IFF_UP;
ASSERT_EQ(ioctl(s.get(), SIOCSIFFLAGS, &ifr), 0) << strerror(errno);
}
void TearDown() override {
tun_.reset();
// Wait for the device to go away so that it does not linger into the next test.
while (if_nametoindex(kTestIfName) > 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
static constexpr char kTestIfName[] = "netlink_test";
private:
fbl::unique_fd tun_;
};
TEST_P(RouteNetlinkSocketNewAddr, AddSubnetRoute) {
// TODO(https://fxbug.dev/317285180) don't skip on baseline
if (!test_helper::HasSysAdmin()) {
GTEST_SKIP() << "Not running with sysadmin capabilities, skipping suite.";
}
fbl::unique_fd nlsock(socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE));
ASSERT_TRUE(nlsock) << strerror(errno);
const auto [family, addr, prefix, expect_subnet_route] = GetParam();
struct sockaddr_nl nladdr = {};
nladdr.nl_family = AF_NETLINK;
struct sockaddr* sa = reinterpret_cast<struct sockaddr*>(&nladdr);
ASSERT_EQ(bind(nlsock.get(), sa, sizeof(nladdr)), 0) << strerror(errno);
{
test_helper::NetlinkEncoder encoder(RTM_NEWADDR, NLM_F_REQUEST | NLM_F_ACK);
struct ifaddrmsg ifa = {
.ifa_family = family,
.ifa_prefixlen = prefix,
.ifa_flags = IFA_F_PERMANENT,
.ifa_scope = RT_SCOPE_UNIVERSE,
.ifa_index = if_nametoindex(kTestIfName),
};
uint8_t addrbuf[sizeof(in6_addr)];
ASSERT_EQ(inet_pton(family, addr, &addrbuf), 1);
auto addr = std::span(addrbuf, family == AF_INET ? sizeof(in_addr) : sizeof(in6_addr));
encoder.Write(ifa);
encoder.BeginNla(IFA_ADDRESS);
encoder.WriteSpan(addr);
encoder.EndNla();
encoder.BeginNla(IFA_LOCAL);
encoder.WriteSpan(addr);
encoder.EndNla();
iovec iov = {};
encoder.Finalize(iov);
struct msghdr header = {};
header.msg_iov = &iov;
header.msg_iovlen = 1;
ASSERT_EQ(sendmsg(nlsock.get(), &header, 0), static_cast<ssize_t>(iov.iov_len))
<< strerror(errno);
}
char buf[4096] = {};
ssize_t len = recv(nlsock.get(), buf, sizeof(buf), 0);
ASSERT_GT(len, 0) << strerror(errno);
nlmsghdr* nlmsg = reinterpret_cast<nlmsghdr*>(buf);
ASSERT_TRUE(MY_NLMSG_OK(nlmsg, len));
ASSERT_EQ(nlmsg->nlmsg_type, NLMSG_ERROR);
auto* errmsg = reinterpret_cast<nlmsgerr*>(NLMSG_DATA(nlmsg));
ASSERT_EQ(errmsg->error, 0);
{
test_helper::NetlinkEncoder encoder(RTM_GETROUTE, NLM_F_REQUEST | NLM_F_DUMP);
rtmsg rtm = {
.rtm_family = family,
};
encoder.Write(rtm);
iovec iov = {};
encoder.Finalize(iov);
struct msghdr msg = {};
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
ASSERT_EQ(sendmsg(nlsock.get(), &msg, 0), static_cast<ssize_t>(iov.iov_len)) << strerror(errno);
}
// Use a timeout instead of MSG_DONTWAIT to avoid timing issues with delays.
struct timeval timeout = {.tv_sec = 0, .tv_usec = 100'000};
ASSERT_EQ(setsockopt(nlsock.get(), SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)), 0);
while (true) {
ssize_t len = recv(nlsock.get(), buf, sizeof(buf), 0);
if (len == 0) {
break;
}
if (len == -1) {
ASSERT_EQ(errno, EAGAIN);
break;
}
rtmsg* rtm = nullptr;
for (nlmsghdr* nlh = reinterpret_cast<nlmsghdr*>(buf); MY_NLMSG_OK(nlh, len);
nlh = NLMSG_NEXT(nlh, len)) {
switch (nlh->nlmsg_type) {
case NLMSG_DONE:
break;
case RTM_NEWROUTE:
rtm = reinterpret_cast<rtmsg*>(NLMSG_DATA(nlh));
// Linux and Fuchsia does local delivery differently, we ignore those by
// filtering out routes that are not in the main table.
if (rtm->rtm_table != RT_TABLE_MAIN) {
continue;
}
if (rtm->rtm_dst_len == prefix) {
ASSERT_TRUE(expect_subnet_route);
return;
}
break;
default:
FAIL() << "unknown message type: " << nlh->nlmsg_type;
}
}
}
ASSERT_FALSE(expect_subnet_route);
}
INSTANTIATE_TEST_SUITE_P(
RouteNetlinkSocket, RouteNetlinkSocketNewAddr,
testing::Values(
RouteNetlinkSocketNewAddrParam{
.family = AF_INET, .addr = "192.0.2.1", .prefix = 0, .expect_subnet_route = false},
RouteNetlinkSocketNewAddrParam{
.family = AF_INET, .addr = "192.0.2.1", .prefix = 1, .expect_subnet_route = true},
RouteNetlinkSocketNewAddrParam{
.family = AF_INET, .addr = "192.0.2.1", .prefix = 32, .expect_subnet_route = false},
RouteNetlinkSocketNewAddrParam{
.family = AF_INET6, .addr = "2001:db8::1", .prefix = 0, .expect_subnet_route = true},
RouteNetlinkSocketNewAddrParam{
.family = AF_INET6, .addr = "2001:db8::1", .prefix = 1, .expect_subnet_route = true},
RouteNetlinkSocketNewAddrParam{
.family = AF_INET6, .addr = "2001:db8::1", .prefix = 128, .expect_subnet_route = true}),
[](const testing::TestParamInfo<RouteNetlinkSocketNewAddr::ParamType>& info) {
return std::format("{}_prefix_{}", info.param.family == AF_INET ? "v4" : "v6",
info.param.prefix);
});
TEST(NetlinkSocket, RecvMsg) {
// TODO(https://fxbug.dev/317285180) don't skip on baseline
if (!test_helper::HasSysAdmin()) {
GTEST_SKIP() << "Not running with sysadmin capabilities, skipping suite.";
}
int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);
ASSERT_GT(fd, 0);
test_helper::NetlinkEncoder encoder(GENL_ID_CTRL, NLM_F_REQUEST);
encoder.BeginGenetlinkHeader(CTRL_CMD_GETFAMILY);
encoder.BeginNla(CTRL_ATTR_FAMILY_NAME);
encoder.Write(TASKSTATS_GENL_NAME);
encoder.EndNla();
iovec iov = {};
encoder.Finalize(iov);
struct msghdr header = {};
header.msg_iov = &iov;
header.msg_iovlen = 1;
ASSERT_EQ(sendmsg(fd, &header, 0), static_cast<ssize_t>(iov.iov_len));
iov.iov_len = 0;
ssize_t received = recvmsg(fd, &header, MSG_PEEK | MSG_TRUNC);
ASSERT_GT(static_cast<size_t>(received), sizeof(nlmsghdr));
struct {
nlmsghdr hdr;
genlmsghdr genl;
// Family ID
nlattr id_attr;
__u16 id;
char padding;
// Family name
nlattr name_attr;
char name[sizeof(TASKSTATS_GENL_NAME)];
char padding_0;
// We should get one multicast group.
// It doesn't seem to matter what the ID
// or name of the group is.
nlattr multicast_group_attr;
} input;
iov.iov_len = sizeof(input);
iov.iov_base = &input;
received = recvmsg(fd, &header, 0);
ASSERT_EQ(static_cast<size_t>(received), sizeof(input));
ASSERT_EQ(input.id_attr.nla_type, CTRL_ATTR_FAMILY_ID);
ASSERT_EQ(input.genl.cmd, CTRL_CMD_NEWFAMILY);
ASSERT_EQ(input.name_attr.nla_type, CTRL_ATTR_FAMILY_NAME);
ASSERT_FALSE(memcmp(input.name, TASKSTATS_GENL_NAME, sizeof(input.name)));
ASSERT_EQ(input.multicast_group_attr.nla_type, CTRL_ATTR_MCAST_GROUPS);
struct {
nlmsghdr hdr;
genlmsghdr genl;
} input_2;
// Connect to TASKSTATS
encoder.StartMessage(input.id, NLM_F_REQUEST);
// We don't parse commands currently, so this number is arbitrary.
encoder.BeginGenetlinkHeader(42);
encoder.Finalize(iov);
ASSERT_EQ(sendmsg(fd, &header, 0), static_cast<ssize_t>(iov.iov_len));
iov.iov_base = &input_2;
iov.iov_len = sizeof(input_2);
// TASKSTATS payload
received = recvmsg(fd, &header, 0);
ASSERT_EQ(static_cast<size_t>(received), sizeof(input_2));
ASSERT_EQ(input_2.hdr.nlmsg_type, input.id);
// ACK payload
received = recvmsg(fd, &header, 0);
ASSERT_EQ(static_cast<size_t>(received), sizeof(input_2));
ASSERT_EQ(input_2.hdr.nlmsg_type, NLMSG_ERROR);
}
TEST(NetlinkSocket, FamilyMissing) {
int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);
ASSERT_GT(fd, 0);
test_helper::NetlinkEncoder encoder(GENL_ID_CTRL, NLM_F_REQUEST);
encoder.BeginGenetlinkHeader(CTRL_CMD_GETFAMILY);
encoder.BeginNla(CTRL_ATTR_FAMILY_NAME);
encoder.Write("Hyainailouridae");
encoder.EndNla();
iovec iov = {};
encoder.Finalize(iov);
struct msghdr header = {};
header.msg_iov = &iov;
header.msg_iovlen = 1;
ASSERT_EQ(sendmsg(fd, &header, 0), static_cast<ssize_t>(iov.iov_len));
nlmsghdr* orig_nlmsghdr = static_cast<nlmsghdr*>(iov.iov_base);
iov.iov_len = 0;
ssize_t received = recvmsg(fd, &header, MSG_PEEK | MSG_TRUNC);
ASSERT_GT(static_cast<size_t>(received), sizeof(nlmsghdr));
struct {
nlmsghdr hdr;
nlmsgerr err;
} input;
iov.iov_len = sizeof(input);
iov.iov_base = &input;
received = recvmsg(fd, &header, 0);
ASSERT_EQ(static_cast<size_t>(received), sizeof(input));
ASSERT_EQ(input.hdr.nlmsg_type, NLMSG_ERROR);
ASSERT_EQ(input.err.error, -ENOENT);
ASSERT_FALSE(memcmp(&input.err.msg, orig_nlmsghdr, sizeof(nlmsghdr)));
}
TEST(NetlinkSocket, NlctrlFamily) {
// TODO(https://fxbug.dev/317285180) don't skip on baseline
if (!test_helper::HasSysAdmin()) {
GTEST_SKIP() << "Not running with sysadmin capabilities, skipping suite.";
}
int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);
ASSERT_GT(fd, 0);
constexpr char kNlctrl[] = "nlctrl";
test_helper::NetlinkEncoder encoder(GENL_ID_CTRL, NLM_F_REQUEST);
encoder.BeginGenetlinkHeader(CTRL_CMD_GETFAMILY);
encoder.BeginNla(CTRL_ATTR_FAMILY_NAME);
encoder.Write(kNlctrl);
encoder.EndNla();
iovec iov = {};
encoder.Finalize(iov);
struct msghdr header = {};
header.msg_iov = &iov;
header.msg_iovlen = 1;
ASSERT_EQ(sendmsg(fd, &header, 0), static_cast<ssize_t>(iov.iov_len));
iov.iov_len = 0;
ssize_t received = recvmsg(fd, &header, MSG_PEEK | MSG_TRUNC);
ASSERT_GT(static_cast<size_t>(received), sizeof(nlmsghdr));
struct {
nlmsghdr hdr;
genlmsghdr genl;
// Family ID
nlattr id_attr;
__u16 id;
char padding;
// Family name
nlattr name_attr;
char name[sizeof(kNlctrl)];
char padding_0;
} input;
iov.iov_len = sizeof(input);
iov.iov_base = &input;
received = recvmsg(fd, &header, 0);
ASSERT_EQ(static_cast<size_t>(received), sizeof(input));
ASSERT_EQ(input.id_attr.nla_type, CTRL_ATTR_FAMILY_ID);
ASSERT_EQ(input.id, GENL_ID_CTRL);
ASSERT_EQ(input.genl.cmd, CTRL_CMD_NEWFAMILY);
ASSERT_EQ(input.name_attr.nla_type, CTRL_ATTR_FAMILY_NAME);
ASSERT_FALSE(memcmp(input.name, "nlctrl", sizeof(input.name)));
}
// Regression test for syzkaller finding on https://fxbug.dev/387599168.
TEST_F(NetlinkRouteTest, IflaCacheInfoMissingBody) {
SetStrictCheck();
// The reproducer sends a message type 0x16, which is RTM_GETADDR.
test_helper::NetlinkEncoder encoder(RTM_GETADDR, NLM_F_REQUEST);
encoder.Write(ifaddrmsg{
.ifa_family = AF_INET6,
});
// Correct size for cache info is 16.
uint16_t cacheinfo = 0;
encoder.AddRtAttr(IFA_CACHEINFO, cacheinfo);
// We expect the syscall to succeed, e.g. not crash.
ASSERT_THAT(SendMsg(encoder), SyscallSucceeds());
struct {
nlmsghdr hdr;
nlmsgerr err;
} response;
ssize_t received = recv(nl_sock_.get(), &response, sizeof(response), 0);
ASSERT_THAT(received, SyscallSucceeds());
ASSERT_EQ(static_cast<size_t>(received), sizeof(response));
EXPECT_EQ(response.hdr.nlmsg_type, NLMSG_ERROR);
ASSERT_THAT(response.err.error, GetNlaMessageParseErrorCode());
}
// Netlink messages that are malformed, are expected to generate an error
// response, as opposed to failing the syscall.
TEST_F(NetlinkRouteTest, MalformedMessageCausesErrorReturn) {
test_helper::NetlinkEncoder encoder(RTM_GETADDR, NLM_F_REQUEST);
// The body should contain struct ifaddrmsg. But instead just write a single
// field from it.
ifaddrmsg ifa = {
.ifa_family = AF_INET6,
};
encoder.Write(ifa.ifa_family);
// We expect the syscall to succeed, e.g. not crash.
ASSERT_THAT(SendMsg(encoder), SyscallSucceeds());
struct {
nlmsghdr hdr;
nlmsgerr err;
} response;
ssize_t received = recv(nl_sock_.get(), &response, sizeof(response), 0);
ASSERT_THAT(received, SyscallSucceeds());
ASSERT_EQ(static_cast<size_t>(received), sizeof(response));
EXPECT_EQ(response.hdr.nlmsg_type, NLMSG_ERROR);
EXPECT_EQ(response.err.error, -EINVAL);
}
TEST_F(NetlinkRouteTest, MessageWithIncompleteHeader) {
struct nlmsghdr header = {.nlmsg_len = sizeof(uint32_t)};
ASSERT_THAT(send(nl_sock_.get(), &header.nlmsg_len, sizeof(header.nlmsg_len), 0),
SyscallSucceeds());
// A message with an incomplete header generates no response.
nlmsghdr response;
ASSERT_THAT(recv(nl_sock_.get(), &response, sizeof(response), MSG_TRUNC | MSG_DONTWAIT),
SyscallFailsWithErrno(EAGAIN));
// Following message still works.
CheckNetlinkAlive();
}
TEST_F(NetlinkRouteTest, HeaderOnlyMessageWithBadLength) {
struct nlmsghdr header = {
.nlmsg_len = sizeof(nlmsghdr) + sizeof(ifaddrmsg),
.nlmsg_type = RTM_GETADDR,
.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK,
.nlmsg_seq = 123456,
.nlmsg_pid = static_cast<uint32_t>(getpid()),
};
ASSERT_THAT(send(nl_sock_.get(), &header.nlmsg_len, sizeof(header.nlmsg_len), 0),
SyscallSucceeds());
// This is a malformed message it should not generate a response.
nlmsghdr response;
ASSERT_THAT(recv(nl_sock_.get(), &response, sizeof(response), MSG_TRUNC | MSG_DONTWAIT),
SyscallFailsWithErrno(EAGAIN));
// Following message still works.
CheckNetlinkAlive();
}
// Regression test for syzkaller finding on https://fxbug.dev/387662319.
TEST_F(NetlinkRouteTest, IncompleteRouteFlow) {
test_helper::NetlinkEncoder encoder(RTM_GETROUTE, NLM_F_REQUEST);
encoder.Write(rtmsg{
.rtm_family = AF_INET,
});
// Correct size for flow info is a u32.
uint16_t bad_attr = 0;
encoder.AddRtAttr(RTA_FLOW, bad_attr);
ASSERT_THAT(SendMsg(encoder), SyscallSucceeds());
struct {
nlmsghdr hdr;
nlmsgerr err;
} response;
ssize_t received = recv(nl_sock_.get(), &response, sizeof(response), 0);
ASSERT_THAT(received, SyscallSucceeds());
ASSERT_EQ(static_cast<size_t>(received), sizeof(response));
EXPECT_EQ(response.hdr.nlmsg_type, NLMSG_ERROR);
ASSERT_THAT(response.err.error, GetNlaMessageParseErrorCode());
}
// Regression test for syzkaller finding on https://fxbug.dev/393764263.
TEST_F(NetlinkGenericTest, PartialCtrlAttrPolicy) {
test_helper::NetlinkEncoder encoder(GENL_ID_CTRL, 0);
encoder.Write(genlmsghdr{
// CTRL_CMD_GET_POLICY. Not available in header.
.cmd = 10,
});
encoder.Write(nlattr{
.nla_len = 8,
.nla_type = 9, // CTRL_ATTR_OP_POLICY. Not available in header.
});
encoder.Write(nlattr{
// Invalid length less than 4.
.nla_len = 2,
});
ASSERT_THAT(SendMsg(encoder), SyscallSucceeds());
}
// Regression test for syzkaller finding on https://fxbug.dev/389503570.
TEST_F(NetlinkGenericTest, ShortTaskstatsMessage) {
test_helper::NetlinkEncoder encoder;
std::optional<uint16_t> taskstats = GetFamily(encoder, TASKSTATS_GENL_NAME);
ASSERT_TRUE(taskstats.has_value());
encoder.StartMessage(*taskstats, NLM_F_DUMP | NLM_F_ACK);
ASSERT_THAT(SendMsg(encoder), SyscallSucceeds());
// This message generates a response. We're not fully asserting on it because
// the implementation is stubbed. Expecting a response as opposed to a starnix
// crash covers the regression for now. See https://fxbug.dev/339675153.
uint8_t buffer[10];
ASSERT_THAT(recv(nl_sock_.get(), &buffer, sizeof(buffer), MSG_TRUNC), SyscallSucceeds());
}
} // namespace