Use loadUnaligned/storeBytes for the DNS wire codec#1893
Open
anxkhn wants to merge 1 commit into
Open
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Type of Change
Motivation and Context
The DNS wire codec in
Sources/DNSServer/Records/UInt8+Binding.swiftreads andwrites multi-byte integers (the
UInt16type/class fields, theUInt32TTL) atoffsets that follow a variable-length domain name, so those fields routinely land
on unaligned addresses. For example
ResourceRecord.serializewrites theUInt32TTL immediately after the name.
copyIn/copyOutaccessed that memory with:That has two problems under the
UnsafeRawPointercontract:assumingMemoryBound(to: T.self)asserts the memory is already bound toT, butthe buffer is a
[UInt8]bound toUInt8. It never rebinds, so it type-puns thestorage.
.pointeeload/store requires the address to be properly aligned forT,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:
UnsafeMutableRawBufferPointer.storeBytes(of:toByteOffset:as:)UnsafeRawBufferPointer.loadUnaligned(fromByteOffset:as:)Both operate on untyped raw bytes (no memory binding is assumed or changed) and
loadUnalignedhas no alignment requirement. The generic parameter is constrainedto
BitwiseCopyable, which is the constraint those overloads require; it also movesstoreBytes's trivial-type precondition to compile time. The only callers passUInt8,UInt16, andUInt32, which all satisfy it. The[UInt8]extension ispackage-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 ... = baseAddressbranch incopyOutwas only reachablefor an empty buffer, which the
count >= size + offsetguard already rejects forevery 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=undefinedtraps the unaligned access (there is noC-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
Added a
Bindingsuite toTests/DNSServerTests/RecordsTests.swift:UInt16andUInt32round-trip throughcopyIn/copyOutat every byte offsetwithin a machine word (0..7), asserting correct big-endian bytes and value.
copyIn/copyOutstill returnnilwhen the buffer is too small.Messageserialize/deserialize round-trip whose question (example.com.) putsthe
type/classUInt16fields at odd offsets (25/27), asserting both the rawbig-endian bytes on the wire and the decoded values.
Verification run:
swift build --target DNSServersucceeds.swift test --filter DNSServerTestspasses (70 tests, including the 5 new ones).make fmtproduces no changes to the touched files; strictswift formatlinton both files is clean.