Skip to content

refactor: read populated kernel structs via start_lifetime_as#49

Merged
sbogomolov merged 1 commit into
mainfrom
cleanup/start-lifetime-as-audit
Jul 8, 2026
Merged

refactor: read populated kernel structs via start_lifetime_as#49
sbogomolov merged 1 commit into
mainfrom
cleanup/start-lifetime-as-audit

Conversation

@sbogomolov

Copy link
Copy Markdown
Owner

Audit of the codebase's reinterpret_cast / memcpy sites for the ones that reinterpret populated storage as a struct and then read/write through it — formally UB (no object of that type was created there). Those move onto reflector::start_lifetime_as, which begins the struct's lifetime over the existing bytes so the access lands on a real object.

Converted:

  • IpAddress::ToSockaddr — the family sockaddr the member writes go to (post-memset).
  • port_reservation / tcp_socket — the getsockname and accept reads of sin_port / sa_family.
  • Both netlink walks (CollectChangedInterfaces monitor + NetlinkDump/ResolveViaNetlink resolver): every struct read is blessed before its first dereference — nlmsghdr (init + each NLMSG_NEXT), the NLMSG_DATA bodies (ifaddrmsg/ifinfomsg), nlmsgerr, the rtattr chains (IFLA_RTA/IFA_RTA + each RTA_NEXT), and the IFA_FLAGS uint32_t payload. The macros only compute addresses and read fields of the already-blessed current struct, so per-step blessing keeps every access on a live object. CollectChangedInterfaces now takes a mutable span<std::byte> (the shim reuses storage).

Left as plain casts, deliberately:

  • Casts handed straight to a syscall (bind/getsockname/connect/recvfrom/accept) — the kernel reads/writes raw bytes outside the C++ abstract machine; no sockaddr object is accessed in our code.
  • reinterpret_cast<const char*>(byte_span.data()) for string_view construction — char may alias any object, and this is the permitted direction (reading bytes as chars).
  • The bpf_insn / sock_filter casts — fed to setsockopt/ioctl (syscall boundary).
  • The getifaddrs IPv6 read — kept as a memcpy into a local, since FromSockaddr re-reads the same storage as a sockaddr regardless.

start_lifetime_as.h gains a note that it's mutable-storage-only (the fallback's memmove writes; no const overload).

Full gate green: native unit (ASan/UBSan) 847/847, docker debug/release 840/840, docker valgrind 0 errors, e2e 33/33 plain and under valgrind.

Where the code reinterpret_cast populated storage to a struct and read
through it — the family sockaddr in ToSockaddr and the getsockname /
accept reads, and the netlink message/attribute walks — begin the
struct's lifetime over those bytes with start_lifetime_as instead. The
read then goes through a real object rather than one that was never
created there.

The netlink walks bless each derived pointer before its first
dereference: the NLMSG_*/RTA_* macros only compute addresses and read
fields of the already-blessed current struct, so nlmsghdr, its
NLMSG_DATA body, nlmsgerr, the rtattr chain, and the IFA_FLAGS payload
are each wrapped in turn. CollectChangedInterfaces now takes a mutable
span<std::byte> since the shim reuses that storage.

Left as plain casts by design: casts handed to a syscall (the kernel
touches raw bytes, no C++ access), byte->char reads for a string_view
(a permitted alias direction), and the bpf/sock_filter casts fed to
setsockopt/ioctl.

Full gate green: native unit, docker debug/release, docker valgrind
unit, e2e, and e2e under valgrind.
@sbogomolov
sbogomolov merged commit 48e9b60 into main Jul 8, 2026
17 checks passed
@sbogomolov
sbogomolov deleted the cleanup/start-lifetime-as-audit branch July 9, 2026 18:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant