commit b003a0da7865caa25b5d1e70c79329b32409b02a (HEAD -> refs/heads/v4, refs/remotes/origin/v4)
Author: Amos Jeffries <yadij@users.noreply.github.com>
Date:   2021-09-24 21:53:11 +0000

    WCCP: Validate packets better (#899)
    
    Update WCCP to support exception based error handling for
    parsing and processing we are moving Squid to for protocol
    handling.
    
    Update the main WCCPv2 parsing checks to throw meaningful
    exceptions when detected.

diff --git a/src/wccp2.cc b/src/wccp2.cc
index ee592449c..6ef469e91 100644
--- a/src/wccp2.cc
+++ b/src/wccp2.cc
@@ -1108,6 +1108,59 @@ wccp2ConnectionClose(void)
  * Functions for handling the requests.
  */
 
+/// Checks that the given area section ends inside the given (whole) area.
+/// \param error the message to throw when the section does not fit
+static void
+CheckSectionLength(const void *sectionStart, const size_t sectionLength, const void *wholeStart, const size_t wholeSize, const char *error)
+{
+    assert(sectionStart);
+    assert(wholeStart);
+
+    const auto wholeEnd = static_cast<const char*>(wholeStart) + wholeSize;
+    assert(sectionStart >= wholeStart && "we never go backwards");
+    assert(sectionStart <= wholeEnd && "we never go beyond our whole (but zero-sized fields are OK)");
+    static_assert(sizeof(wccp2_i_see_you_t) <= PTRDIFF_MAX, "paranoid: no UB when subtracting in-whole pointers");
+    // subtraction safe due to the three assertions above
+    const auto remainderDiff = wholeEnd - static_cast<const char*>(sectionStart);
+
+    // casting safe due to the assertions above (and size_t definition)
+    assert(remainderDiff >= 0);
+    const auto remainderSize = static_cast<size_t>(remainderDiff);
+
+    if (sectionLength <= remainderSize)
+        return;
+
+    throw TextException(error, Here());
+}
+
+/// Checks that the area contains at least dataLength bytes after the header.
+/// The size of the field header itself is not included in dataLength.
+/// \returns the total field size -- the field header and field data combined
+template<class FieldHeader>
+static size_t
+CheckFieldDataLength(const FieldHeader *header, const size_t dataLength, const void *areaStart, const size_t areaSize, const char *error)
+{
+    assert(header);
+    const auto dataStart = reinterpret_cast<const char*>(header) + sizeof(header);
+    CheckSectionLength(dataStart, dataLength, areaStart, areaSize, error);
+    return sizeof(header) + dataLength; // no overflow after CheckSectionLength()
+}
+
+/// Positions the given field at a given start within a given packet area.
+/// The Field type determines the correct field size (used for bounds checking).
+/// \param field the field pointer the function should set
+/// \param areaStart the start of a packet (sub)structure containing the field
+/// \param areaSize the size of the packet (sub)structure starting at areaStart
+/// \param fieldStart the start of a field within the given area
+/// \param error the message to throw when the field does not fit the area
+template<class Field>
+static void
+SetField(Field *&field, const void *fieldStart, const void *areaStart, const size_t areaSize, const char *error)
+{
+    CheckSectionLength(fieldStart, sizeof(Field), areaStart, areaSize, error);
+    field = static_cast<Field*>(const_cast<void*>(fieldStart));
+}
+
 /*
  * Accept the UDP packet
  */
@@ -1124,8 +1177,6 @@ wccp2HandleUdp(int sock, void *)
 
     /* These structs form the parts of the packet */
 
-    struct wccp2_item_header_t *header = NULL;
-
     struct wccp2_security_none_t *security_info = NULL;
 
     struct wccp2_service_info_t *service_info = NULL;
@@ -1141,14 +1192,13 @@ wccp2HandleUdp(int sock, void *)
     struct wccp2_cache_identity_info_t *cache_identity = NULL;
 
     struct wccp2_capability_info_header_t *router_capability_header = NULL;
+    char *router_capability_data_start = nullptr;
 
     struct wccp2_capability_element_t *router_capability_element;
 
     struct sockaddr_in from;
 
     struct in_addr cache_address;
-    int len, found;
-    short int data_length, offset;
     uint32_t tmp;
     char *ptr;
     int num_caches;
@@ -1161,20 +1211,18 @@ wccp2HandleUdp(int sock, void *)
     Ip::Address from_tmp;
     from_tmp.setIPv4();
 
-    len = comm_udp_recvfrom(sock,
-                            &wccp2_i_see_you,
-                            WCCP_RESPONSE_SIZE,
-                            0,
-                            from_tmp);
+    const auto lenOrError = comm_udp_recvfrom(sock, &wccp2_i_see_you, WCCP_RESPONSE_SIZE, 0, from_tmp);
 
-    if (len < 0)
+    if (lenOrError < 0)
         return;
+    const auto len = static_cast<size_t>(lenOrError);
 
-    if (ntohs(wccp2_i_see_you.version) != WCCP2_VERSION)
-        return;
-
-    if (ntohl(wccp2_i_see_you.type) != WCCP2_I_SEE_YOU)
-        return;
+    try {
+        // TODO: Remove wccp2_i_see_you.data and use a buffer to read messages.
+        const auto message_header_size = sizeof(wccp2_i_see_you) - sizeof(wccp2_i_see_you.data);
+        Must2(len >= message_header_size, "incomplete WCCP message header");
+        Must2(ntohs(wccp2_i_see_you.version) == WCCP2_VERSION, "WCCP version unsupported");
+        Must2(ntohl(wccp2_i_see_you.type) == WCCP2_I_SEE_YOU, "WCCP packet type unsupported");
 
     /* FIXME INET6 : drop conversion boundary */
     from_tmp.getSockAddr(from);
@@ -1182,73 +1230,60 @@ wccp2HandleUdp(int sock, void *)
     debugs(80, 3, "Incoming WCCPv2 I_SEE_YOU length " << ntohs(wccp2_i_see_you.length) << ".");
 
     /* Record the total data length */
-    data_length = ntohs(wccp2_i_see_you.length);
+    const auto data_length = ntohs(wccp2_i_see_you.length);
+    Must2(data_length <= len - message_header_size,
+          "malformed packet claiming it's bigger than received data");
 
-    offset = 0;
-
-    if (data_length > len) {
-        debugs(80, DBG_IMPORTANT, "ERROR: Malformed WCCPv2 packet claiming it's bigger than received data");
-        return;
-    }
+    size_t offset = 0;
 
     /* Go through the data structure */
-    while (data_length > offset) {
+    while (offset + sizeof(struct wccp2_item_header_t) <= data_length) {
 
         char *data = wccp2_i_see_you.data;
 
-        header = (struct wccp2_item_header_t *) &data[offset];
+        const auto itemHeader = reinterpret_cast<const wccp2_item_header_t*>(&data[offset]);
+        const auto itemSize = CheckFieldDataLength(itemHeader, ntohs(itemHeader->length),
+                              data, data_length, "truncated record");
+        // XXX: Check "The specified length must be a multiple of 4 octets"
+        // requirement to avoid unaligned memory reads after the first item.
 
-        switch (ntohs(header->type)) {
+        switch (ntohs(itemHeader->type)) {
 
         case WCCP2_SECURITY_INFO:
-
-            if (security_info != NULL) {
-                debugs(80, DBG_IMPORTANT, "Duplicate security definition");
-                return;
-            }
-
-            security_info = (struct wccp2_security_none_t *) &wccp2_i_see_you.data[offset];
+            Must2(!security_info, "duplicate security definition");
+            SetField(security_info, itemHeader, itemHeader, itemSize,
+                     "security definition truncated");
             break;
 
         case WCCP2_SERVICE_INFO:
-
-            if (service_info != NULL) {
-                debugs(80, DBG_IMPORTANT, "Duplicate service_info definition");
-                return;
-            }
-
-            service_info = (struct wccp2_service_info_t *) &wccp2_i_see_you.data[offset];
+            Must2(!service_info, "duplicate service_info definition");
+            SetField(service_info, itemHeader, itemHeader, itemSize,
+                     "service_info definition truncated");
             break;
 
         case WCCP2_ROUTER_ID_INFO:
-
-            if (router_identity_info != NULL) {
-                debugs(80, DBG_IMPORTANT, "Duplicate router_identity_info definition");
-                return;
-            }
-
-            router_identity_info = (struct router_identity_info_t *) &wccp2_i_see_you.data[offset];
+            Must2(!router_identity_info, "duplicate router_identity_info definition");
+            SetField(router_identity_info, itemHeader, itemHeader, itemSize,
+                     "router_identity_info definition truncated");
             break;
 
         case WCCP2_RTR_VIEW_INFO:
-
-            if (router_view_header != NULL) {
-                debugs(80, DBG_IMPORTANT, "Duplicate router_view definition");
-                return;
-            }
-
-            router_view_header = (struct router_view_t *) &wccp2_i_see_you.data[offset];
+            Must2(!router_view_header, "duplicate router_view definition");
+            SetField(router_view_header, itemHeader, itemHeader, itemSize,
+                     "router_view definition truncated");
             break;
 
-        case WCCP2_CAPABILITY_INFO:
-
-            if (router_capability_header != NULL) {
-                debugs(80, DBG_IMPORTANT, "Duplicate router_capability definition");
-                return;
-            }
+        case WCCP2_CAPABILITY_INFO: {
+            Must2(!router_capability_header, "duplicate router_capability definition");
+            SetField(router_capability_header, itemHeader, itemHeader, itemSize,
+                     "router_capability definition truncated");
 
-            router_capability_header = (struct wccp2_capability_info_header_t *) &wccp2_i_see_you.data[offset];
+            CheckFieldDataLength(router_capability_header, ntohs(router_capability_header->capability_info_length),
+                                 itemHeader, itemSize, "capability info truncated");
+            router_capability_data_start = reinterpret_cast<char*>(router_capability_header) +
+                                           sizeof(*router_capability_header);
             break;
+        }
 
         /* Nothing to do for the types below */
 
@@ -1257,22 +1292,17 @@ wccp2HandleUdp(int sock, void *)
             break;
 
         default:
-            debugs(80, DBG_IMPORTANT, "Unknown record type in WCCPv2 Packet (" << ntohs(header->type) << ").");
+            debugs(80, DBG_IMPORTANT, "Unknown record type in WCCPv2 Packet (" << ntohs(itemHeader->type) << ").");
         }
 
-        offset += sizeof(struct wccp2_item_header_t);
-        offset += ntohs(header->length);
-
-        if (offset > data_length) {
-            debugs(80, DBG_IMPORTANT, "Error: WCCPv2 packet tried to tell us there is data beyond the end of the packet");
-            return;
-        }
+        offset += itemSize;
+        assert(offset <= data_length && "CheckFieldDataLength(itemHeader...) established that");
     }
 
-    if ((security_info == NULL) || (service_info == NULL) || (router_identity_info == NULL) || (router_view_header == NULL)) {
-        debugs(80, DBG_IMPORTANT, "Incomplete WCCPv2 Packet");
-        return;
-    }
+    Must2(security_info, "packet missing security definition");
+    Must2(service_info, "packet missing service_info definition");
+    Must2(router_identity_info, "packet missing router_identity_info definition");
+    Must2(router_view_header, "packet missing router_view definition");
 
     debugs(80, 5, "Complete packet received");
 
@@ -1308,10 +1338,7 @@ wccp2HandleUdp(int sock, void *)
             break;
     }
 
-    if (router_list_ptr->next == NULL) {
-        debugs(80, DBG_IMPORTANT, "WCCPv2 Packet received from unknown router");
-        return;
-    }
+    Must2(router_list_ptr->next, "packet received from unknown router");
 
     /* Set the router id */
     router_list_ptr->info->router_address = router_identity_info->router_id_element.router_address;
@@ -1331,11 +1358,20 @@ wccp2HandleUdp(int sock, void *)
         }
     } else {
 
-        char *end = ((char *) router_capability_header) + sizeof(*router_capability_header) + ntohs(router_capability_header->capability_info_length) - sizeof(struct wccp2_capability_info_header_t);
-
-        router_capability_element = (struct wccp2_capability_element_t *) (((char *) router_capability_header) + sizeof(*router_capability_header));
-
-        while ((char *) router_capability_element <= end) {
+        const auto router_capability_data_length = ntohs(router_capability_header->capability_info_length);
+        assert(router_capability_data_start);
+        const auto router_capability_data_end = router_capability_data_start +
+                                                router_capability_data_length;
+        for (auto router_capability_data_current = router_capability_data_start;
+                router_capability_data_current < router_capability_data_end;) {
+
+            SetField(router_capability_element, router_capability_data_current,
+                     router_capability_data_start, router_capability_data_length,
+                     "capability element header truncated");
+            const auto elementSize = CheckFieldDataLength(
+                                         router_capability_element, ntohs(router_capability_element->capability_length),
+                                         router_capability_data_start, router_capability_data_length,
+                                         "capability element truncated");
 
             switch (ntohs(router_capability_element->capability_type)) {
 
@@ -1377,7 +1413,7 @@ wccp2HandleUdp(int sock, void *)
                 debugs(80, DBG_IMPORTANT, "Unknown capability type in WCCPv2 Packet (" << ntohs(router_capability_element->capability_type) << ").");
             }
 
-            router_capability_element = (struct wccp2_capability_element_t *) (((char *) router_capability_element) + sizeof(struct wccp2_item_header_t) + ntohs(router_capability_element->capability_length));
+            router_capability_data_current += elementSize;
         }
     }
 
@@ -1396,23 +1432,34 @@ wccp2HandleUdp(int sock, void *)
     num_caches = 0;
 
     /* Check to see if we're the master cache and update the cache list */
-    found = 0;
+    bool found = false;
     service_list_ptr->lowest_ip = 1;
     cache_list_ptr = &router_list_ptr->cache_list_head;
 
     /* to find the list of caches, we start at the end of the router view header */
 
     ptr = (char *) (router_view_header) + sizeof(struct router_view_t);
+    const auto router_view_size = sizeof(struct router_view_t) +
+                                  ntohs(router_view_header->header.length);
 
     /* Then we read the number of routers */
-    memcpy(&tmp, ptr, sizeof(tmp));
+    const uint32_t *routerCountRaw = nullptr;
+    SetField(routerCountRaw, ptr, router_view_header, router_view_size,
+             "malformed packet (truncated router view info w/o number of routers)");
 
     /* skip the number plus all the ip's */
-
-    ptr += sizeof(tmp) + (ntohl(tmp) * sizeof(struct in_addr));
+    ptr += sizeof(*routerCountRaw);
+    const auto ipCount = ntohl(*routerCountRaw);
+    const auto ipsSize = ipCount * sizeof(struct in_addr); // we check for unsigned overflow below
+    Must2(ipsSize / sizeof(struct in_addr) != ipCount, "huge IP address count");
+    CheckSectionLength(ptr, ipsSize, router_view_header, router_view_size, "invalid IP address count");
+    ptr += ipsSize;
 
     /* Then read the number of caches */
-    memcpy(&tmp, ptr, sizeof(tmp));
+    const uint32_t *cacheCountRaw = nullptr;
+    SetField(cacheCountRaw, ptr, router_view_header, router_view_size,
+             "malformed packet (truncated router view info w/o cache count)");
+    memcpy(&tmp, cacheCountRaw, sizeof(tmp)); // TODO: Replace tmp with cacheCount
     ptr += sizeof(tmp);
 
     if (ntohl(tmp) != 0) {
@@ -1426,7 +1473,8 @@ wccp2HandleUdp(int sock, void *)
 
             case WCCP2_ASSIGNMENT_METHOD_HASH:
 
-                cache_identity = (struct wccp2_cache_identity_info_t *) ptr;
+                SetField(cache_identity, ptr, router_view_header, router_view_size,
+                         "malformed packet (truncated router view info cache w/o assignment hash)");
 
                 ptr += sizeof(struct wccp2_cache_identity_info_t);
 
@@ -1437,13 +1485,15 @@ wccp2HandleUdp(int sock, void *)
 
             case WCCP2_ASSIGNMENT_METHOD_MASK:
 
-                cache_mask_info = (struct cache_mask_info_t *) ptr;
+                SetField(cache_mask_info, ptr, router_view_header, router_view_size,
+                         "malformed packet (truncated router view info cache w/o assignment mask)");
 
                 /* The mask assignment has an undocumented variable length entry here */
 
                 if (ntohl(cache_mask_info->num1) == 3) {
 
-                    cache_mask_identity = (struct wccp2_cache_mask_identity_info_t *) ptr;
+                    SetField(cache_mask_identity, ptr, router_view_header, router_view_size,
+                             "malformed packet (truncated router view info cache w/o assignment mask identity)");
 
                     ptr += sizeof(struct wccp2_cache_mask_identity_info_t);
 
@@ -1474,10 +1524,7 @@ wccp2HandleUdp(int sock, void *)
             debugs (80, 5,  "checking cache list: (" << std::hex << cache_address.s_addr << ":" <<  router_list_ptr->local_ip.s_addr << ")");
 
             /* Check to see if it's the master, or us */
-
-            if (cache_address.s_addr == router_list_ptr->local_ip.s_addr) {
-                found = 1;
-            }
+            found = found || (cache_address.s_addr == router_list_ptr->local_ip.s_addr);
 
             if (cache_address.s_addr < router_list_ptr->local_ip.s_addr) {
                 service_list_ptr->lowest_ip = 0;
@@ -1494,7 +1541,7 @@ wccp2HandleUdp(int sock, void *)
         cache_list_ptr->next = NULL;
 
         service_list_ptr->lowest_ip = 1;
-        found = 1;
+        found = true;
         num_caches = 1;
     }
 
@@ -1502,7 +1549,7 @@ wccp2HandleUdp(int sock, void *)
 
     router_list_ptr->num_caches = htonl(num_caches);
 
-    if ((found == 1) && (service_list_ptr->lowest_ip == 1)) {
+    if (found && (service_list_ptr->lowest_ip == 1)) {
         if (ntohl(router_view_header->change_number) != router_list_ptr->member_change) {
             debugs(80, 4, "Change detected - queueing up new assignment");
             router_list_ptr->member_change = ntohl(router_view_header->change_number);
@@ -1515,6 +1562,10 @@ wccp2HandleUdp(int sock, void *)
         eventDelete(wccp2AssignBuckets, NULL);
         debugs(80, 5, "I am not the lowest ip cache - not assigning buckets");
     }
+
+    } catch (...) {
+        debugs(80, DBG_IMPORTANT, "ERROR: Ignoring WCCPv2 message: " << CurrentException);
+    }
 }
 
 static void
