Repository navigation
ares_dns_write: reject messages larger than 64k - #1314
Conversation
bradh352
left a comment
There was a problem hiding this comment.
Thanks, this is a real bug: ares_dns_write() wrote a cut-down length for record data over 65535 bytes, which breaks the framing of every record after it. I confirmed the new test fails on the base commit and passes with the fix, including under the address and undefined-behaviour sanitizers.
The main request is in the inline comments: put the limit on the whole message, not on each record or option. The parser and the TCP writer both cap a message at 65535 bytes, so this change still produces output the library itself rejects.
- Low —
src/lib/record/ares_dns_write.c, inares_dns_write_buf_tcp(): thegoto doneafterares_dns_write_buf()is tagged/* LCOV_EXCL_LINE: OutOfMemory */. An oversized message sent over TCP can now reach it throughARES_EBADQUERY, so the tag hides a path that can actually run. Drop the tag. - Low —
docs/ares_dns_record.3, RETURN VALUES: the man page lists onlyARES_SUCCESS,ARES_ENOMEMandARES_EFORMERRforares_dns_write(). Please addARES_EBADQUERY, described as "the message is too large to encode". - Nit — PRs are squash-merged, so the title and body become the commit. The title is too long for one line, which is why GitHub split it. The commit body is only that leftover fragment, with no explanation and no
Signed-off-by:line (seeCONTRIBUTING.md). Suggested title:ares_dns_write: reject messages larger than 64k. - Overlap: #1257, an open PR from another author, adds the same per-record check in
ares_dns_write_rr()and has had the same request to move it to the whole message. One message-level check covers both PRs.
301a6dc to
0fd35bc
Compare
bradh352
left a comment
There was a problem hiding this comment.
Thanks, the message-level check is exactly what was needed. I confirmed both new tests fail on the base commit and pass here, including under the address and undefined-behaviour sanitizers, and that the 65535-byte edge case round-trips through ares_dns_parse().
What's left is small: two items from the last round's summary weren't picked up (the coverage-exclusion tag and the man page), and clang-format still needs a pass over the tests.
- Low —
src/lib/record/ares_dns_write.c,ares_dns_write_buf_tcp(): thegoto doneafterares_dns_write_buf()is still tagged/* LCOV_EXCL_LINE: OutOfMemory */. Now thatares_dns_write_buf()returnsARES_EBADQUERYfor any message over 65535 bytes, an oversized query sent over TCP reaches this line on a normal path. The tag hides a path that can actually run. Please drop it. - Low —
docs/ares_dns_record.3, RETURN VALUES: the man page still lists onlyARES_SUCCESS,ARES_ENOMEMandARES_EFORMERRforares_dns_write(). It can now also returnARES_EBADQUERY. Please add it, described as "the message is too large to encode". - Nit — The reply on the formatting thread says the tests were reformatted, but
git clang-format --diffstill rewrites both new tests: the declaration alignment inDNSWriteOversizedRdlengthand most of the wrappedEXPECT_EQ(...)calls. Please rungit clang-format origin/mainand commit the result. - Nit — PRs are squash-merged, so the title becomes the commit subject. At 71 characters it's still long for one line;
ares_dns_write: reject messages larger than 64ksays the same thing. The PR description now explains the change well.
0fd35bc to
afdc2cb
Compare
bradh352
left a comment
There was a problem hiding this comment.
The two test nits from the last round are fixed: the framing comment now matches the code, and the oversized test reuses one buffer. The writer code hasn't changed since the last round, and both tests still pass here, including under the address and undefined-behaviour sanitizers.
Three items from the previous two rounds are still open. They're all small, but this is the third time they've come up, so please check each one before the next push.
- Low —
src/lib/record/ares_dns_write.c,ares_dns_write_buf_tcp(), line 1497: thegoto doneafterares_dns_write_buf()is still tagged/* LCOV_EXCL_LINE: OutOfMemory */. That tag tells the coverage tool to ignore the line because only an allocation failure can reach it. That is no longer true:ares_dns_write_buf()now returnsARES_EBADQUERYfor any message over 65535 bytes, so an oversized query sent over TCP reaches this line on a normal path. Please drop the tag:status = ares_dns_write_buf(dnsrec, buf); if (status != ARES_SUCCESS) { goto done; }
- Low —
docs/ares_dns_record.3, RETURN VALUES (around line 451): the man page still lists onlyARES_SUCCESS,ARES_ENOMEMandARES_EFORMERR.ares_dns_write()can now also returnARES_EBADQUERY. A public function's man page has to change in the same PR as its behaviour. After theARES_EFORMERRsentence, add for example:\fIares_dns_write(3)\fP also returns .B ARES_EBADQUERY if the message is too large to encode. - Nit — Formatting:
git clang-format --diff origin/mainstill rewrites both new tests. It changes the declaration alignment at the top ofDNSWriteOversizedRdlength(the= nullptrcolumns are out of line) and nearly every wrappedEXPECT_EQ(...)call in both tests. Runninggit clang-format origin/mainand committing the result fixes all of it at once. Please run the tool itself rather than editing by hand: the last two replies said it was done, but the output hasn't changed. - Nit — PR title: PRs are squash-merged, so the title becomes the commit subject. At 71 characters it's still long for one line. Suggested:
ares_dns_write: reject messages larger than 64k.
ares_dns_write() wrote a cut-down 16-bit rdlength for record data over 65535 bytes, breaking the framing of every record after it, and a record of that size still produced a message the parser and the TCP writer both reject. ares_dns_write_buf() now refuses any message over 65535 bytes with ARES_EBADQUERY, checked after the additional section is written; the per-record rdlength check stays as an early exit. The matching check in ares_dns_write_buf_tcp() is removed as redundant, and the ares_dns_record(3) man page lists the new return value. Signed-off-by: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com>
afdc2cb to
d2e869e
Compare
bradh352
left a comment
There was a problem hiding this comment.
All four items from the last round are in. I re-checked them at d2e869e: the coverage-exclusion tag is gone from the goto done in ares_dns_write_buf_tcp(), the ares_dns_record(3) man page lists ARES_EBADQUERY for ares_dns_write(), git clang-format --diff now leaves both tests unchanged, and the title is the short form. Thanks for explaining the clang-format version mismatch.
It builds with warnings as errors and both new tests pass, including under the address and undefined-behaviour sanitizers. The branch merges cleanly with current main. Nothing further from me.
|
/backport v1.34 |
# Description Backport of #1314 to `v1.34`. Signed-off-by: Manohar Paturi (@ManoharPaturi) Co-authored-by: Manohar Paturi <manoharpaturi777@gmail.com>

