There is a buffer size calculation issue in replace_string that can
result in a heap overflow with a specially crafted FTP packet.  This
is a result of integer truncation when downscaling from size_t into
uint8_t size.  Correct this by setting the types to size_t until the
underlying memmove to keep the sizes intact.

The total_size, substr_size, and rep_str_size are expected to all be
sane values for the memmove, and modify_packet also expects this, so
document that as well.  In the case of FTP, those are enforced in
repl_ftp_v*_addr at the checks for MAX_FTP_V*_NAT_DELTA, and the
packet data itself should be sanitized by the ovs_strlcpy that runs
early to extract a string of appropriate length.

Fixes: bd5e81a0e596 ("Userspace Datapath: Add ALG infra and FTP.")
Reported-by: Seiji Sakurai <[email protected]>
Signed-off-by: Aaron Conole <[email protected]>
---
 AUTHORS.rst            |   1 +
 lib/conntrack.c        |   9 +-
 tests/library.at       |   4 +
 tests/test-conntrack.c | 182 +++++++++++++++++++++++++++++++++++++++++
 4 files changed, 193 insertions(+), 3 deletions(-)

diff --git a/AUTHORS.rst b/AUTHORS.rst
index 037851ad1e..8e1bf6c075 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -763,6 +763,7 @@ Scott Hendricks
 Sean Brady                      [email protected]
 Sebastian Andrzej Siewior       [email protected]
 Sébastien RICCIO                [email protected]
+Seiji Sakurai                   [email protected]
 Shweta Seth                     [email protected]
 Simon Jouet                     [email protected]
 Spiro Kourtessis                [email protected]
diff --git a/lib/conntrack.c b/lib/conntrack.c
index 921f63cfe8..e25cc25ca8 100644
--- a/lib/conntrack.c
+++ b/lib/conntrack.c
@@ -3242,9 +3242,9 @@ expectation_create(struct conntrack *ct, ovs_be16 
dst_port,
 }
 
 static void
-replace_substring(char *substr, uint8_t substr_size,
-                  uint8_t total_size, char *rep_str,
-                  uint8_t rep_str_size)
+replace_substring(char *substr, size_t substr_size,
+                  size_t total_size, char *rep_str,
+                  size_t rep_str_size)
 {
     memmove(substr + rep_str_size, substr + substr_size,
             total_size - substr_size);
@@ -3266,6 +3266,9 @@ repl_bytes(char *str, char c1, char c2, int max)
     }
 }
 
+/* Replaces a substring in the packet and rewrites the packet
+ * size to match.  This function assumes the caller has verified
+ * the lengths to prevent under/over flow. */
 static void
 modify_packet(struct dp_packet *pkt, char *pkt_str, size_t size,
               char *repl_str, size_t repl_size,
diff --git a/tests/library.at b/tests/library.at
index 106c0abe76..449f15fd5a 100644
--- a/tests/library.at
+++ b/tests/library.at
@@ -303,3 +303,7 @@ AT_CHECK([ovstest 
test-cooperative-multitasking-nested-yield], [0], [], [dnl
 cooperative_multitasking|ERR|Nested yield avoided, this is a bug! Enable debug 
logging for more details.
 ])
 AT_CLEANUP
+
+AT_SETUP([Conntrack Library - FTP ALG parsing])
+AT_CHECK([ovstest test-conntrack ftp-alg-large-payload])
+AT_CLEANUP
diff --git a/tests/test-conntrack.c b/tests/test-conntrack.c
index dc8d6cff94..81f414f39f 100644
--- a/tests/test-conntrack.c
+++ b/tests/test-conntrack.c
@@ -54,6 +54,100 @@ build_packet(uint16_t udp_src, uint16_t udp_dst, ovs_be16 
*dl_type)
     return pkt;
 }
 
