/* * the dhcpv4 message layer. * * this is the part both the server and the client stand on, so it is tested on * its own first: it is pure - a buffer in, a buffer out, no sockets and no * timing - and every one of its failure modes is reachable from a unit test. * * the cases that matter most are the ones a hand-written parser gets wrong: * an option payload carries no alignment, a length field that disagrees with * the option's definition is a malformed packet rather than a value to * salvage, and a hardware address length is an index into a fixed field that a * hostile packet can overrun. */ #include #include #include "tap.h" #include #include #include #define BUFCAPA 512 static hio_uint8_t g_buf[BUFCAPA]; /* the walker takes no context argument, so a count has to live out here */ static int g_walked; static int g_walk_codes[32]; static int count_opt (hio_dhcp4_opt_hdr_t* opt) { if (g_walked < (int)HIO_COUNTOF(g_walk_codes)) g_walk_codes[g_walked] = opt->code; g_walked++; return 1; /* non-zero continues; 0 would stop the walk after the first */ } /* a request as a client would send it, for the reply and client-id cases */ static void make_request (hio_dhcp4_pktbuf_t* pkt, hio_uint8_t mtype) { static const hio_uint8_t mac[6] = { 0x02, 0x00, 0xde, 0xad, 0xbe, 0xef }; hio_dhcp4_init_pktbuf (pkt, g_buf, BUFCAPA); pkt->hdr->op = HIO_DHCP4_OP_BOOTREQUEST; pkt->hdr->htype = HIO_DHCP4_HTYPE_ETHERNET; pkt->hdr->hlen = 6; HIO_MEMCPY (pkt->hdr->chaddr, mac, 6); pkt->hdr->xid = hio_hton32(0x11223344); pkt->hdr->flags = hio_hton16(0x8000); /* broadcast */ pkt->hdr->giaddr = hio_hton32(0x0a000001); hio_dhcp4_add_option_uint8 (pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, mtype); } static void as_pktinf (hio_dhcp4_pktinf_t* inf, const hio_dhcp4_pktbuf_t* pkt) { inf->hdr = pkt->hdr; inf->len = pkt->len; } /* ------------------------------------------------------------------ */ static void test_cookie_and_layout (void) { hio_dhcp4_pktbuf_t pkt; hio_uint32_t cookie; OK (hio_dhcp4_init_pktbuf(&pkt, g_buf, BUFCAPA) == 0, "a packet buffer initialises to just the fixed header"); OK (pkt.len == HIO_SIZEOF(hio_dhcp4_pkt_hdr_t), "and its length is the header alone until an option is added"); OK (hio_dhcp4_add_option_uint8(&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, HIO_DHCP4_MSG_DISCOVER) == 0, "an option can be added"); /* the cookie has to be there, and in network order, or nothing downstream * will recognise the packet as dhcp at all */ HIO_MEMCPY (&cookie, &g_buf[HIO_SIZEOF(hio_dhcp4_pkt_hdr_t)], 4); OK (cookie == HIO_CONST_HTON32(HIO_DHCP4_MAGIC_COOKIE), "and adding the first one writes the magic cookie ahead of it"); /* header + cookie + (code,len,value) */ OK (pkt.len == HIO_SIZEOF(hio_dhcp4_pkt_hdr_t) + 4 + 3, "and the length accounts for the cookie, the option header and its payload"); } static void test_byte_order (void) { hio_dhcp4_pktbuf_t pkt; hio_dhcp4_pktinf_t inf; const hio_uint8_t* p; hio_uint8_t l; hio_uint16_t v16; hio_uint32_t v32; hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA); OK (hio_dhcp4_add_option_uint16(&pkt, HIO_DHCP4_OPT_MAX_SIZE, 0x0102) == 0 && hio_dhcp4_add_option_uint32(&pkt, HIO_DHCP4_OPT_LEASE_TIME, 0x01020304) == 0, "16- and 32-bit options can be added"); as_pktinf (&inf, &pkt); /* on the wire, most significant octet first - the whole reason these * helpers exist rather than callers writing the bytes themselves */ OK (hio_dhcp4_get_option_data(&inf, HIO_DHCP4_OPT_MAX_SIZE, &p, &l) == 0 && l == 2 && p[0] == 0x01 && p[1] == 0x02, "and a 16-bit option is written most significant octet first"); OK (hio_dhcp4_get_option_data(&inf, HIO_DHCP4_OPT_LEASE_TIME, &p, &l) == 0 && l == 4 && p[0] == 0x01 && p[1] == 0x02 && p[2] == 0x03 && p[3] == 0x04, "and so is a 32-bit one"); OK (hio_dhcp4_get_option_uint16(&inf, HIO_DHCP4_OPT_MAX_SIZE, &v16) == 0 && v16 == 0x0102, "and reading it back gives the host-order value again"); OK (hio_dhcp4_get_option_uint32(&inf, HIO_DHCP4_OPT_LEASE_TIME, &v32) == 0 && v32 == 0x01020304, "for both widths"); } static void test_unaligned_payload (void) { hio_dhcp4_pktbuf_t pkt; hio_dhcp4_pktinf_t inf; hio_uint32_t v32; /* an option payload starts wherever the options before it ended, so its * alignment is whatever happens to fall out. a one-octet option ahead of * the 32-bit one guarantees the latter is misaligned - which is the case * that reading through a cast gets wrong, silently on x86 and fatally * elsewhere. */ hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA); hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, HIO_DHCP4_MSG_ACK); hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_IP_TTL, 64); hio_dhcp4_add_option_uint32 (&pkt, HIO_DHCP4_OPT_LEASE_TIME, 0xDEADBEEF); as_pktinf (&inf, &pkt); OK (hio_dhcp4_get_option_uint32(&inf, HIO_DHCP4_OPT_LEASE_TIME, &v32) == 0 && v32 == 0xDEADBEEF, "a 32-bit option is read correctly from an unaligned offset"); } static void test_wrong_width_is_refused (void) { hio_dhcp4_pktbuf_t pkt; hio_dhcp4_pktinf_t inf; hio_uint8_t two[2] = { 0x12, 0x34 }; hio_uint32_t v32; hio_uint16_t v16; /* a lease time is four octets by definition. two is a malformed packet, * and the answer to that is a refusal rather than whichever two octets * happen to follow in the buffer. */ hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA); hio_dhcp4_add_option (&pkt, HIO_DHCP4_OPT_LEASE_TIME, two, 2); as_pktinf (&inf, &pkt); OK (hio_dhcp4_get_option_uint32(&inf, HIO_DHCP4_OPT_LEASE_TIME, &v32) <= -1, "an option whose length disagrees with its definition is refused"); OK (hio_dhcp4_get_option_uint16(&inf, HIO_DHCP4_OPT_LEASE_TIME, &v16) == 0 && v16 == 0x1234, "and is readable at the width it actually has"); /* an option that is not there is not an error to be papered over either */ OK (hio_dhcp4_get_option_uint32(&inf, HIO_DHCP4_OPT_T1, &v32) <= -1, "and an absent option reports absence rather than a value"); } static void test_msg_type (void) { hio_dhcp4_pktbuf_t pkt; hio_dhcp4_pktinf_t inf; hio_uint8_t mtype; hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA); hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, HIO_DHCP4_MSG_REQUEST); as_pktinf (&inf, &pkt); OK (hio_dhcp4_get_msg_type(&inf, &mtype) == 0 && mtype == HIO_DHCP4_MSG_REQUEST, "the message type is read from option 53"); /* no option 53 is what distinguishes bootp from dhcp, so it must not be * mistaken for a message type of zero */ hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA); hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_IP_TTL, 64); as_pktinf (&inf, &pkt); OK (hio_dhcp4_get_msg_type(&inf, &mtype) <= -1, "a packet without one is refused rather than read as type zero"); hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA); hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, 0); as_pktinf (&inf, &pkt); OK (hio_dhcp4_get_msg_type(&inf, &mtype) <= -1, "and nor is a type of zero accepted"); hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA); hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, 99); as_pktinf (&inf, &pkt); OK (hio_dhcp4_get_msg_type(&inf, &mtype) <= -1, "nor one this implementation does not know"); } static void test_client_id (void) { hio_dhcp4_pktbuf_t pkt; hio_dhcp4_pktinf_t inf; const hio_uint8_t* p; hio_uint8_t l; static const hio_uint8_t cid[5] = { 0xff, 'a', 'b', 'c', 'd' }; /* with no option 61, the hardware address is the identity */ make_request (&pkt, HIO_DHCP4_MSG_DISCOVER); as_pktinf (&inf, &pkt); OK (hio_dhcp4_get_client_id(&inf, &p, &l) == 0 && l == 6 && p[0] == 0x02 && p[5] == 0xef, "with no client-id option, the hardware address identifies the client"); /* with one, it wins - and it is opaque, so it is returned as it came */ make_request (&pkt, HIO_DHCP4_MSG_DISCOVER); hio_dhcp4_add_option (&pkt, HIO_DHCP4_OPT_CLIENT_ID, (void*)cid, 5); as_pktinf (&inf, &pkt); OK (hio_dhcp4_get_client_id(&inf, &p, &l) == 0 && l == 5 && HIO_MEMCMP(p, cid, 5) == 0, "and a client-id option takes precedence over it"); /* a packet may claim a hardware address longer than the field that holds * it. trusting hlen there is how a parser reads past the header. */ make_request (&pkt, HIO_DHCP4_MSG_DISCOVER); pkt.hdr->hlen = 200; as_pktinf (&inf, &pkt); OK (hio_dhcp4_get_client_id(&inf, &p, &l) <= -1, "and a hardware address longer than the field is refused rather than trusted"); } static void test_check_pkt (void) { hio_dhcp4_pktbuf_t pkt; hio_dhcp4_pktinf_t inf; make_request (&pkt, HIO_DHCP4_MSG_DISCOVER); as_pktinf (&inf, &pkt); OK (hio_dhcp4_check_pkt(&inf) == 0, "a well-formed packet passes the shape check"); /* short of the fixed header there is nothing to read at all */ as_pktinf (&inf, &pkt); inf.len = HIO_SIZEOF(hio_dhcp4_pkt_hdr_t) - 1; OK (hio_dhcp4_check_pkt(&inf) <= -1, "one shorter than the fixed header is refused"); /* and with no room for the cookie there are no options */ as_pktinf (&inf, &pkt); inf.len = HIO_SIZEOF(hio_dhcp4_pkt_hdr_t) + 2; OK (hio_dhcp4_check_pkt(&inf) <= -1, "and one with no room for the cookie is refused"); make_request (&pkt, HIO_DHCP4_MSG_DISCOVER); g_buf[HIO_SIZEOF(hio_dhcp4_pkt_hdr_t)] ^= 0xFF; /* break the cookie */ as_pktinf (&inf, &pkt); OK (hio_dhcp4_check_pkt(&inf) <= -1, "and one whose cookie is wrong is refused"); make_request (&pkt, HIO_DHCP4_MSG_DISCOVER); pkt.hdr->hlen = 200; as_pktinf (&inf, &pkt); OK (hio_dhcp4_check_pkt(&inf) <= -1, "and one claiming an oversized hardware address is refused"); } static void test_reply_echoes_the_request (void) { hio_dhcp4_pktbuf_t req, rep; hio_dhcp4_pktinf_t inf; hio_uint8_t reqbuf[BUFCAPA], repbuf[BUFCAPA]; hio_dhcp4_pktbuf_t tmp; /* build the request in its own buffer so the reply does not overwrite it */ make_request (&tmp, HIO_DHCP4_MSG_DISCOVER); HIO_MEMCPY (reqbuf, g_buf, tmp.len); req = tmp; req.hdr = (hio_dhcp4_pkt_hdr_t*)reqbuf; as_pktinf (&inf, &req); OK (hio_dhcp4_init_reply_pktbuf(&rep, repbuf, BUFCAPA, &inf) == 0, "a reply can be started from a request"); OK (rep.hdr->op == HIO_DHCP4_OP_BOOTREPLY, "and is marked as a reply"); /* these four are what let the reply reach the right client and be * recognised by it */ OK (rep.hdr->xid == req.hdr->xid, "and echoes the transaction id"); OK (rep.hdr->flags == req.hdr->flags, "and the broadcast flag"); OK (rep.hdr->giaddr == req.hdr->giaddr, "and the relay address"); OK (rep.hdr->hlen == req.hdr->hlen && rep.hdr->htype == req.hdr->htype && HIO_MEMCMP(rep.hdr->chaddr, req.hdr->chaddr, req.hdr->hlen) == 0, "and the client's hardware address"); } static void test_option_editing (void) { hio_dhcp4_pktbuf_t pkt; hio_dhcp4_pktinf_t inf; hio_uint8_t v8; hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA); hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, HIO_DHCP4_MSG_OFFER); hio_dhcp4_add_option_uint32 (&pkt, HIO_DHCP4_OPT_LEASE_TIME, 3600); hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_IP_TTL, 64); as_pktinf (&inf, &pkt); g_walked = 0; OK (hio_dhcp4_walk_options(&inf, count_opt) == 0, "the options of a packet that fills its buffer walk to the end"); OK (g_walked == 3 && g_walk_codes[0] == HIO_DHCP4_OPT_MESSAGE_TYPE && g_walk_codes[2] == HIO_DHCP4_OPT_IP_TTL, "walking the options visits each one in the order written"); OK (hio_dhcp4_delete_option(&pkt, HIO_DHCP4_OPT_LEASE_TIME) == 0, "an option can be deleted"); as_pktinf (&inf, &pkt); OK (hio_dhcp4_find_option(&inf, HIO_DHCP4_OPT_LEASE_TIME) == HIO_NULL, "and is then no longer found"); OK (hio_dhcp4_get_option_uint8(&inf, HIO_DHCP4_OPT_IP_TTL, &v8) == 0 && v8 == 64, "while the options around it survive intact"); } /* the one-octet options. PADDING and END carry no length field, and a * traversal that does not account for that goes wrong in two separate ways - * both of which every conforming packet reaches, because every conforming * packet ends with an END option. */ static void test_one_octet_options (void) { hio_dhcp4_pktbuf_t pkt; hio_dhcp4_pktinf_t inf; /* a packet whose final octet is a lone END. this is what this library's * own reply builder emits, so a walk that rejects it rejects almost * everything. */ hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA); hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, HIO_DHCP4_MSG_ACK); hio_dhcp4_add_option_uint32 (&pkt, HIO_DHCP4_OPT_LEASE_TIME, 3600); hio_dhcp4_add_option (&pkt, HIO_DHCP4_OPT_END, HIO_NULL, 0); as_pktinf (&inf, &pkt); g_walked = 0; OK (hio_dhcp4_walk_options(&inf, count_opt) == 0 && g_walked == 2, "a packet ending in a lone END option walks cleanly"); /* a padding octet between two options. padding is one octet, so advancing * by two lands the cursor inside the following option and everything read * after it is misaligned. */ hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA); hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, HIO_DHCP4_MSG_ACK); hio_dhcp4_add_option (&pkt, HIO_DHCP4_OPT_PADDING, HIO_NULL, 0); hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_IP_TTL, 64); hio_dhcp4_add_option_uint32 (&pkt, HIO_DHCP4_OPT_LEASE_TIME, 3600); hio_dhcp4_add_option (&pkt, HIO_DHCP4_OPT_END, HIO_NULL, 0); as_pktinf (&inf, &pkt); g_walked = 0; OK (hio_dhcp4_walk_options(&inf, count_opt) == 0 && g_walked == 3 && g_walk_codes[0] == HIO_DHCP4_OPT_MESSAGE_TYPE && g_walk_codes[1] == HIO_DHCP4_OPT_IP_TTL && g_walk_codes[2] == HIO_DHCP4_OPT_LEASE_TIME, "and interior padding is stepped over one octet at a time"); /* and find_option must agree with the walk about the same packet - they * are two traversals of one format, and they disagreed before */ { hio_uint32_t lt = 0; OK (hio_dhcp4_get_option_uint32(&inf, HIO_DHCP4_OPT_LEASE_TIME, <) == 0 && lt == 3600, "and find_option reads the option after the padding too"); } } static void test_overlong_option_still_refused (void) { hio_dhcp4_pktbuf_t pkt; hio_dhcp4_pktinf_t inf; hio_uint8_t* lenfield; /* the bounds check above was loosened by one octet to stop it rejecting * the last option of a well-formed packet. this is the other side of that: * a length that genuinely runs past the end must still be refused, or the * loosening would have opened a read past the buffer. */ hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA); hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, HIO_DHCP4_MSG_ACK); hio_dhcp4_add_option_uint32 (&pkt, HIO_DHCP4_OPT_LEASE_TIME, 3600); as_pktinf (&inf, &pkt); /* overstate the last option's length by one */ lenfield = &g_buf[pkt.len - 4 - 1]; OK (*lenfield == 4, "the length field is where the test expects it"); *lenfield = 5; g_walked = 0; OK (hio_dhcp4_walk_options(&inf, count_opt) <= -1, "an option length that runs past the end of the packet is still refused"); } static void test_capacity (void) { hio_dhcp4_pktbuf_t pkt; hio_uint8_t small[HIO_SIZEOF(hio_dhcp4_pkt_hdr_t) + 8]; hio_uint8_t payload[16]; OK (hio_dhcp4_init_pktbuf(&pkt, small, HIO_SIZEOF(hio_dhcp4_pkt_hdr_t) - 1) <= -1, "a buffer too small for the header is refused outright"); /* room for the cookie and a little else, so a large option must not be * written past the end - the failure a fixed-size datagram buffer makes * reachable from the network */ HIO_MEMSET (payload, 'x', HIO_SIZEOF(payload)); OK (hio_dhcp4_init_pktbuf(&pkt, small, HIO_SIZEOF(small)) == 0, "a buffer with room for the header alone initialises"); OK (hio_dhcp4_add_option(&pkt, HIO_DHCP4_OPT_HOST_NAME, payload, HIO_SIZEOF(payload)) <= -1, "and an option that would not fit is refused rather than overrunning it"); OK (pkt.len <= HIO_SIZEOF(small), "leaving the length within the buffer"); } /* ------------------------------------------------------------------ */ /* the relay agent information option, RFC 3046 */ /* option 82 carries suboptions inside its own value, in the same * code/length/value shape as options themselves but one level down. what makes * it worth testing is the arithmetic: adding a suboption grows an option whose * length field holds only 255, inside a packet buffer that is also finite - * two limits that a length computed in an octet cannot express. */ static void test_relay_suboptions (void) { hio_dhcp4_pktbuf_t pkt; hio_dhcp4_pktinf_t inf; hio_uint8_t* v; hio_uint8_t vlen = 0; static const hio_uint8_t circuit[4] = { 'e', 't', 'h', '0' }; static const hio_uint8_t remote[3] = { 'r', 'i', 'd' }; hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA); hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, HIO_DHCP4_MSG_DISCOVER); OK (hio_dhcp4_add_relay_suboption(&pkt, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, circuit, 4) == 0, "a relay suboption can be added to a packet with no relay option"); as_pktinf (&inf, &pkt); OK (hio_dhcp4_find_option(&inf, HIO_DHCP4_OPT_RELAY) != HIO_NULL, "and the relay option comes into being to hold it"); v = hio_dhcp4_find_relay_suboption_value(&inf, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, &vlen); OK (v && vlen == 4 && HIO_MEMCMP(v, circuit, 4) == 0, "and it is found again with its value intact"); OK (hio_dhcp4_add_relay_suboption(&pkt, HIO_DHCP4_OPT_RELAY_REMOTE_ID, remote, 3) == 0, "a second suboption is appended to the option that exists"); as_pktinf (&inf, &pkt); v = hio_dhcp4_find_relay_suboption_value(&inf, HIO_DHCP4_OPT_RELAY_REMOTE_ID, &vlen); OK (v && vlen == 3 && HIO_MEMCMP(v, remote, 3) == 0, "and is found by its own code"); v = hio_dhcp4_find_relay_suboption_value(&inf, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, &vlen); OK (v && vlen == 4 && HIO_MEMCMP(v, circuit, 4) == 0, "while the first is still there"); OK (hio_dhcp4_delete_relay_suboption(&pkt, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID) == 0, "one suboption can be deleted"); as_pktinf (&inf, &pkt); OK (hio_dhcp4_find_relay_suboption_value(&inf, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, &vlen) == HIO_NULL, "and is then gone"); v = hio_dhcp4_find_relay_suboption_value(&inf, HIO_DHCP4_OPT_RELAY_REMOTE_ID, &vlen); OK (v && vlen == 3 && HIO_MEMCMP(v, remote, 3) == 0, "while the other survives it"); /* deleting the last one takes the option with it rather than leaving an * empty option behind for something else to trip over */ OK (hio_dhcp4_delete_relay_suboption(&pkt, HIO_DHCP4_OPT_RELAY_REMOTE_ID) == 0, "the last suboption can be deleted too"); as_pktinf (&inf, &pkt); OK (hio_dhcp4_find_option(&inf, HIO_DHCP4_OPT_RELAY) == HIO_NULL, "and the now-empty relay option goes with it"); { hio_uint8_t mt = 0; OK (hio_dhcp4_get_msg_type(&inf, &mt) == 0 && mt == HIO_DHCP4_MSG_DISCOVER, "and the options around it are undisturbed by the shuffling"); } OK (hio_dhcp4_delete_relay_suboption(&pkt, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID) <= -1, "deleting a suboption that is not there reports failure"); } static void test_relay_suboption_limits (void) { hio_dhcp4_pktbuf_t pkt; hio_dhcp4_pktinf_t inf; hio_uint8_t big[255]; hio_uint8_t small[8]; HIO_MEMSET (big, 'x', HIO_SIZEOF(big)); HIO_MEMSET (small, 'y', HIO_SIZEOF(small)); /* a suboption costs two octets of header on top of its value, and an * option length field holds 255 - so 254 does not fit. computed in an * octet that sum wraps, and a huge suboption becomes a zero-length option: * worse than a refusal, because nothing reports it. */ hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA); OK (hio_dhcp4_add_relay_suboption(&pkt, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, big, 254) <= -1, "a suboption too large for an option length field is refused"); OK (hio_dhcp4_add_relay_suboption(&pkt, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, big, 253) == 0, "and the largest one that does fit is accepted"); /* the same arithmetic one level up: appending to an option already at its * limit would take its length past 255 */ OK (hio_dhcp4_add_relay_suboption(&pkt, HIO_DHCP4_OPT_RELAY_REMOTE_ID, small, 8) <= -1, "a suboption that would take the option past 255 octets is refused"); as_pktinf (&inf, &pkt); { hio_dhcp4_opt_hdr_t* o = hio_dhcp4_find_option(&inf, HIO_DHCP4_OPT_RELAY); OK (o && o->len == 255, "leaving the option at its true length, not a wrapped one"); } /* and the packet buffer is a limit of its own */ { hio_uint8_t tiny[HIO_SIZEOF(hio_dhcp4_pkt_hdr_t) + 16]; hio_dhcp4_pktbuf_t tp; hio_dhcp4_init_pktbuf (&tp, tiny, HIO_SIZEOF(tiny)); hio_dhcp4_add_option_uint8 (&tp, HIO_DHCP4_OPT_MESSAGE_TYPE, HIO_DHCP4_MSG_DISCOVER); OK (hio_dhcp4_add_relay_suboption(&tp, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, big, 100) <= -1, "and one that would not fit the packet buffer is refused"); OK (tp.len <= HIO_SIZEOF(tiny), "leaving the length within the buffer"); } } static void test_relay_suboption_bounds (void) { hio_uint8_t raw[8]; hio_uint8_t* v; hio_uint8_t vlen = 99; /* a suboption claiming more than the enclosing option holds. the value * pointer returned for it would be read that far by the caller - past the * packet, when the relay option is the last one in it. */ raw[0] = HIO_DHCP4_OPT_RELAY_CIRCUIT_ID; raw[1] = 200; raw[2] = 'a'; raw[3] = 'b'; v = hio_dhcp4_get_relay_suboption_value(raw, 4, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, &vlen); OK (v == HIO_NULL, "a suboption whose length overruns its option is refused"); raw[0] = HIO_DHCP4_OPT_RELAY_CIRCUIT_ID; v = hio_dhcp4_get_relay_suboption_value(raw, 1, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, &vlen); OK (v == HIO_NULL, "and so is a truncated suboption header"); /* the length pointer is optional - a caller may only want the value */ raw[0] = HIO_DHCP4_OPT_RELAY_REMOTE_ID; raw[1] = 2; raw[2] = 'z'; raw[3] = 'z'; v = hio_dhcp4_get_relay_suboption_value(raw, 4, HIO_DHCP4_OPT_RELAY_REMOTE_ID, HIO_NULL); OK (v != HIO_NULL && v[0] == 'z', "and the length pointer may be omitted"); } static void test_option_value_accessor (void) { hio_dhcp4_pktbuf_t pkt; hio_dhcp4_pktinf_t inf; hio_uint8_t* v; hio_uint8_t vlen = 0; static const hio_bch_t name[] = "host.example"; hio_dhcp4_init_pktbuf (&pkt, g_buf, BUFCAPA); hio_dhcp4_add_option_uint8 (&pkt, HIO_DHCP4_OPT_MESSAGE_TYPE, HIO_DHCP4_MSG_DISCOVER); hio_dhcp4_add_option (&pkt, HIO_DHCP4_OPT_HOST_NAME, (void*)name, HIO_SIZEOF(name) - 1); as_pktinf (&inf, &pkt); v = hio_dhcp4_get_option_value(&inf, HIO_DHCP4_OPT_HOST_NAME, &vlen); OK (v && vlen == HIO_SIZEOF(name) - 1 && HIO_MEMCMP(v, name, vlen) == 0, "an option's value is reachable without the caller stepping over its header"); OK (hio_dhcp4_get_option_value(&inf, HIO_DHCP4_OPT_ROOT_PATH, &vlen) == HIO_NULL, "and an absent option yields nothing rather than a header pointer"); } /* a code is one octet on the wire, but the argument carrying it is an int, and * it reaches the packet through a plain assignment. so a code past 255 was * truncated: adding 'suboption 257' wrote suboption 1 - an Agent Circuit ID * nobody asked for - and reported success, leaving data in the packet that the * code the caller passed could never find again. */ static void test_code_must_fit_an_octet (void) { hio_dhcp4_pktbuf_t pkt; hio_uint8_t olen; hio_oow_t before; /* --- the option level --- */ make_request (&pkt, HIO_DHCP4_MSG_DISCOVER); before = pkt.len; OK (hio_dhcp4_add_option(&pkt, 0x101, "x", 1) <= -1, "an option code past one octet is refused"); OK (hio_dhcp4_add_option(&pkt, -1, "x", 1) <= -1, "and a negative one"); OK (pkt.len == before, "neither wrote anything"); OK (hio_dhcp4_find_option((hio_dhcp4_pktinf_t*)&pkt, HIO_DHCP4_OPT_SUBNET) == HIO_NULL, "and option 1 was not created by the truncation of 0x101"); /* the ends of the octet range are legitimate codes: PADDING is 0 and * END is 255, so the bound is the octet, not a narrower guess */ OK (hio_dhcp4_add_option(&pkt, HIO_DHCP4_OPT_PADDING, HIO_NULL, 0) == 0, "padding still adds"); OK (hio_dhcp4_add_option(&pkt, HIO_DHCP4_OPT_END, HIO_NULL, 0) == 0, "and so does END"); OK (pkt.len == before + 2, "one octet each"); /* --- the suboption level --- */ make_request (&pkt, HIO_DHCP4_MSG_DISCOVER); before = pkt.len; OK (hio_dhcp4_add_relay_suboption(&pkt, 0x101, (const hio_uint8_t*)"secret", 6) <= -1, "a suboption code past one octet is refused"); OK (pkt.len == before, "nothing was added"); OK (hio_dhcp4_find_option((hio_dhcp4_pktinf_t*)&pkt, HIO_DHCP4_OPT_RELAY) == HIO_NULL, "no relay option was created"); olen = 0xff; OK (hio_dhcp4_find_relay_suboption_value((hio_dhcp4_pktinf_t*)&pkt, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, &olen) == HIO_NULL, "and in particular no circuit id, which 0x101 truncates to"); /* 0 is refused on the way in as well as on the way out: a suboption * stored under it could never be looked up or deleted again, since this * api spends 0 as its 'no suboption wanted' sentinel */ OK (hio_dhcp4_add_relay_suboption(&pkt, 0, (const hio_uint8_t*)"zero", 4) <= -1, "a suboption code of 0 is refused when adding"); OK (pkt.len == before, "leaving the packet as it was"); /* and the whole legitimate range still works at both ends */ OK (hio_dhcp4_add_relay_suboption(&pkt, 1, (const hio_uint8_t*)"a", 1) == 0, "code 1 adds"); OK (hio_dhcp4_delete_relay_suboption(&pkt, 1) == 0, "and deletes"); OK (hio_dhcp4_add_relay_suboption(&pkt, 255, (const hio_uint8_t*)"b", 1) == 0, "code 255 adds"); OK (hio_dhcp4_find_relay_suboption_value((hio_dhcp4_pktinf_t*)&pkt, 255, &olen) != HIO_NULL, "and is findable by its own code"); OK (hio_dhcp4_delete_relay_suboption(&pkt, 255) == 0, "and deletes too"); OK (pkt.len == before, "back where it started"); } /* dhcp4_find_option() only looks inside a relay option for a suboption when it * is asked for one - a code above zero - and otherwise returns the relay option * having written neither the value pointer nor the value length. the two * suboption entry points go on to use both, so a code of zero or below has to * be refused before the lookup rather than after it. there is no suboption 0 * to ask for in the first place. */ static void test_suboption_code_zero (void) { hio_dhcp4_pktbuf_t pkt; hio_uint8_t olen; hio_uint8_t* v; hio_oow_t before; make_request (&pkt, HIO_DHCP4_MSG_DISCOVER); OK (hio_dhcp4_add_relay_suboption(&pkt, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, (const hio_uint8_t*)"eth0", 4) == 0, "a relay option with one suboption"); before = pkt.len; /* the suboption that is there reads back */ olen = 0xff; v = hio_dhcp4_find_relay_suboption_value((hio_dhcp4_pktinf_t*)&pkt, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID, &olen); OK (v != HIO_NULL && olen == 4 && HIO_MEMCMP(v, "eth0", 4) == 0, "and it is found by its own code"); /* code 0 is not a suboption. the relay option is present, so a lookup * that forgot to check would find it and hand back a value pointer that * was never written. */ olen = 0xff; v = hio_dhcp4_find_relay_suboption_value((hio_dhcp4_pktinf_t*)&pkt, 0, &olen); OK (v == HIO_NULL, "a suboption code of 0 finds nothing"); OK (olen == 0xff, "and the caller's length is left alone rather than half-written"); v = hio_dhcp4_find_relay_suboption_value((hio_dhcp4_pktinf_t*)&pkt, -1, &olen); OK (v == HIO_NULL, "nor does a negative one"); /* and nothing above an octet can name a suboption either */ olen = 0xff; OK (hio_dhcp4_find_relay_suboption_value((hio_dhcp4_pktinf_t*)&pkt, 256, &olen) == HIO_NULL, "a code past one octet finds nothing"); OK (olen == 0xff, "with the caller's length untouched"); OK (hio_dhcp4_find_relay_suboption_value((hio_dhcp4_pktinf_t*)&pkt, 0x101, &olen) == HIO_NULL, "and one that would truncate to a real code finds nothing"); OK (hio_dhcp4_delete_relay_suboption(&pkt, 256) <= -1, "nor can such a code delete"); OK (hio_dhcp4_delete_relay_suboption(&pkt, 0x101) <= -1, "including one that would truncate"); OK (pkt.len == before, "and the packet is untouched by any of it"); OK (hio_dhcp4_delete_relay_suboption(&pkt, 0) <= -1, "deleting suboption 0 is refused"); OK (pkt.len == before, "and the packet is untouched"); OK (hio_dhcp4_delete_relay_suboption(&pkt, -1) <= -1, "as is a negative code"); OK (pkt.len == before, "leaving it untouched too"); /* and the real one still deletes, so the guard did not break the path. * it was the option's only suboption, so what goes is the whole relay * option: its own two-octet header plus the six the suboption occupied. */ OK (hio_dhcp4_delete_relay_suboption(&pkt, HIO_DHCP4_OPT_RELAY_CIRCUIT_ID) == 0, "the suboption that is there still deletes"); OK (pkt.len == before - 8, "taking the now-empty relay option with it"); OK (hio_dhcp4_find_option((hio_dhcp4_pktinf_t*)&pkt, HIO_DHCP4_OPT_RELAY) == HIO_NULL, "so no relay option is left"); } /* an option area ending exactly with {OVERLOAD, len=1} and no value octet. * the overload branch reads that value to learn which fields are overloaded, * and it runs for an option the search did not match - so the read has to be * bounded by the option's length check, not by the match. * * the packet is heap-allocated at exactly its own size so that a read one * octet past it is a heap overflow rather than a quiet touch of the next * global, which is what let this sit unnoticed in three separate trees. */ static void test_overload_at_the_very_end (void) { hio_oow_t plen = HIO_SIZEOF(hio_dhcp4_pkt_hdr_t) + 4 + 2; hio_uint8_t* buf = (hio_uint8_t*)malloc(plen); hio_uint32_t cookie = HIO_CONST_HTON32(HIO_DHCP4_MAGIC_COOKIE); hio_dhcp4_pktinf_t pkt; OK (buf != HIO_NULL, "a packet sized to the octet"); memset (buf, 0, plen); HIO_MEMCPY (buf + HIO_SIZEOF(hio_dhcp4_pkt_hdr_t), &cookie, 4); buf[plen - 2] = HIO_DHCP4_OPT_OVERLOAD; buf[plen - 1] = 1; /* claims a value octet that is not there */ pkt.hdr = (hio_dhcp4_pkt_hdr_t*)buf; pkt.len = plen; /* searching for something else reaches the overload option through the * path that does not match it */ OK (hio_dhcp4_find_option(&pkt, HIO_DHCP4_OPT_SUBNET) == HIO_NULL, "an option area ending in a truncated overload yields nothing"); OK (hio_dhcp4_find_option(&pkt, HIO_DHCP4_OPT_OVERLOAD) == HIO_NULL, "and the truncated overload option is not returned either"); /* the walker reaches it too, and must refuse the packet rather than * reading the octet that is not there */ g_walked = 0; OK (hio_dhcp4_walk_options(&pkt, count_opt) <= -1, "the walker refuses it"); OK (g_walked == 0, "having visited nothing"); free (buf); } /* ------------------------------------------------------------------ */ int main (void) { no_plan (); test_cookie_and_layout (); test_byte_order (); test_unaligned_payload (); test_wrong_width_is_refused (); test_msg_type (); test_client_id (); test_check_pkt (); test_reply_echoes_the_request (); test_option_editing (); test_one_octet_options (); test_overlong_option_still_refused (); test_capacity (); test_relay_suboptions (); test_relay_suboption_limits (); test_relay_suboption_bounds (); test_option_value_accessor (); test_suboption_code_zero (); test_code_must_fit_an_octet (); test_overload_at_the_very_end (); return exit_status(); }