Problem
ares_dns_write()silently truncates any RDATA or option value that does not fit its 16-bit length prefix:ares_dns_write_rr()(src/lib/record/ares_dns_write.c:1401):ares_buf_append_be16(buf, (unsigned short)(rdlength & 0xFFFF))ares_buf_append_be16(buf, (unsigned short)(val_len & 0xFFFF))A record whose RDATA exceeds 65535 bytes is serialized "successfully" with a wrapped rdlength while the full RDATA is still emitted — a structurally corrupt message reported as
ARES_SUCCESS.Reproduced against current
main: a TXT RR with 300 × 255-byte character-strings (rdlength 76500) yields a 76,841-byte message with rdlength wrapped to 10964; re-parsing the output fails withARES_EFORMERR. The same class is reachable throughares_dns_rr_set_opt/SVCB/HTTPS values > 65535 (the length prefix truncates while the value is appended, desynchronizing every following option).The TCP-framing writer already guards this exact condition (
if (msg_len > 65535) { status = ARES_EBADQUERY; }at :1492) — the RR-level writers were missing the same check.Fix
ares_dns_write_buf()now enforces the real limit, the one the parser and the TCP writer already enforce: after the additional section is written, a message larger than 65535 bytes is rejected withARES_EBADQUERY. That single check also bounds the header's unchecked(unsigned short)section count casts, since more than 65535 records cannot fit in 65535 bytes, and it replaces the now redundant post-hoc check inares_dns_write_buf_tcp().The per-record
rdlengthcheck is kept as an early exit for record data over 65535 bytes, and the three option value length checks (OPT, SVCB, HTTPS) are dropped: an option value over 65535 bytes always makes the record's data over 65535 bytes too, so therdlengthcheck already catches it and the message level check catches that.Verification
DNSWriteOversizedRdlength(test/ares-test-internal.cc) builds the 300x255 TXT RR through the public record API and requiresARES_EBADQUERYexactly.main: FAILS (write "succeeds" with a corrupt message)ares_dns_write/parse round-trip tests unaffectedDNSWriteMaximumMessageSizehits the exact byte limit with a raw record (ARES_REC_TYPE_RAW_RR+ares_dns_rr_set_bin()): the framing is measured with a one byte payload, then the largest message that fits (65535 bytes total) must writeARES_SUCCESSand parse back throughares_dns_parse(), and one byte more must fail withARES_EBADQUERY.Signed-off-by: Manohar Paturi 186662190+ManoharPaturi@users.noreply.github.com