+/* Build an Ethernet + IPv4 packet.  If 'pkt' is NULL a new buffer is
+ * allocated with 64 bytes of extra headroom so the FTP MTU guard passes.
+ * The buffer is populated up through the IP header; l4 is set to point
+ * directly after the IP header.  The caller is responsible for filling
+ * the L4 header and payload that follow. */
+static struct dp_packet *
+build_eth_ip_packet(struct dp_packet *pkt, struct eth_addr eth_src,
+                    struct eth_addr eth_dst, ovs_be32 ip_src, ovs_be32 ip_dst,
+                    uint8_t proto, uint16_t payload_alloc)
+{
+    struct ip_header *iph;
+    uint16_t proto_len;
+
+    switch (proto) {
+    case IPPROTO_TCP:  proto_len = TCP_HEADER_LEN;  break;
+    case IPPROTO_UDP:  proto_len = UDP_HEADER_LEN;  break;
+    case IPPROTO_ICMP: proto_len = ICMP_HEADER_LEN; break;
+    default:           proto_len = 0;               break;
+    }
+
+    if (pkt == NULL) {
+        /* 64-byte extra headroom keeps dp_packet_get_allocated() large enough
+         * that the FTP V4 MTU guard (orig_used_size + 8 <= allocated) passes
+         * even when the packet is near its maximum size. */
+        pkt = dp_packet_new_with_headroom(ETH_HEADER_LEN + IP_HEADER_LEN
+                                          + proto_len + payload_alloc, 64);
+    }
+
+    eth_compose(pkt, eth_src, eth_dst, ETH_TYPE_IP,
+                IP_HEADER_LEN + proto_len + payload_alloc);
+    iph = dp_packet_l3(pkt);
+    iph->ip_ihl_ver = IP_IHL_VER(5, 4);
+    iph->ip_tot_len = htons(IP_HEADER_LEN + proto_len + payload_alloc);
+    iph->ip_ttl = 64;
+    iph->ip_proto = proto;
+    packet_set_ipv4_addr(pkt, &iph->ip_src, ip_src);
+    packet_set_ipv4_addr(pkt, &iph->ip_dst, ip_dst);
+    iph->ip_csum = csum(iph, IP_HEADER_LEN);
+    dp_packet_set_l4(pkt, (char *) iph + IP_HEADER_LEN);
+    return pkt;
+}
+
+/* Fill the TCP header and optional payload for a packet previously built with
+ * build_eth_ip_packet().  The 'payload' buffer of 'payload_len' bytes is
+ * appended after the TCP header if non-NULL.  IP total-length, IP checksum,
+ * and TCP checksum are all updated to reflect the final packet contents. */
+static struct dp_packet *
+build_tcp_packet(struct dp_packet *pkt, uint16_t tcp_src, uint16_t tcp_dst,
+                 uint16_t tcp_flags, const char *tcp_payload,
+                 size_t payload_len)
+{
+    struct tcp_header *tcph;
+    struct ip_header *iph;
+    uint16_t ip_tot_len;
+    uint32_t tcp_csum;
+    struct flow flow;
+
+    ovs_assert(pkt);
+    tcph = dp_packet_l4(pkt);
+    ovs_assert(tcph);
+
+    tcph->tcp_src = htons(tcp_src);
+    tcph->tcp_dst = htons(tcp_dst);
+    put_16aligned_be32(&tcph->tcp_seq, 0);
+    put_16aligned_be32(&tcph->tcp_ack, 0);
+    tcph->tcp_ctl = TCP_CTL(tcp_flags, TCP_HEADER_LEN / 4);
+    tcph->tcp_winsz = htons(65535);
+    tcph->tcp_csum = 0;
+    tcph->tcp_urg = 0;
+
+    if (tcp_payload && payload_len > 0) {
+        /* The caller must have pre-allocated space via build_eth_ip_packet's
+         * payload_alloc argument.  Write directly to avoid a realloc that
+         * would lose the extra headroom required by the FTP MTU guard. */
+        memcpy((char *) tcph + TCP_HEADER_LEN, tcp_payload, payload_len);
+    }
+
+    /* Update IP total length and recompute IP checksum. */
+    iph = dp_packet_l3(pkt);
+    ip_tot_len = IP_HEADER_LEN + TCP_HEADER_LEN + payload_len;
+    iph->ip_tot_len = htons(ip_tot_len);
+    iph->ip_csum = 0;
+    iph->ip_csum = csum(iph, IP_HEADER_LEN);
+
+    /* Compute TCP checksum over pseudo-header + TCP segment. */
+    tcp_csum = packet_csum_pseudoheader(iph);
+    tcph->tcp_csum = csum_finish(
+        csum_continue(tcp_csum, tcph, TCP_HEADER_LEN + payload_len));
+
+    /* Set l3/l4 offsets so conntrack can extract a flow key. */
+    flow_extract(pkt, &flow);
+    return pkt;
+}
+
 static struct dp_packet_batch *
 prepare_packets(size_t n, bool change, unsigned tid, ovs_be16 *dl_type)
 {
@@ -397,6 +491,87 @@ test_pcap(struct ovs_cmdl_context *ctx)
     conntrack_destroy(ct);
     ovs_pcap_close(pcap);
 }
