Skip to content

Use loadUnaligned/storeBytes for the DNS wire codec#1893

Open
anxkhn wants to merge 1 commit into
apple:mainfrom
anxkhn:fix/dns-unaligned-wire-codec
Open

Use loadUnaligned/storeBytes for the DNS wire codec#1893
anxkhn wants to merge 1 commit into
apple:mainfrom
anxkhn:fix/dns-unaligned-wire-codec

Conversation

@anxkhn

@anxkhn anxkhn commented Jul 4, 2026

Copy link
Copy Markdown

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Motivation and Context

The DNS wire codec in Sources/DNSServer/Records/UInt8+Binding.swift reads and
writes multi-byte integers (the UInt16 type/class fields, the UInt32 TTL) at
offsets that follow a variable-length domain name, so those fields routinely land
on unaligned addresses. For example ResourceRecord.serialize writes the UInt32
TTL immediately after the name.

copyIn/copyOut accessed that memory with:

$0.baseAddress?.advanced(by: offset).assumingMemoryBound(to: T.self).pointee

That has two problems under the UnsafeRawPointer contract:

  • assumingMemoryBound(to: T.self) asserts the memory is already bound to T, but
    the buffer is a [UInt8] bound to UInt8. It never rebinds, so it type-puns the
    storage.
  • Typed .pointee load/store requires the address to be properly aligned for T,
    while these accesses happen at variable, frequently odd, offsets.

Both are undefined behavior. arm64 happens to tolerate unaligned scalar loads, so
this works in practice today, but it is not guaranteed by the language and is
exactly the kind of access the raw-pointer API has dedicated, well-defined
alternatives for.

This switches to the alignment- and binding-agnostic raw buffer APIs:

  • write path -> UnsafeMutableRawBufferPointer.storeBytes(of:toByteOffset:as:)
  • read path -> UnsafeRawBufferPointer.loadUnaligned(fromByteOffset:as:)

Both operate on untyped raw bytes (no memory binding is assumed or changed) and
loadUnaligned has no alignment requirement. The generic parameter is constrained
to BitwiseCopyable, which is the constraint those overloads require; it also moves
storeBytes's trivial-type precondition to compile time. The only callers pass
UInt8, UInt16, and UInt32, which all satisfy it. The [UInt8] extension is
package-scoped, so the tightened signature is not visible outside the module.

The existing size/bounds guards and the nil-on-too-small behavior are unchanged
(the removed guard let ... = baseAddress branch in copyOut was only reachable
for an empty buffer, which the count >= size + offset guard already rejects for
every caller, since every value type here is at least one byte).

Note on scope: this fixes latent undefined behavior rather than a currently
observable failure. On arm64 the old code returns the correct bytes, and neither
AddressSanitizer nor -sanitize=undefined traps the unaligned access (there is no
C-style alignment check in the Swift sanitizers), so I could not write a test that
is red before the fix and green after on this architecture. The added tests instead
pin the codec's observable contract: correct big-endian layout and lossless
round-trips at unaligned offsets, so the intended wire behavior is locked in and the
fix is exercised on the actual unaligned path.

Testing

  • Tested locally
  • Added/updated tests
  • Added/updated docs

Added a Binding suite to Tests/DNSServerTests/RecordsTests.swift:

  • UInt16 and UInt32 round-trip through copyIn/copyOut at every byte offset
    within a machine word (0..7), asserting correct big-endian bytes and value.
  • copyIn/copyOut still return nil when the buffer is too small.
  • A Message serialize/deserialize round-trip whose question (example.com.) puts
    the type/class UInt16 fields at odd offsets (25/27), asserting both the raw
    big-endian bytes on the wire and the decoded values.

Verification run:

  • swift build --target DNSServer succeeds.
  • swift test --filter DNSServerTests passes (70 tests, including the 5 new ones).
  • make fmt produces no changes to the touched files; strict swift format lint
    on both files is clean.

The DNS wire codec writes and reads multi-byte integers (UInt16 type and
class, UInt32 ttl) at offsets that follow a variable-length domain name, so
they routinely land on unaligned addresses. copyIn/copyOut accessed that
memory via advanced(by:).assumingMemoryBound(to:).pointee, which assumes the
address is correctly aligned for T and treats the UInt8-bound buffer as if it
were bound to T. Both assumptions are invalid here, so the accesses are
undefined behavior even though arm64 happens to tolerate them.

Switch the write path to UnsafeMutableRawBufferPointer.storeBytes and the read
path to UnsafeRawBufferPointer.loadUnaligned, which make no alignment or
type-binding assumptions for trivial types. Constrain the generic parameter to
BitwiseCopyable so the trivial-type precondition is enforced at compile time;
the only callers pass UInt8, UInt16, and UInt32.

Add tests that round-trip UInt16 and UInt32 through copyIn/copyOut at every
byte offset within a machine word, asserting correct big-endian layout, plus a
Message serialize/deserialize round-trip whose fields land on odd offsets.
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