Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions src/reflector/default_address_monitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ void DefaultAddressMonitor::OnReadable(int /*fd*/) noexcept {
// parsing — the collected list would only be discarded.
if (!overflowed) {
CollectChangedInterfaces(
std::span<const std::byte>{buffer.data(), static_cast<size_t>(received)}, changed);
std::span<std::byte>{buffer.data(), static_cast<size_t>(received)}, changed);
}
}

Expand All @@ -236,25 +236,27 @@ void DefaultAddressMonitor::OnReadable(int /*fd*/) noexcept {
#if defined(__linux__)

// The kernel netlink macros (NLMSG_OK, NLMSG_NEXT, NLMSG_DATA) use C-style casts and byte-wise
// pointer arithmetic that the project's strict warning set rejects; the access is sound (OnReadable
// aligns the buffer for nlmsghdr and messages are NLMSG_ALIGN padded). Scope the suppression to the
// parser.
// pointer arithmetic that the project's strict warning set rejects; the alignment is sound
// (OnReadable aligns the buffer for nlmsghdr and messages are NLMSG_ALIGN padded). Scope the
// suppression to the parser. Every struct the walk reads is start_lifetime_as'd first: the macros
// only compute addresses and read fields of the already-blessed current header, so blessing each
// derived pointer before its first dereference keeps all accesses on live objects.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wold-style-cast"
#pragma GCC diagnostic ignored "-Wcast-align"
#pragma GCC diagnostic ignored "-Wsign-conversion"
#pragma GCC diagnostic ignored "-Wconversion"

void DefaultAddressMonitor::CollectChangedInterfaces(std::span<const std::byte> messages,
void DefaultAddressMonitor::CollectChangedInterfaces(std::span<std::byte> messages,
std::vector<unsigned>& changed) const noexcept {
const auto* header = reinterpret_cast<const nlmsghdr*>(messages.data());
auto* header = start_lifetime_as<nlmsghdr>(messages.data());
for (int length = static_cast<int>(messages.size()); NLMSG_OK(header, length);
header = NLMSG_NEXT(header, length)) {
header = start_lifetime_as<nlmsghdr>(NLMSG_NEXT(header, length))) {
if (header->nlmsg_type == RTM_NEWADDR || header->nlmsg_type == RTM_DELADDR) {
const auto* address = static_cast<const ifaddrmsg*>(NLMSG_DATA(header));
const auto* address = start_lifetime_as<ifaddrmsg>(NLMSG_DATA(header));
AddUnique(changed, address->ifa_index);
} else if (header->nlmsg_type == RTM_NEWLINK || header->nlmsg_type == RTM_DELLINK) {
const auto* link = static_cast<const ifinfomsg*>(NLMSG_DATA(header));
const auto* link = start_lifetime_as<ifinfomsg>(NLMSG_DATA(header));
AddUnique(changed, static_cast<unsigned>(link->ifi_index));
}
}
Expand All @@ -264,7 +266,7 @@ void DefaultAddressMonitor::CollectChangedInterfaces(std::span<const std::byte>

#else

void DefaultAddressMonitor::CollectChangedInterfaces(std::span<const std::byte> messages,
void DefaultAddressMonitor::CollectChangedInterfaces(std::span<std::byte> messages,
std::vector<unsigned>& changed) const noexcept {
// PF_ROUTE messages pack back-to-back; each begins with rt_msghdr's prefix (u_short msglen;
// u_char version; u_char type). Read the fields with memcpy — the buffer carries no
Expand Down
5 changes: 3 additions & 2 deletions src/reflector/default_address_monitor.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ class DefaultAddressMonitor : public AddressMonitor, NoMove {

// Parses a buffer of kernel notification messages and appends each changed interface index to
// `changed`, skipping any already present. Split out from OnReadable so tests can drive it with
// synthesized messages.
void CollectChangedInterfaces(std::span<const std::byte> messages,
// synthesized messages. The span is mutable because the Linux walk start_lifetime_as's the
// netlink structs over the received bytes, which reuses that storage.
void CollectChangedInterfaces(std::span<std::byte> messages,
std::vector<unsigned>& changed) const noexcept;

Dispatcher* dispatcher_;
Expand Down
39 changes: 24 additions & 15 deletions src/reflector/interface_address.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "error.h"
#include "logger.h"
#include "platform.h"
#include "util/start_lifetime_as.h"

#include <cstddef>
#include <cstdint>
Expand Down Expand Up @@ -87,10 +88,12 @@ bool IsUsable(uint32_t ifa_flags) noexcept {
}

// The kernel netlink macros (NLMSG_*, RTA_*, IFLA_RTA, IFA_RTA) use C-style casts and
// byte-wise pointer arithmetic that the project's strict warning set rejects. The accesses are
// sound — the receive buffer is netlink-aligned (alignas below) and every message/attribute is
// NLMSG_ALIGN/RTA_ALIGN padded — so scope the suppression to the netlink code rather than
// hand-reimplementing the macros.
// byte-wise pointer arithmetic that the project's strict warning set rejects; the alignment is
// sound (the receive buffer is max-aligned and every message/attribute is NLMSG_ALIGN/RTA_ALIGN
// padded), so scope the suppression to the netlink code rather than hand-reimplementing the
// macros. Every struct the walks read is start_lifetime_as'd first: the macros only compute
// addresses and read fields of the already-blessed current struct, so blessing each derived
// pointer before its first dereference keeps all accesses on live objects.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wold-style-cast"
#pragma GCC diagnostic ignored "-Wcast-align"
Expand Down Expand Up @@ -154,9 +157,9 @@ bool NetlinkDump(int fd, uint16_t request_type, uint32_t seq, Handler&& handle)
continue;
}

auto* header = reinterpret_cast<nlmsghdr*>(buffer.data());
auto* header = start_lifetime_as<nlmsghdr>(buffer.data());
for (int length = static_cast<int>(received); NLMSG_OK(header, length);
header = NLMSG_NEXT(header, length)) {
header = start_lifetime_as<nlmsghdr>(NLMSG_NEXT(header, length))) {
if (header->nlmsg_seq != seq) {
continue;
}
Expand All @@ -165,7 +168,7 @@ bool NetlinkDump(int fd, uint16_t request_type, uint32_t seq, Handler&& handle)
}
if (header->nlmsg_type == NLMSG_ERROR) {
// nlmsgerr.error is a negative errno (0 is an ACK, not a failure).
const auto* error = static_cast<const nlmsgerr*>(NLMSG_DATA(header));
const auto* error = start_lifetime_as<nlmsgerr>(NLMSG_DATA(header));
if (error->error != 0) {
GetLogger().Error("Netlink dump returned an error: {}", Error::FromErrno(-error->error));
return false;
Expand All @@ -184,13 +187,14 @@ void ResolveViaNetlink(unsigned index, InterfaceAddresses& result) noexcept {
return;
}

NetlinkDump(fd, RTM_GETLINK, 1, [&](const nlmsghdr* header) {
const auto* link = static_cast<const ifinfomsg*>(NLMSG_DATA(header));
NetlinkDump(fd, RTM_GETLINK, 1, [&](nlmsghdr* header) {
const auto* link = start_lifetime_as<ifinfomsg>(NLMSG_DATA(header));
if (static_cast<unsigned>(link->ifi_index) != index) {
return;
}
int length = IFLA_PAYLOAD(header);
for (const rtattr* attr = IFLA_RTA(link); RTA_OK(attr, length); attr = RTA_NEXT(attr, length)) {
for (auto* attr = start_lifetime_as<rtattr>(IFLA_RTA(link)); RTA_OK(attr, length);
attr = start_lifetime_as<rtattr>(RTA_NEXT(attr, length))) {
if (attr->rta_type == IFLA_ADDRESS && RTA_PAYLOAD(attr) == MAC_SIZE) {
const auto* mac = static_cast<const std::byte*>(RTA_DATA(attr));
result.mac = MacAddress::FromBytes(std::span<const std::byte, MAC_SIZE>{mac, MAC_SIZE});
Expand All @@ -200,8 +204,8 @@ void ResolveViaNetlink(unsigned index, InterfaceAddresses& result) noexcept {
});

std::vector<IpAddress> candidates;
NetlinkDump(fd, RTM_GETADDR, 2, [&](const nlmsghdr* header) {
const auto* addr = static_cast<const ifaddrmsg*>(NLMSG_DATA(header));
NetlinkDump(fd, RTM_GETADDR, 2, [&](nlmsghdr* header) {
const auto* addr = start_lifetime_as<ifaddrmsg>(NLMSG_DATA(header));
if (addr->ifa_index != index) {
return;
}
Expand All @@ -213,7 +217,8 @@ void ResolveViaNetlink(unsigned index, InterfaceAddresses& result) noexcept {
const std::byte* address_attr = nullptr;
const std::byte* local_attr = nullptr;
int length = IFA_PAYLOAD(header);
for (const rtattr* attr = IFA_RTA(addr); RTA_OK(attr, length); attr = RTA_NEXT(attr, length)) {
for (auto* attr = start_lifetime_as<rtattr>(IFA_RTA(addr)); RTA_OK(attr, length);
attr = start_lifetime_as<rtattr>(RTA_NEXT(attr, length))) {
switch (attr->rta_type) {
case IFA_ADDRESS:
address_attr = static_cast<const std::byte*>(RTA_DATA(attr));
Expand All @@ -222,7 +227,7 @@ void ResolveViaNetlink(unsigned index, InterfaceAddresses& result) noexcept {
local_attr = static_cast<const std::byte*>(RTA_DATA(attr));
break;
case IFA_FLAGS:
flags = *static_cast<const uint32_t*>(RTA_DATA(attr));
flags = *start_lifetime_as<uint32_t>(RTA_DATA(attr));
break;
default:
break;
Expand Down Expand Up @@ -329,7 +334,11 @@ void ResolveViaGetifaddrs(std::string_view interface, InterfaceAddresses& result
}
break;
case AF_INET6: {
const auto& sin6 = *reinterpret_cast<const sockaddr_in6*>(ifa->ifa_addr);
// Copy rather than cast-and-read: getifaddrs owns the storage and FromSockaddr below
// still reads it as a sockaddr, so beginning a sockaddr_in6's lifetime over it would
// just move the aliasing problem there. The copy is defined either way.
sockaddr_in6 sin6{};
std::memcpy(&sin6, ifa->ifa_addr, sizeof(sin6));
if (!IsUsableIpv6(inet6_fd, ifa->ifa_name, sin6)) {
break;
}
Expand Down
8 changes: 6 additions & 2 deletions src/reflector/ip_address.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "reflector/logger.h"
#include "reflector/platform.h"
#include "reflector/util/narrow_cast.h"
#include "reflector/util/start_lifetime_as.h"

#include <arpa/inet.h>
#include <cstring>
Expand Down Expand Up @@ -185,10 +186,13 @@ std::optional<IpAddress> IpAddress::FromSockaddr(const sockaddr* address) noexce
socklen_t IpAddress::ToSockaddr(sockaddr_storage& storage, uint16_t port, unsigned scope_id) const noexcept {
std::memset(&storage, 0, sizeof(storage));

// start_lifetime_as, not a reinterpret_cast: it begins the family struct's lifetime over the
// zeroed bytes, so the member writes below go to a real object instead of aliasing one that
// was never created there.
switch (family_) {
using enum Family;
case V4: {
auto* v4 = reinterpret_cast<sockaddr_in*>(&storage);
auto* v4 = start_lifetime_as<sockaddr_in>(&storage);
v4->sin_family = AF_INET;
v4->sin_port = htons(port);
std::memcpy(&v4->sin_addr, bytes_.data(), sizeof(v4->sin_addr));
Expand All @@ -201,7 +205,7 @@ socklen_t IpAddress::ToSockaddr(sockaddr_storage& storage, uint16_t port, unsign
return sizeof(sockaddr_in);
}
case V6: {
auto* v6 = reinterpret_cast<sockaddr_in6*>(&storage);
auto* v6 = start_lifetime_as<sockaddr_in6>(&storage);
v6->sin6_family = AF_INET6;
v6->sin6_port = htons(port);
v6->sin6_scope_id = NeedsScopeId(*this) ? scope_id : 0;
Expand Down
7 changes: 5 additions & 2 deletions src/reflector/port_reservation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "error.h"
#include "logger.h"
#include "util/start_lifetime_as.h"

#include <cstdint>
#include <netinet/in.h>
Expand Down Expand Up @@ -61,9 +62,11 @@ std::optional<PortReservation> PortReservation::Create(const IpAddress& source_i
close(fd);
return std::nullopt;
}
// getsockname filled `bound`'s bytes; begin the family struct's lifetime over them so the port
// read is from a real object, not a cast to one that was never created there.
const uint16_t port = v6
? ntohs(reinterpret_cast<const sockaddr_in6*>(&bound)->sin6_port)
: ntohs(reinterpret_cast<const sockaddr_in*>(&bound)->sin_port);
? ntohs(start_lifetime_as<sockaddr_in6>(&bound)->sin6_port)
: ntohs(start_lifetime_as<sockaddr_in>(&bound)->sin_port);

return PortReservation{fd, port};
}
Expand Down
7 changes: 5 additions & 2 deletions src/reflector/tcp_socket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "platform.h"
#include "util/fd_util.h"
#include "util/narrow_cast.h"
#include "util/start_lifetime_as.h"

#include <array>
#include <cerrno>
Expand Down Expand Up @@ -82,7 +83,9 @@ Logger& GetLogger() noexcept {
GetLogger().Error("Cannot read local endpoint for fd {}: {}", fd, Error::FromErrno());
return std::nullopt;
}
return IpEndpoint::FromSockaddr(reinterpret_cast<const sockaddr*>(&addr));
// getsockname filled the bytes; begin a sockaddr's lifetime over them for FromSockaddr's
// sa_family read (it memcpy's the rest out itself).
return IpEndpoint::FromSockaddr(start_lifetime_as<sockaddr>(&addr));
}

} // namespace
Expand Down Expand Up @@ -253,7 +256,7 @@ std::optional<TcpSocket> TcpSocket::Accept() noexcept {
::close(client);
return std::nullopt;
}
return TcpSocket{client, *local, IpEndpoint::FromSockaddr(reinterpret_cast<const sockaddr*>(&peer))};
return TcpSocket{client, *local, IpEndpoint::FromSockaddr(start_lifetime_as<sockaddr>(&peer))};
}

bool TcpSocket::FinishConnect() noexcept {
Expand Down
4 changes: 4 additions & 0 deletions src/reflector/util/start_lifetime_as.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ namespace reflector {
// memmove implicitly creates the implicit-lifetime object (P0593) and the self-copy keeps the bytes.
// is_trivially_copyable_v is the exact precondition — it implies implicit-lifetime (so std accepts it
// too) and is precisely what memmove can reconstitute.
//
// Mutable storage only: the fallback's memmove writes, so there is no const overload. To read a
// const-qualified or foreign-owned buffer as a T, memcpy it into a local instead (see
// IpAddress::FromSockaddr).
template <typename T>
requires std::is_trivially_copyable_v<T>
[[nodiscard]] T* start_lifetime_as(void* p) noexcept {
Expand Down