+
+/* ALG related testing. */
+
+/* FTP IPv4 PORT payload for testing. */
+#define FTP_PORT_CMD_STR  "PORT 192,168,123,2,113,42\r\n"
+#define FTP_CMD_PAD       234
+#define FTP_PAYLOAD_LEN   (sizeof FTP_PORT_CMD_STR - 1 + FTP_CMD_PAD)
+
+/* Test modify_packet wrapping.
+ *
+ * The test builds a minimal FTP control-channel exchange:
+ *   1. A TCP SYN that creates a conntrack entry with helper=ftp and SNAT.
+ *   2. A PSH|ACK carrying "PORT 192,168,123,2,113,42\r\n" padded to exactly
+ *      261 bytes of TCP payload, which makes total_size == 256.
+ *
+ * After the PORT packet is processed the address field in the payload must
+ * read "192,168,1,1" (the SNAT address with dots replaced by commas). */
+static void
+test_ftp_alg_large_payload(struct ovs_cmdl_context *ctx OVS_UNUSED)
+{
+    /* Packet endpoints. */
+    struct eth_addr eth_src = ETH_ADDR_C(00, 01, 02, 03, 04, 05);
+    struct eth_addr eth_dst = ETH_ADDR_C(00, 06, 07, 08, 09, 0a);
+    ovs_be32 ip_src = inet_addr("192.168.123.2"); /* FTP client */
+    ovs_be32 ip_dst = inet_addr("192.168.1.1");   /* FTP server / SNAT addr */
+    uint16_t sport = 12345;
+    uint16_t dport = 21;                          /* FTP control port */
+
+    /* SNAT: rewrite client address to 192.168.1.1 in PORT commands. */
+    struct nat_action_info_t nat_info;
+    memset(&nat_info, 0, sizeof nat_info);
+    nat_info.nat_action = NAT_ACTION_SRC;
+    nat_info.min_addr.ipv4 = ip_dst;
+    nat_info.max_addr.ipv4 = ip_dst;
+
+    struct conntrack *ct_ = conntrack_init();
+    conntrack_set_tcp_seq_chk(ct_, false);
+
+    long long now = time_msec();
+
+    struct dp_packet *syn = build_eth_ip_packet(NULL, eth_src, eth_dst,
+                                                ip_src, ip_dst,
+                                                IPPROTO_TCP, 0);
+    build_tcp_packet(syn, sport, dport, TCP_SYN, NULL, 0);
+
+    struct dp_packet_batch syn_batch;
+    dp_packet_batch_init_packet(&syn_batch, syn);
+    conntrack_execute(ct_, &syn_batch, htons(ETH_TYPE_IP), false, true, 0,
+                      NULL, NULL, "ftp", &nat_info, now, 0);
+    dp_packet_delete_batch(&syn_batch, true);
+
+    /* We get to skip some of the processing because the conntrack execute
+     * above will create the required conntrack entries. */
+
+    /* Build the large payload: PORT command followed by padding spaces
+     * and a final "\r\n" to reach exactly FTP_PAYLOAD_LEN bytes.  The
+     * FTP parser only looks at the first LARGEST_FTP_MSG_OF_INTEREST (128)
+     * bytes, so the trailing spaces do not interfere with parsing. */
+    char ftp_payload[FTP_PAYLOAD_LEN];
+    memcpy(ftp_payload, FTP_PORT_CMD_STR, sizeof FTP_PORT_CMD_STR - 1);
+    memset(ftp_payload + sizeof FTP_PORT_CMD_STR - 1, ' ', FTP_CMD_PAD);
+
+    struct dp_packet *port_pkt =
+        build_eth_ip_packet(NULL, eth_src, eth_dst, ip_src, ip_dst,
+                            IPPROTO_TCP, FTP_PAYLOAD_LEN);
+    build_tcp_packet(port_pkt, sport, dport, TCP_PSH | TCP_ACK,
+                     ftp_payload, FTP_PAYLOAD_LEN);
+
+    struct dp_packet_batch port_batch;
+    dp_packet_batch_init_packet(&port_batch, port_pkt);
+    conntrack_execute(ct_, &port_batch, htons(ETH_TYPE_IP), false, true, 0,
+                      NULL, NULL, "ftp", &nat_info, now, 0);
+
+    struct tcp_header *th = dp_packet_l4(port_pkt);
+    size_t tcp_hdr_len = TCP_OFFSET(th->tcp_ctl) * 4;
+    const char *ftp_start = (const char *) th + tcp_hdr_len;
+    ovs_assert(!strncmp(ftp_start, "PORT 192,168,1,1,", 17));
+    dp_packet_delete_batch(&port_batch, true);
+    conntrack_destroy(ct_);
+}
+
 
 static const struct ovs_cmdl_command commands[] = {
     /* Connection tracker tests. */
@@ -415,6 +590,13 @@ static const struct ovs_cmdl_command commands[] = {
      * and an empty zone. */
     {"benchmark-zones", "n_conns n_zones iterations", 3, 3,
         test_benchmark_zones, OVS_RO},
+    /* Verifies that the FTP ALG replace_substring function correctly handles
+     * a packet whose payload puts total_size at exactly 256 bytes.  The
+     * original uint8_t parameter type truncated 256 to 0, leading to a
+     * near-SIZE_MAX memmove (heap overflow).  The test confirms the address
+     * is rewritten to the SNAT target rather than causing a crash. */
+    {"ftp-alg-large-payload", "", 0, 0,
+        test_ftp_alg_large_payload, OVS_RO},
 
     {NULL, NULL, 0, 0, NULL, OVS_RO},
 };
-- 
2.51.0

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to