Query with a huge question section can lead to crash
Closed this issue · 2 comments
After trying to fuzz the primary server from the example folder with big inputs, I found some packets that could crash main.native
. In this example, the culprit is a query of size 3451 that consists of many questions (https://pastebin.com/tnV0JUbR for a hexadecimal and byte representation of that packet) :
2018-07-05 16:21:20 +01:00: INF [tcpip-stack-socket] Manager: connect
2018-07-05 16:21:20 +01:00: INF [tcpip-stack-socket] Manager: configuring
2018-07-05 16:21:20 +01:00: WRN [application] no secondaries keys found (err not found TTL 300 soa SOA foo._key-management foo._key-management 0 16384 2048 1048576 300)
2018-07-05 16:21:20 +01:00: INF [application] loaded zone: mirage. 2560 SOA ns.mirage.hostmaster.mirage. 1 10 5 60 2560
mirage. 2560 NS ns.mirage.
charrua.mirage. 2560 A 10.0.42.3
ns.mirage. 2560 A 10.0.42.2
resolver.mirage. 2560 A 10.0.42.5
router.mirage. 2560 A 10.0.42.1
secondary.mirage. 2560 A 10.0.42.4
www.mirage. 2560 CNAME router.mirage.
2018-07-05 16:21:20 +01:00: INF [dns_mirage_server] DNS server listening on UDP port 53
2018-07-05 16:21:20 +01:00: INF [dns_mirage_server] DNS server listening on TCP port 53
2018-07-05 16:21:26 +01:00: INF [dns_mirage_server] udp frame from 127.0.0.1:33525
2018-07-05 16:21:26 +01:00: ERR [dns_server] 181 questions foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?, foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, foo.my.domain A?,
foo.my.domain A?, bailing
Fatal error: exception (Invalid_argument
"invalid bounds in Cstruct.BE.set_uint16 [0,450](450) off=449 len=2")
Raised at file "format.ml" (inlined), line 242, characters 35-52
Called from file "format.ml", line 469, characters 8-33
Called from file "format.ml", line 484, characters 6-24
TL;DR : The packet is faulty according to the uDNS primary server handle function because the question section contains more than one question. The server tries to reply with an answer containing the same question section (I think it must be done for security purposes), but it creates a buffer with a shorter length, that's why that error is raised and crashes the application.
Now for a more detailed explanation (took me a really long time to figure out !). If I understood correctly :
- The server allows any frame which size is under 4096 (otherwise it says
ERR [dns_server] partial frame (length 4096)
), so the packet goes through UDns_server.Primary.handle
is called at one point- In that call,
UDns_server.Primary.handle_inner
is called, which calls itselfUDns_server.handle_frame
- As the frame is a query-type packet, it calls
UDns_server.handle_query
UDns_server.handle_query
checks the question section and sees that there is more than one question, therefore it returns the rcodeFormErr
toUDns_server.handle_inner
UDns_server.handle_inner
, seeing that it got an rcode error, callsUDns_server.err
, which callsDns_packet.error
to create a reply with the same content but with the rcode set toFormErr
Dns_packet.error
creates a buffer of sizeDns_packet.max_reply_udp
(= 450 !!) and tries to copy the initial packet into that buffer throughDns_packet.encode_query
Dns_packet.encode_query
callsList.fold_left
usingDns_packet.encode_question
as the folding function, which shifts the offset value in the reply buffer- As 3451 is greater than 450, the final error
invalid bounds
is raised.
Maybe a solution would be to rise max_udp_size
to 4096 ?
thanks for the report :)
I added a regression test (which decodes the frame and afterwards expects error
to produce something sensible (and esp. not crash).
the fix I applied is if 450 byte are not sufficient to encode out only the first question. max_udp_size
is my sanity guard to not act as an amplifier (which maybe should be tweaked to be a ratio between incoming and outgoing packet) -- given that I don't support any frames with multiple questions, I don't think I need to send the entire question back (please let me know if there's a requirement in some RFC to always send back the entire question section).
Sending the entire question back isn't a compulsory requirement but RFC5452, a Proposed Standard RFC, advises that the question section in the answer should match the question section from the query to help against spoofed packets. There may be some resolvers implementing this RFC, so that may be an issue, although I don't really know if there are many packets asking for multiples questions in a single query.
I will continue fuzzing in that direction, there may be other cases in which it could happen. The parsing function looks really solid though, I haven't found any bugs here 🤔