diff --git a/CMakeLists.txt b/CMakeLists.txt index be7ce84d..6ce19d67 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -271,6 +271,8 @@ if (UA2F_BUILD_TESTS) test/cli_test.cc test/http_session_test.cc test/http_parser_ua_test.cc + test/http_parser_ua_alloc_test.cc + test/alloc_tracker.c src/util.c src/cache.c src/http_session.c @@ -287,6 +289,9 @@ if (UA2F_BUILD_TESTS) pthread ) ua2f_link_atomic_if_needed(ua2f_test) + # Count and inject allocation failures only inside explicitly scoped tests. + target_link_options(ua2f_test PRIVATE + -Wl,--wrap=malloc -Wl,--wrap=calloc -Wl,--wrap=realloc -Wl,--wrap=free) target_include_directories(ua2f_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) target_include_directories(ua2f_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/third/llhttp) if (UA2F_ENABLE_UCI) diff --git a/src/handler.c b/src/handler.c index 17f58f48..071cc6a9 100644 --- a/src/handler.c +++ b/src/handler.c @@ -277,6 +277,8 @@ void handle_packet(const struct packet_io *io, void *io_ctx, const struct nf_pac assert(pkt->payload != NULL && "Packet payload cannot be NULL"); assert(pkt->payload_len > 0 && "Packet payload length must be positive"); struct pkt_buff *pkt_buff = NULL; + struct ua_mangle_entry ua_entries_inline[UA_INLINE_ENTRIES]; + struct ua_mangle_entry *ua_entries_copy = ua_entries_inline; bool ct_ok = use_conntrack && pkt->has_conntrack; bool verdict_sent = false; @@ -469,16 +471,34 @@ void handle_packet(const struct packet_io *io, void *io_ctx, const struct nf_pac // Level 3: feed to llhttp (session is valid, protected by its own state lock) session_state_lock(session); session_reset_per_packet(session, tcp_payload); - const int parse_ret = http_parser_feed(session, (const char *)tcp_payload, tcp_payload_len); + int parse_ret = http_parser_feed(session, (const char *)tcp_payload, tcp_payload_len); // Copy results out before releasing lock - const int ua_count = session->ua_entry_count; - struct ua_mangle_entry ua_entries_copy[UA_MAX_ENTRIES]; - if (ua_count > 0) { - memcpy(ua_entries_copy, session->ua_entries, ua_count * sizeof(struct ua_mangle_entry)); + const size_t ua_count = session->ua_entry_count; + if (parse_ret == 0 && ua_count > UA_INLINE_ENTRIES) { + ua_entries_copy = malloc(ua_count * sizeof(*ua_entries_copy)); + if (ua_entries_copy == NULL) { + session->ua_allocation_failed = true; + parse_ret = HTTP_PARSER_NO_MEMORY; + } + } + if (parse_ret == 0 && ua_count > 0) { + for (size_t i = 0; i < ua_count; i++) { + ua_entries_copy[i] = *session_ua_entry_const(session, i); + } } session_state_unlock(session); + if (parse_ret == HTTP_PARSER_NO_MEMORY) { + // Keep the failed session so a retransmitted continuation cannot be + // mistaken for a new, non-HTTP stream and bypass rewriting. + session_release(session); + session = NULL; + syslog(LOG_ERR, "Failed to allocate User-Agent entries, dropping packet"); + SEND_VERDICT(NF_DROP, MARK_NONE, NULL); + goto end; + } + if (parse_ret != 0) { session_wrlock(); session_delete(session); @@ -499,7 +519,7 @@ void handle_packet(const struct packet_io *io, void *io_ctx, const struct nf_pac session = NULL; // Mangle UA entries (using copied data, session lock released) - for (int i = 0; i < ua_count; i++) { + for (size_t i = 0; i < ua_count; i++) { const size_t ua_offset = ua_entries_copy[i].offset; const size_t ua_len = ua_entries_copy[i].len; const size_t replacement_offset = ua_entries_copy[i].replacement_offset; @@ -539,6 +559,9 @@ void handle_packet(const struct packet_io *io, void *io_ctx, const struct nf_pac SEND_VERDICT(NF_ACCEPT, (ct_ok && new_session) ? MARK_HTTP : MARK_NONE, pkt_buff); end: + if (ua_entries_copy != ua_entries_inline) { + free(ua_entries_copy); + } if (!verdict_sent) { SEND_VERDICT(NF_ACCEPT, MARK_NONE, NULL); } diff --git a/src/http_parser_ua.c b/src/http_parser_ua.c index 11190861..c916fb26 100644 --- a/src/http_parser_ua.c +++ b/src/http_parser_ua.c @@ -1,6 +1,8 @@ #include "http_parser_ua.h" +#include #include +#include #include #include #include @@ -9,6 +11,38 @@ #include "statistics.h" #include "third/llhttp/llhttp.h" +// Override only at this expansion site: the upstream header remains unchanged. +#undef utarray_oom +#define utarray_oom() goto allocation_failed + +static bool append_ua_entry(struct http_session *session, const struct ua_mangle_entry *entry) { + if (session->ua_entry_count < UA_INLINE_ENTRIES) { + session->ua_entries_inline[session->ua_entry_count++] = *entry; + return true; + } + + UT_array *overflow = &session->ua_entries_overflow; + const unsigned old_capacity = overflow->n; + // utarray uses unsigned counts and doubles capacity. Guard both its count + // arithmetic and byte-size multiplication, including the inline prefix. + const size_t max_capacity = SIZE_MAX / sizeof(*entry) - UA_INLINE_ENTRIES; + if (overflow->i == UINT_MAX || + (overflow->i == overflow->n && + (overflow->n > UINT_MAX / 2 || overflow->n > max_capacity / 2))) { + return false; + } + utarray_push_back(overflow, entry); + session->ua_entry_count++; + return true; + +allocation_failed: + // reserve changes n before realloc; i and d still describe the old buffer. + overflow->n = old_capacity; + return false; +} + +#undef utarray_oom + static int on_header_field(llhttp_t *parser, const char *data, size_t len) { struct http_session *session = (struct http_session *)parser->data; @@ -82,7 +116,7 @@ static int on_header_value(llhttp_t *parser, const char *data, size_t len) { if (session->in_ua_value && session->ua_entry_count > 0) { // Continuation of the same UA value — extend current entry - size_t *entry_len = &session->ua_entries[session->ua_entry_count - 1].len; + size_t *entry_len = &session_ua_entry(session, session->ua_entry_count - 1)->len; if (SIZE_MAX - *entry_len < len) { *entry_len = SIZE_MAX; } else { @@ -90,11 +124,11 @@ static int on_header_value(llhttp_t *parser, const char *data, size_t len) { } } else { // New UA entry - if (session->ua_entry_count < UA_MAX_ENTRIES) { - session->ua_entries[session->ua_entry_count].offset = offset; - session->ua_entries[session->ua_entry_count].len = len; - session->ua_entries[session->ua_entry_count].replacement_offset = session->ua_value_seen_len; - session->ua_entry_count++; + const struct ua_mangle_entry entry = {offset, len, session->ua_value_seen_len}; + if (!append_ua_entry(session, &entry)) { + session->ua_allocation_failed = true; + llhttp_set_error_reason(parser, "Failed to allocate User-Agent entries"); + return HPE_USER; } session->in_ua_value = true; } @@ -118,7 +152,6 @@ static int on_headers_complete(llhttp_t *parser) { static int on_message_complete(llhttp_t *parser) { struct http_session *session = (struct http_session *)parser->data; session_reset_per_message(session); - session->last_active = time(NULL); return 0; } @@ -138,16 +171,33 @@ void http_parser_init_session(struct http_session *session) { llhttp_init(&session->parser, HTTP_REQUEST, &shared_settings); session->parser.data = session; + session->ua_allocation_failed = false; + + if (session->ua_entries_overflow.icd.sz == 0) { + const UT_icd entry_icd = {sizeof(struct ua_mangle_entry), NULL, NULL, NULL}; + utarray_init(&session->ua_entries_overflow, &entry_icd); + } session_reset_per_message(session); } int http_parser_feed(struct http_session *session, const char *data, size_t len) { + // TTL measures idle time, including unfinished headers/bodies and retries + // on a session whose rewriting failed. Empty feeds are not activity. + if (len > 0) { + session->last_active = time(NULL); + } + // A failed allocation may leave llhttp partway through a payload. Keep + // rejecting this stream until it is idle or closed, rather than allowing + // later fragments to be reclassified as non-HTTP traffic. + if (session->ua_allocation_failed) { + return HTTP_PARSER_NO_MEMORY; + } llhttp_errno_t err = llhttp_execute(&session->parser, data, len); if (err != HPE_OK) { syslog(LOG_DEBUG, "llhttp parse error: %s (%s)", llhttp_errno_name(err), llhttp_get_error_reason(&session->parser)); - return -1; + return session->ua_allocation_failed ? HTTP_PARSER_NO_MEMORY : -1; } return 0; } diff --git a/src/http_parser_ua.h b/src/http_parser_ua.h index f1e06717..642218d9 100644 --- a/src/http_parser_ua.h +++ b/src/http_parser_ua.h @@ -6,8 +6,11 @@ // Initialize llhttp parser and callbacks on a session. void http_parser_init_session(struct http_session *session); -// Feed TCP payload to llhttp parser. Updates session->ua_entries. -// Returns: 0 on success, -1 on parse error. +// Allocation failures must not be treated as non-HTTP traffic and forwarded. +#define HTTP_PARSER_NO_MEMORY (-2) + +// Feed TCP payload to llhttp parser. Records entries accessible through session_ua_entry(). +// Returns: 0 on success, -1 on parse error, HTTP_PARSER_NO_MEMORY on allocation failure. int http_parser_feed(struct http_session *session, const char *data, size_t len); #endif /* UA2F_HTTP_PARSER_UA_H */ diff --git a/src/http_session.c b/src/http_session.c index 9f1ed7b8..1897894b 100644 --- a/src/http_session.c +++ b/src/http_session.c @@ -188,11 +188,16 @@ bool session_state_init(struct http_session *session) { } void session_state_destroy(struct http_session *session) { - if (session == NULL || !session->state_lock_initialized) { + if (session == NULL) { return; } - pthread_mutex_destroy(&session->state_lock); - session->state_lock_initialized = false; + utarray_done(&session->ua_entries_overflow); + memset(&session->ua_entries_overflow, 0, sizeof(session->ua_entries_overflow)); + session->ua_entry_count = 0; + if (session->state_lock_initialized) { + pthread_mutex_destroy(&session->state_lock); + session->state_lock_initialized = false; + } } void session_state_lock(struct http_session *session) { @@ -209,9 +214,8 @@ void session_state_unlock(struct http_session *session) { void session_reset_per_packet(struct http_session *session, const void *tcp_payload_base) { session->ua_entry_count = 0; + utarray_clear(&session->ua_entries_overflow); session->tcp_payload_base = tcp_payload_base; - // last_active is updated in session_create and by the cleaner's TTL check. - // Avoid time() syscall on every packet — the TTL is coarse (300s default). } void session_reset_per_message(struct http_session *session) { diff --git a/src/http_session.h b/src/http_session.h index 408d3723..3b06aece 100644 --- a/src/http_session.h +++ b/src/http_session.h @@ -10,8 +10,9 @@ #include "third/llhttp/llhttp.h" #include "third/nfqueue-mnl/nfqueue-mnl.h" #include "third/uthash/uthash.h" +#include "third/uthash/utarray.h" -#define UA_MAX_ENTRIES 8 +#define UA_INLINE_ENTRIES 8 #define FIELD_BUF_SIZE 32 struct ua_mangle_entry { @@ -50,8 +51,11 @@ struct http_session { bool in_ua_value; size_t ua_value_seen_len; - struct ua_mangle_entry ua_entries[UA_MAX_ENTRIES]; - int ua_entry_count; + // Keep the common case allocation-free; grow for large batches of headers. + struct ua_mangle_entry ua_entries_inline[UA_INLINE_ENTRIES]; + UT_array ua_entries_overflow; + size_t ua_entry_count; + bool ua_allocation_failed; // fail closed until this session is destroyed/reinitialized const void *tcp_payload_base; @@ -59,6 +63,21 @@ struct http_session { UT_hash_handle hh; }; +// Entry storage is split: the first eight slots are inline, the rest are utarray-owned. +static inline struct ua_mangle_entry *session_ua_entry(struct http_session *session, size_t index) { + if (index < UA_INLINE_ENTRIES) { + return &session->ua_entries_inline[index]; + } + return (struct ua_mangle_entry *)utarray_eltptr(&session->ua_entries_overflow, index - UA_INLINE_ENTRIES); +} + +static inline const struct ua_mangle_entry *session_ua_entry_const(const struct http_session *session, size_t index) { + if (index < UA_INLINE_ENTRIES) { + return &session->ua_entries_inline[index]; + } + return (const struct ua_mangle_entry *)utarray_eltptr(&session->ua_entries_overflow, index - UA_INLINE_ENTRIES); +} + void init_http_sessions(int max_sessions); struct session_key session_key_from_connid(uint32_t conn_id); struct session_key session_key_from_tuple(const struct ip_tuple *tuple); @@ -73,6 +92,7 @@ int session_cleanup_expired(int ttl_seconds); void session_wrlock(void); void session_wrunlock(void); bool session_state_init(struct http_session *session); +// Release parser entry storage and, if initialized, the state mutex. void session_state_destroy(struct http_session *session); void session_state_lock(struct http_session *session); void session_state_unlock(struct http_session *session); diff --git a/src/proxy.c b/src/proxy.c index 8c8818d4..83f9aea3 100644 --- a/src/proxy.c +++ b/src/proxy.c @@ -416,6 +416,7 @@ static void free_closed_connections(struct proxy_context *ctx) { struct proxy_connection *conn = ctx->closing; ctx->closing = conn->close_next; connection_unlink(ctx, conn); + session_state_destroy(&conn->session); free(conn); } } @@ -516,10 +517,11 @@ static void rewrite_user_agent_entries(uint8_t *buf, size_t len, const struct ht } const size_t replacement_len = UA2F_MAX_USER_AGENT_LENGTH; - for (int i = 0; i < session->ua_entry_count; i++) { - const size_t offset = session->ua_entries[i].offset; - const size_t ua_len = session->ua_entries[i].len; - const size_t replacement_offset = session->ua_entries[i].replacement_offset; + for (size_t i = 0; i < session->ua_entry_count; i++) { + const struct ua_mangle_entry *entry = session_ua_entry_const(session, i); + const size_t offset = entry->offset; + const size_t ua_len = entry->len; + const size_t replacement_offset = entry->replacement_offset; if (offset > len || ua_len > len - offset) { continue; } @@ -535,14 +537,18 @@ static void rewrite_user_agent_entries(uint8_t *buf, size_t len, const struct ht } } -static void process_client_payload(struct proxy_connection *conn, uint8_t *buf, size_t len) { +static int process_client_payload(struct proxy_connection *conn, uint8_t *buf, size_t len) { if (conn->rewrite_disabled) { - return; + return 0; } count_tcp_packet(); session_reset_per_packet(&conn->session, buf); const int parse_ret = http_parser_feed(&conn->session, (const char *)buf, len); + if (parse_ret == HTTP_PARSER_NO_MEMORY) { + syslog(LOG_ERR, "Failed to allocate User-Agent entries, closing connection"); + return -1; + } if (conn->session.ua_entry_count > 0) { rewrite_user_agent_entries(buf, len, &conn->session); count_user_agent_packet(); @@ -553,6 +559,7 @@ static void process_client_payload(struct proxy_connection *conn, uint8_t *buf, if (parse_ret != 0) { conn->rewrite_disabled = true; } + return 0; } static int flush_buffer(int fd, struct proxy_buffer *buf) { @@ -607,8 +614,9 @@ static int read_into_buffer(struct proxy_connection *conn, enum proxy_side side) return -1; } - if (side == PROXY_SIDE_CLIENT) { - process_client_payload(conn, out->data + out->len, (size_t)n); + if (side == PROXY_SIDE_CLIENT && + process_client_payload(conn, out->data + out->len, (size_t)n) != 0) { + return -1; } out->len += (size_t)n; } diff --git a/src/third/uthash/README.md b/src/third/uthash/README.md new file mode 100644 index 00000000..7a7e3382 --- /dev/null +++ b/src/third/uthash/README.md @@ -0,0 +1,22 @@ +# uthash + +The existing `uthash.h` declares version 2.3.0. The added `utarray.h` is +vendored from the upstream v2.3.0 release: + +- Upstream: https://github.com/troydhanson/uthash +- Pinned commit: `e493aa90a2833b4655927598f169c31cfcdf7861` +- `utarray.h` is copied unchanged from `src/utarray.h` at that commit +- The upstream BSD license notice is retained in each header + +UA2F embeds a `UT_array` for User-Agent entries beyond the eight inline slots. +`utarray_init` allocates nothing, `utarray_clear` retains overflow capacity, and +`utarray_done` frees only the overflow buffer. The inline storage is never given +to utarray. The parser's append helper overrides utarray's default fatal OOM +handling locally and restores capacity when a reserve fails. + +The zero-allocation guarantee is limited to UA entry storage for at most eight +entries per parser feed. Session objects and network buffers are separate. +Overflow parser capacity is reused across feeds; NFQUEUE still allocates a +separate copy-out buffer for each payload with more than eight entries. +Allocation-count and failure-injection tests use GNU-compatible linker `--wrap` +options; production builds do not replace the allocator. diff --git a/src/third/uthash/utarray.h b/src/third/uthash/utarray.h new file mode 100644 index 00000000..992fe7cc --- /dev/null +++ b/src/third/uthash/utarray.h @@ -0,0 +1,248 @@ +/* +Copyright (c) 2008-2021, Troy D. Hanson http://troydhanson.github.com/uthash/ +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/* a dynamic array implementation using macros + */ +#ifndef UTARRAY_H +#define UTARRAY_H + +#define UTARRAY_VERSION 2.3.0 + +#include /* size_t */ +#include /* memset, etc */ +#include /* exit */ + +#ifdef __GNUC__ +#define UTARRAY_UNUSED __attribute__((__unused__)) +#else +#define UTARRAY_UNUSED +#endif + +#ifdef oom +#error "The name of macro 'oom' has been changed to 'utarray_oom'. Please update your code." +#define utarray_oom() oom() +#endif + +#ifndef utarray_oom +#define utarray_oom() exit(-1) +#endif + +typedef void (ctor_f)(void *dst, const void *src); +typedef void (dtor_f)(void *elt); +typedef void (init_f)(void *elt); +typedef struct { + size_t sz; + init_f *init; + ctor_f *copy; + dtor_f *dtor; +} UT_icd; + +typedef struct { + unsigned i,n;/* i: index of next available slot, n: num slots */ + UT_icd icd; /* initializer, copy and destructor functions */ + char *d; /* n slots of size icd->sz*/ +} UT_array; + +#define utarray_init(a,_icd) do { \ + memset(a,0,sizeof(UT_array)); \ + (a)->icd = *(_icd); \ +} while(0) + +#define utarray_done(a) do { \ + if ((a)->n) { \ + if ((a)->icd.dtor) { \ + unsigned _ut_i; \ + for(_ut_i=0; _ut_i < (a)->i; _ut_i++) { \ + (a)->icd.dtor(utarray_eltptr(a,_ut_i)); \ + } \ + } \ + free((a)->d); \ + } \ + (a)->n=0; \ +} while(0) + +#define utarray_new(a,_icd) do { \ + (a) = (UT_array*)malloc(sizeof(UT_array)); \ + if ((a) == NULL) { \ + utarray_oom(); \ + } \ + utarray_init(a,_icd); \ +} while(0) + +#define utarray_free(a) do { \ + utarray_done(a); \ + free(a); \ +} while(0) + +#define utarray_reserve(a,by) do { \ + if (((a)->i+(by)) > (a)->n) { \ + char *utarray_tmp; \ + while (((a)->i+(by)) > (a)->n) { (a)->n = ((a)->n ? (2*(a)->n) : 8); } \ + utarray_tmp=(char*)realloc((a)->d, (a)->n*(a)->icd.sz); \ + if (utarray_tmp == NULL) { \ + utarray_oom(); \ + } \ + (a)->d=utarray_tmp; \ + } \ +} while(0) + +#define utarray_push_back(a,p) do { \ + utarray_reserve(a,1); \ + if ((a)->icd.copy) { (a)->icd.copy( _utarray_eltptr(a,(a)->i++), p); } \ + else { memcpy(_utarray_eltptr(a,(a)->i++), p, (a)->icd.sz); }; \ +} while(0) + +#define utarray_pop_back(a) do { \ + if ((a)->icd.dtor) { (a)->icd.dtor( _utarray_eltptr(a,--((a)->i))); } \ + else { (a)->i--; } \ +} while(0) + +#define utarray_extend_back(a) do { \ + utarray_reserve(a,1); \ + if ((a)->icd.init) { (a)->icd.init(_utarray_eltptr(a,(a)->i)); } \ + else { memset(_utarray_eltptr(a,(a)->i),0,(a)->icd.sz); } \ + (a)->i++; \ +} while(0) + +#define utarray_len(a) ((a)->i) + +#define utarray_eltptr(a,j) (((j) < (a)->i) ? _utarray_eltptr(a,j) : NULL) +#define _utarray_eltptr(a,j) ((void*)((a)->d + ((a)->icd.sz * (j)))) + +#define utarray_insert(a,p,j) do { \ + if ((j) > (a)->i) utarray_resize(a,j); \ + utarray_reserve(a,1); \ + if ((j) < (a)->i) { \ + memmove( _utarray_eltptr(a,(j)+1), _utarray_eltptr(a,j), \ + ((a)->i - (j))*((a)->icd.sz)); \ + } \ + if ((a)->icd.copy) { (a)->icd.copy( _utarray_eltptr(a,j), p); } \ + else { memcpy(_utarray_eltptr(a,j), p, (a)->icd.sz); }; \ + (a)->i++; \ +} while(0) + +#define utarray_inserta(a,w,j) do { \ + if (utarray_len(w) == 0) break; \ + if ((j) > (a)->i) utarray_resize(a,j); \ + utarray_reserve(a,utarray_len(w)); \ + if ((j) < (a)->i) { \ + memmove(_utarray_eltptr(a,(j)+utarray_len(w)), \ + _utarray_eltptr(a,j), \ + ((a)->i - (j))*((a)->icd.sz)); \ + } \ + if ((a)->icd.copy) { \ + unsigned _ut_i; \ + for(_ut_i=0;_ut_i<(w)->i;_ut_i++) { \ + (a)->icd.copy(_utarray_eltptr(a, (j) + _ut_i), _utarray_eltptr(w, _ut_i)); \ + } \ + } else { \ + memcpy(_utarray_eltptr(a,j), _utarray_eltptr(w,0), \ + utarray_len(w)*((a)->icd.sz)); \ + } \ + (a)->i += utarray_len(w); \ +} while(0) + +#define utarray_resize(dst,num) do { \ + unsigned _ut_i; \ + if ((dst)->i > (unsigned)(num)) { \ + if ((dst)->icd.dtor) { \ + for (_ut_i = (num); _ut_i < (dst)->i; ++_ut_i) { \ + (dst)->icd.dtor(_utarray_eltptr(dst, _ut_i)); \ + } \ + } \ + } else if ((dst)->i < (unsigned)(num)) { \ + utarray_reserve(dst, (num) - (dst)->i); \ + if ((dst)->icd.init) { \ + for (_ut_i = (dst)->i; _ut_i < (unsigned)(num); ++_ut_i) { \ + (dst)->icd.init(_utarray_eltptr(dst, _ut_i)); \ + } \ + } else { \ + memset(_utarray_eltptr(dst, (dst)->i), 0, (dst)->icd.sz*((num) - (dst)->i)); \ + } \ + } \ + (dst)->i = (num); \ +} while(0) + +#define utarray_concat(dst,src) do { \ + utarray_inserta(dst, src, utarray_len(dst)); \ +} while(0) + +#define utarray_erase(a,pos,len) do { \ + if ((a)->icd.dtor) { \ + unsigned _ut_i; \ + for (_ut_i = 0; _ut_i < (len); _ut_i++) { \ + (a)->icd.dtor(utarray_eltptr(a, (pos) + _ut_i)); \ + } \ + } \ + if ((a)->i > ((pos) + (len))) { \ + memmove(_utarray_eltptr(a, pos), _utarray_eltptr(a, (pos) + (len)), \ + ((a)->i - ((pos) + (len))) * (a)->icd.sz); \ + } \ + (a)->i -= (len); \ +} while(0) + +#define utarray_renew(a,u) do { \ + if (a) utarray_clear(a); \ + else utarray_new(a, u); \ +} while(0) + +#define utarray_clear(a) do { \ + if ((a)->i > 0) { \ + if ((a)->icd.dtor) { \ + unsigned _ut_i; \ + for(_ut_i=0; _ut_i < (a)->i; _ut_i++) { \ + (a)->icd.dtor(_utarray_eltptr(a, _ut_i)); \ + } \ + } \ + (a)->i = 0; \ + } \ +} while(0) + +#define utarray_sort(a,cmp) do { \ + qsort((a)->d, (a)->i, (a)->icd.sz, cmp); \ +} while(0) + +#define utarray_find(a,v,cmp) bsearch((v),(a)->d,(a)->i,(a)->icd.sz,cmp) + +#define utarray_front(a) (((a)->i) ? (_utarray_eltptr(a,0)) : NULL) +#define utarray_next(a,e) (((e)==NULL) ? utarray_front(a) : (((a)->i != utarray_eltidx(a,e)+1) ? _utarray_eltptr(a,utarray_eltidx(a,e)+1) : NULL)) +#define utarray_prev(a,e) (((e)==NULL) ? utarray_back(a) : ((utarray_eltidx(a,e) != 0) ? _utarray_eltptr(a,utarray_eltidx(a,e)-1) : NULL)) +#define utarray_back(a) (((a)->i) ? (_utarray_eltptr(a,(a)->i-1)) : NULL) +#define utarray_eltidx(a,e) (((char*)(e) - (a)->d) / (a)->icd.sz) + +/* last we pre-define a few icd for common utarrays of ints and strings */ +static void utarray_str_cpy(void *dst, const void *src) { + char *const *srcc = (char *const *)src; + char **dstc = (char**)dst; + *dstc = (*srcc == NULL) ? NULL : strdup(*srcc); +} +static void utarray_str_dtor(void *elt) { + char **eltc = (char**)elt; + if (*eltc != NULL) free(*eltc); +} +static const UT_icd ut_str_icd UTARRAY_UNUSED = {sizeof(char*),NULL,utarray_str_cpy,utarray_str_dtor}; +static const UT_icd ut_int_icd UTARRAY_UNUSED = {sizeof(int),NULL,NULL,NULL}; +static const UT_icd ut_ptr_icd UTARRAY_UNUSED = {sizeof(void*),NULL,NULL,NULL}; + + +#endif /* UTARRAY_H */ diff --git a/test/alloc_tracker.c b/test/alloc_tracker.c new file mode 100644 index 00000000..7eb8709e --- /dev/null +++ b/test/alloc_tracker.c @@ -0,0 +1,68 @@ +#include "alloc_tracker.h" + +#include +#include + +static _Thread_local bool tracking; +static _Thread_local size_t fail_on_call; +static _Thread_local size_t allocation_calls; +static _Thread_local struct allocation_counts counts; + +void *__real_malloc(size_t size); +void *__real_calloc(size_t count, size_t size); +void *__real_realloc(void *ptr, size_t size); +void __real_free(void *ptr); + +void ua2f_test_alloc_begin(size_t fail_call) { + counts = (struct allocation_counts){0}; + allocation_calls = 0; + fail_on_call = fail_call; + tracking = true; +} + +struct allocation_counts ua2f_test_alloc_end(void) { + tracking = false; + return counts; +} + +static bool fail_allocation(void) { + allocation_calls++; + return fail_on_call != 0 && allocation_calls == fail_on_call; +} + +void *__wrap_malloc(size_t size) { + if (tracking) { + counts.malloc_calls++; + if (fail_allocation()) { + return NULL; + } + } + return __real_malloc(size); +} + +void *__wrap_calloc(size_t count, size_t size) { + if (tracking) { + counts.calloc_calls++; + if (fail_allocation()) { + return NULL; + } + } + return __real_calloc(count, size); +} + +void *__wrap_realloc(void *ptr, size_t size) { + if (tracking) { + counts.realloc_calls++; + if (fail_allocation()) { + return NULL; + } + } + return __real_realloc(ptr, size); +} + +void __wrap_free(void *ptr) { + if (tracking && ptr != NULL) { + counts.free_calls++; + } + __real_free(ptr); +} diff --git a/test/alloc_tracker.h b/test/alloc_tracker.h new file mode 100644 index 00000000..07a33fc8 --- /dev/null +++ b/test/alloc_tracker.h @@ -0,0 +1,25 @@ +#ifndef UA2F_TEST_ALLOC_TRACKER_H +#define UA2F_TEST_ALLOC_TRACKER_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct allocation_counts { + size_t malloc_calls; + size_t calloc_calls; + size_t realloc_calls; + size_t free_calls; +}; + +// Track only the synchronous region between begin/end; fail_call=0 never fails. +void ua2f_test_alloc_begin(size_t fail_call); +struct allocation_counts ua2f_test_alloc_end(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/test/handler_test.cc b/test/handler_test.cc index b9acf976..0ea626ec 100644 --- a/test/handler_test.cc +++ b/test/handler_test.cc @@ -450,3 +450,73 @@ TEST_F(HandlerTest, MalformedIpv4StillSendsVerdict) { EXPECT_EQ(mock_ctx.verdicts[0].verdict, NF_ACCEPT); EXPECT_TRUE(mock_ctx.verdicts[0].mangled_data.empty()); } + +TEST_F(HandlerTest, RewritesAllPipelinedRequestsBeyondInlineCapacity) { + std::string request; + std::string expected; + for (size_t i = 0; i < 33; ++i) { + request += "GET / HTTP/1.1\r\nUser-Agent: Original\r\n\r\n"; + expected += "GET / HTTP/1.1\r\nUser-Agent: FFFFFFFF\r\n\r\n"; + } + auto pkt = make_http_packet(request.c_str()); + handle_packet(&mock_packet_io, &mock_ctx, &pkt); + ASSERT_EQ(mock_ctx.verdicts.size(), 1u); + EXPECT_EQ(mock_ctx.verdicts[0].verdict, NF_ACCEPT); + const auto payload = extract_tcp_payload(mock_ctx.verdicts[0].mangled_data, IPV4); + EXPECT_EQ(std::string(payload.begin(), payload.end()), expected); +} + +TEST_F(HandlerTest, RewritesAllDuplicateUaHeadersBeyondInlineCapacityIpv6) { + std::string request = "GET / HTTP/1.1\r\n"; + std::string expected = request; + for (size_t i = 0; i < 33; ++i) { + request += "User-Agent: Original\r\n"; + expected += "User-Agent: FFFFFFFF\r\n"; + } + request += "\r\n"; + expected += "\r\n"; + struct in6_addr src = IN6ADDR_LOOPBACK_INIT; + struct in6_addr dst = IN6ADDR_LOOPBACK_INIT; + dst.s6_addr[15] = 2; + const auto raw = build_ipv6_tcp_packet(src, dst, 12345, 80, request.data(), request.size()); + auto pkt = make_nf_packet(raw, 1, IPV6); + handle_packet(&mock_packet_io, &mock_ctx, &pkt); + ASSERT_EQ(mock_ctx.verdicts.size(), 1u); + EXPECT_EQ(mock_ctx.verdicts[0].verdict, NF_ACCEPT); + const auto payload = extract_tcp_payload(mock_ctx.verdicts[0].mangled_data, IPV6); + EXPECT_EQ(std::string(payload.begin(), payload.end()), expected); +} + +TEST_F(HandlerTest, AllocationFailureKeepsSessionClosedForRetransmittedFragments) { + use_conntrack = true; + auto first = make_http_packet_ct("GET / HTTP/1.1\r\nUser-Agent: First"); + handle_packet(&mock_packet_io, &mock_ctx, &first); + ASSERT_EQ(mock_ctx.verdicts.size(), 1u); + ASSERT_EQ(mock_ctx.verdicts[0].verdict, NF_ACCEPT); + + const auto key = session_key_from_connid(100); + session_wrlock(); + auto *session = session_find(&key); + session_wrunlock(); + ASSERT_NE(session, nullptr); + session_state_lock(session); + // Model the persistent state left by an entry/copy allocation failure. + session->ua_allocation_failed = true; + const auto stale_time = time(nullptr) - 301; + session->last_active = stale_time; + session_state_unlock(session); + + for (uint32_t packet_id = 2; packet_id <= 3; ++packet_id) { + mock_ctx.verdicts.clear(); + auto continuation = make_http_packet_ct("Original\r\n\r\n", packet_id); + handle_packet(&mock_packet_io, &mock_ctx, &continuation); + ASSERT_EQ(mock_ctx.verdicts.size(), 1u); + EXPECT_EQ(mock_ctx.verdicts[0].verdict, NF_DROP); + EXPECT_FALSE(mock_ctx.verdicts[0].mark.should_set); + EXPECT_TRUE(mock_ctx.verdicts[0].mangled_data.empty()); + session_wrlock(); + EXPECT_EQ(session_find(&key), session); + EXPECT_EQ(session_cleanup_expired(300), 0); + session_wrunlock(); + } +} diff --git a/test/http_parser_ua_alloc_test.cc b/test/http_parser_ua_alloc_test.cc new file mode 100644 index 00000000..8c6894a2 --- /dev/null +++ b/test/http_parser_ua_alloc_test.cc @@ -0,0 +1,174 @@ +#include +#include +#include +#include + +#include "alloc_tracker.h" + +extern "C" { +#include +#include +} + +namespace { + +std::string request_with_uas(size_t count) { + std::string request = "GET / HTTP/1.1\r\n"; + for (size_t i = 0; i < count; ++i) { + request += "User-Agent: Original\r\n"; + } + return request + "\r\n"; +} + +size_t allocation_count(const allocation_counts &counts) { + return counts.malloc_calls + counts.calloc_calls + counts.realloc_calls; +} + +class HttpParserUAAllocationTest : public ::testing::Test { +protected: + struct http_session session{}; + + void SetUp() override { http_parser_init_session(&session); } + void TearDown() override { session_state_destroy(&session); } + + int feed(const std::string &request) { + session_reset_per_packet(&session, request.data()); + return http_parser_feed(&session, request.data(), request.size()); + } +}; + +TEST_F(HttpParserUAAllocationTest, InitializeAndDestroyWithoutHeapAllocation) { + struct http_session empty{}; + ua2f_test_alloc_begin(0); + http_parser_init_session(&empty); + session_state_destroy(&empty); + const auto counts = ua2f_test_alloc_end(); + EXPECT_EQ(allocation_count(counts), 0u); + EXPECT_EQ(counts.free_calls, 0u); +} + +TEST_F(HttpParserUAAllocationTest, UpToEightEntriesNeedNoHeapAllocation) { + for (size_t entries = 0; entries <= 8; ++entries) { + const auto request = request_with_uas(entries); + ua2f_test_alloc_begin(0); + const int result = feed(request); + const auto counts = ua2f_test_alloc_end(); + ASSERT_EQ(result, 0); + EXPECT_EQ(allocation_count(counts), 0u); + EXPECT_EQ(counts.free_calls, 0u); + EXPECT_EQ(session.ua_entry_count, entries); + EXPECT_EQ(session.ua_entries_overflow.d, nullptr); + } + ua2f_test_alloc_begin(0); + session_state_destroy(&session); + const auto counts = ua2f_test_alloc_end(); + EXPECT_EQ(allocation_count(counts), 0u); + EXPECT_EQ(counts.free_calls, 0u); +} + +TEST_F(HttpParserUAAllocationTest, NinthEntryAllocatesOnlyOverflowStorage) { + const auto request = request_with_uas(9); + ua2f_test_alloc_begin(0); + const int result = feed(request); + const auto counts = ua2f_test_alloc_end(); + ASSERT_EQ(result, 0); + EXPECT_EQ(counts.realloc_calls, 1u); + EXPECT_EQ(allocation_count(counts), 1u); + EXPECT_EQ(utarray_len(&session.ua_entries_overflow), 1u); + EXPECT_EQ(session_ua_entry(&session, 0), &session.ua_entries_inline[0]); + EXPECT_EQ(session_ua_entry(&session, 7), &session.ua_entries_inline[7]); + EXPECT_EQ(session_ua_entry(&session, 8), utarray_front(&session.ua_entries_overflow)); + + ua2f_test_alloc_begin(0); + session_state_destroy(&session); + const auto destroy_counts = ua2f_test_alloc_end(); + EXPECT_EQ(allocation_count(destroy_counts), 0u); + EXPECT_EQ(destroy_counts.free_calls, 1u); +} + +TEST_F(HttpParserUAAllocationTest, OverflowCapacityIsReusedAcrossPayloads) { + const auto large = request_with_uas(33); + const auto small = request_with_uas(1); + ASSERT_EQ(feed(large), 0); + const auto *buffer = session.ua_entries_overflow.d; + for (const auto *request : {&large, &small, &large}) { + ua2f_test_alloc_begin(0); + const int result = feed(*request); + const auto counts = ua2f_test_alloc_end(); + ASSERT_EQ(result, 0); + EXPECT_EQ(allocation_count(counts), 0u); + EXPECT_EQ(counts.free_calls, 0u); + EXPECT_EQ(session.ua_entries_overflow.d, buffer); + } +} + +TEST_F(HttpParserUAAllocationTest, FirstOverflowAllocationFailureIsRecoverableAndSticky) { + const auto request = request_with_uas(9); + ua2f_test_alloc_begin(1); + const int result = feed(request); + const auto counts = ua2f_test_alloc_end(); + EXPECT_EQ(result, HTTP_PARSER_NO_MEMORY); + EXPECT_EQ(counts.realloc_calls, 1u); + EXPECT_EQ(session.ua_entry_count, 8u); + EXPECT_EQ(session.ua_entries_overflow.d, nullptr); + EXPECT_EQ(session.ua_entries_overflow.n, 0u); + EXPECT_EQ(utarray_len(&session.ua_entries_overflow), 0u); + EXPECT_TRUE(session.ua_allocation_failed); + + ua2f_test_alloc_begin(0); + const int retry_result = feed(request); + session_state_destroy(&session); + const auto retry_counts = ua2f_test_alloc_end(); + EXPECT_EQ(retry_result, HTTP_PARSER_NO_MEMORY); + EXPECT_EQ(allocation_count(retry_counts), 0u); + EXPECT_EQ(retry_counts.free_calls, 0u); +} + +TEST_F(HttpParserUAAllocationTest, GrowthFailurePreservesExistingBufferAndCapacity) { + const auto first = request_with_uas(16); + const auto larger = request_with_uas(17); + ASSERT_EQ(feed(first), 0); + const auto *buffer = session.ua_entries_overflow.d; + const auto capacity = session.ua_entries_overflow.n; + + ua2f_test_alloc_begin(1); + const int result = feed(larger); + const auto counts = ua2f_test_alloc_end(); + EXPECT_EQ(result, HTTP_PARSER_NO_MEMORY); + EXPECT_EQ(counts.realloc_calls, 1u); + EXPECT_EQ(session.ua_entries_overflow.d, buffer); + EXPECT_EQ(session.ua_entries_overflow.n, capacity); + EXPECT_EQ(utarray_len(&session.ua_entries_overflow), 8u); + EXPECT_EQ(session.ua_entry_count, 16u); + EXPECT_TRUE(session.ua_allocation_failed); + + ua2f_test_alloc_begin(0); + session_state_destroy(&session); + const auto destroy_counts = ua2f_test_alloc_end(); + EXPECT_EQ(allocation_count(destroy_counts), 0u); + EXPECT_EQ(destroy_counts.free_calls, 1u); +} + +TEST_F(HttpParserUAAllocationTest, RejectsCountAndGrowthOverflowBeforeAllocating) { + const auto request = request_with_uas(1); + const size_t byte_limit = SIZE_MAX / sizeof(struct ua_mangle_entry) - UA_INLINE_ENTRIES; + const unsigned growth_limit = static_cast( + std::min(static_cast(std::numeric_limits::max() / 2), byte_limit / 2)); + for (const unsigned capacity : {std::numeric_limits::max(), growth_limit + 1}) { + http_parser_init_session(&session); + session_reset_per_packet(&session, request.data()); + session.ua_entry_count = UA_INLINE_ENTRIES; + session.ua_entries_overflow.i = capacity; + session.ua_entries_overflow.n = capacity; + ua2f_test_alloc_begin(0); + const int result = http_parser_feed(&session, request.data(), request.size()); + const auto counts = ua2f_test_alloc_end(); + EXPECT_EQ(result, HTTP_PARSER_NO_MEMORY); + EXPECT_EQ(allocation_count(counts), 0u); + EXPECT_TRUE(session.ua_allocation_failed); + session.ua_entries_overflow.i = 0; + session.ua_entries_overflow.n = 0; + } +} + +} // namespace diff --git a/test/http_parser_ua_test.cc b/test/http_parser_ua_test.cc index 84701bb4..384c51d0 100644 --- a/test/http_parser_ua_test.cc +++ b/test/http_parser_ua_test.cc @@ -1,5 +1,6 @@ #include #include +#include #include extern "C" { @@ -43,9 +44,9 @@ TEST_F(HttpParserUATest, SinglePacketWithUA) { ASSERT_EQ(session->ua_entry_count, 1); // Verify offset points to "Mozilla/5.0" - const char *ua_start = req + session->ua_entries[0].offset; + const char *ua_start = req + session_ua_entry(session, 0)->offset; EXPECT_EQ(strncmp(ua_start, "Mozilla/5.0", 11), 0); - EXPECT_EQ(session->ua_entries[0].len, 11u); + EXPECT_EQ(session_ua_entry(session, 0)->len, 11u); } // 2. Single packet with no User-Agent @@ -63,9 +64,9 @@ TEST_F(HttpParserUATest, CaseInsensitiveUA) { EXPECT_EQ(ret, 0); ASSERT_EQ(session->ua_entry_count, 1); - const char *ua_start = req + session->ua_entries[0].offset; + const char *ua_start = req + session_ua_entry(session, 0)->offset; EXPECT_EQ(strncmp(ua_start, "TestAgent", 9), 0); - EXPECT_EQ(session->ua_entries[0].len, 9u); + EXPECT_EQ(session_ua_entry(session, 0)->len, 9u); } // 4. Non-HTTP data should cause a parse error @@ -92,9 +93,9 @@ TEST_F(HttpParserUATest, CrossPacketUAFieldName) { // The UA value appears in the second packet ASSERT_EQ(session->ua_entry_count, 1); - const char *ua_start = pkt2 + session->ua_entries[0].offset; + const char *ua_start = pkt2 + session_ua_entry(session, 0)->offset; EXPECT_EQ(strncmp(ua_start, "Mozilla/5.0", 11), 0); - EXPECT_EQ(session->ua_entries[0].len, 11u); + EXPECT_EQ(session_ua_entry(session, 0)->len, 11u); } // 6. Cross-packet UA value split @@ -106,7 +107,7 @@ TEST_F(HttpParserUATest, CrossPacketUAValue) { EXPECT_EQ(ret1, 0); // First packet should have found the partial UA EXPECT_EQ(session->ua_entry_count, 1); - EXPECT_EQ(session->ua_entries[0].replacement_offset, 0u); + EXPECT_EQ(session_ua_entry(session, 0)->replacement_offset, 0u); // Second packet has the rest of the UA value const char *pkt2 = "0 (Windows)\r\n\r\n"; @@ -117,9 +118,9 @@ TEST_F(HttpParserUATest, CrossPacketUAValue) { // Second packet should also have recorded its portion EXPECT_EQ(session->ua_entry_count, 1); // The second packet's entry offset should point to "0 (Windows)" - const char *ua_start2 = pkt2 + session->ua_entries[0].offset; + const char *ua_start2 = pkt2 + session_ua_entry(session, 0)->offset; EXPECT_EQ(strncmp(ua_start2, "0 (Windows)", 11), 0); - EXPECT_EQ(session->ua_entries[0].replacement_offset, 10u); + EXPECT_EQ(session_ua_entry(session, 0)->replacement_offset, 10u); } TEST_F(HttpParserUATest, UaValueSeenLengthSaturatesOnOverflow) { @@ -136,7 +137,7 @@ TEST_F(HttpParserUATest, UaValueSeenLengthSaturatesOnOverflow) { int ret2 = http_parser_feed(session, pkt2, strlen(pkt2)); EXPECT_EQ(ret2, 0); ASSERT_EQ(session->ua_entry_count, 1); - EXPECT_EQ(session->ua_entries[0].replacement_offset, std::numeric_limits::max() - 1); + EXPECT_EQ(session_ua_entry(session, 0)->replacement_offset, std::numeric_limits::max() - 1); EXPECT_EQ(session->ua_value_seen_len, std::numeric_limits::max()); } @@ -146,14 +147,14 @@ TEST_F(HttpParserUATest, KeepAliveMultipleRequests) { int ret1 = feed(req1); EXPECT_EQ(ret1, 0); EXPECT_EQ(session->ua_entry_count, 1); - const char *ua1 = req1 + session->ua_entries[0].offset; + const char *ua1 = req1 + session_ua_entry(session, 0)->offset; EXPECT_EQ(strncmp(ua1, "AgentOne", 8), 0); const char *req2 = "GET /second HTTP/1.1\r\nHost: example.com\r\nUser-Agent: AgentTwo\r\n\r\n"; int ret2 = feed(req2); EXPECT_EQ(ret2, 0); EXPECT_EQ(session->ua_entry_count, 1); - const char *ua2 = req2 + session->ua_entries[0].offset; + const char *ua2 = req2 + session_ua_entry(session, 0)->offset; EXPECT_EQ(strncmp(ua2, "AgentTwo", 8), 0); } @@ -166,11 +167,11 @@ TEST_F(HttpParserUATest, PipelinedRequestsSinglePacket) { EXPECT_EQ(session->ua_entry_count, 2); // First entry points into req - const char *ua1 = req + session->ua_entries[0].offset; + const char *ua1 = req + session_ua_entry(session, 0)->offset; EXPECT_EQ(strncmp(ua1, "AgentOne", 8), 0); // Second entry points into req - const char *ua2 = req + session->ua_entries[1].offset; + const char *ua2 = req + session_ua_entry(session, 1)->offset; EXPECT_EQ(strncmp(ua2, "AgentTwo", 8), 0); } @@ -184,9 +185,9 @@ TEST_F(HttpParserUATest, LongFieldNameIgnored) { int ret = feed(req); EXPECT_EQ(ret, 0); ASSERT_EQ(session->ua_entry_count, 1); - const char *ua_start = req + session->ua_entries[0].offset; + const char *ua_start = req + session_ua_entry(session, 0)->offset; EXPECT_EQ(strncmp(ua_start, "BrowserAgent", 12), 0); - EXPECT_EQ(session->ua_entries[0].len, 12u); + EXPECT_EQ(session_ua_entry(session, 0)->len, 12u); } // 10. Multiple non-UA headers before User-Agent — verify field_buf resets correctly @@ -200,7 +201,116 @@ TEST_F(HttpParserUATest, MultipleNonUAHeadersThenUA) { int ret = feed(req); EXPECT_EQ(ret, 0); ASSERT_EQ(session->ua_entry_count, 1); - const char *ua_start = req + session->ua_entries[0].offset; + const char *ua_start = req + session_ua_entry(session, 0)->offset; EXPECT_EQ(strncmp(ua_start, "TargetAgent", 11), 0); - EXPECT_EQ(session->ua_entries[0].len, 11u); + EXPECT_EQ(session_ua_entry(session, 0)->len, 11u); +} + +TEST_F(HttpParserUATest, RecordsAllPipelinedRequestsBeyondInlineCapacity) { + for (const size_t count : {9u, 32u, 257u}) { + std::string requests; + for (size_t i = 0; i < count; ++i) { + requests += "GET / HTTP/1.1\r\nUser-Agent: Original" + std::to_string(i) + "\r\n\r\n"; + } + ASSERT_EQ(feed(requests.c_str()), 0); + ASSERT_EQ(session->ua_entry_count, count); + for (size_t i = 0; i < count; ++i) { + const auto &entry = *session_ua_entry(session, i); + EXPECT_EQ(requests.substr(entry.offset, entry.len), "Original" + std::to_string(i)); + EXPECT_EQ(entry.replacement_offset, 0u); + } + } +} + +TEST_F(HttpParserUATest, RecordsAllDuplicateUaHeadersBeyondInlineCapacity) { + std::string request = "GET / HTTP/1.1\r\n"; + for (size_t i = 0; i < 1000; ++i) { + request += "User-Agent: X\r\n"; + } + request += "\r\n"; + ASSERT_EQ(feed(request.c_str()), 0); + ASSERT_EQ(session->ua_entry_count, 1000u); + for (size_t i = 0; i < session->ua_entry_count; ++i) { + const auto &entry = *session_ua_entry(session, i); + EXPECT_EQ(request.substr(entry.offset, entry.len), "X"); + EXPECT_EQ(entry.replacement_offset, 0u); + } + + // Storage can be reused without carrying entries into the next payload. + const auto *entries = session->ua_entries_overflow.d; + ASSERT_EQ(feed("GET / HTTP/1.1\r\nUser-Agent: Next\r\n\r\n"), 0); + EXPECT_EQ(session->ua_entries_overflow.d, entries); + EXPECT_EQ(session->ua_entry_count, 1u); +} + +TEST_F(HttpParserUATest, ContinuesSplitUaAfterGrowingEntries) { + std::string request = "GET / HTTP/1.1\r\n"; + for (size_t i = 0; i < 8; ++i) { + request += "User-Agent: First\r\n"; + } + request += "User-Agent: Ninth"; + ASSERT_EQ(feed(request.c_str()), 0); + ASSERT_EQ(session->ua_entry_count, 9u); + EXPECT_EQ(session_ua_entry(session, 8)->len, 5u); + + ASSERT_EQ(feed("Agent\r\nUser-Agent: Tenth\r\n\r\n"), 0); + ASSERT_EQ(session->ua_entry_count, 2u); + EXPECT_EQ(session_ua_entry(session, 0)->offset, 0u); + EXPECT_EQ(session_ua_entry(session, 0)->len, 5u); + EXPECT_EQ(session_ua_entry(session, 0)->replacement_offset, 5u); + EXPECT_EQ(session_ua_entry(session, 1)->replacement_offset, 0u); +} + +TEST_F(HttpParserUATest, EntryCapacityOverflowIsNotAParseErrorOrSuccess) { + const char *request = "GET / HTTP/1.1\r\nUser-Agent: Original\r\n\r\n"; + session_reset_per_packet(session, request); + // Exercise the allocation size guard without attempting an enormous allocation. + session->ua_entries_overflow.n = std::numeric_limits::max(); + session->ua_entries_overflow.i = session->ua_entries_overflow.n; + session->ua_entry_count = UA_INLINE_ENTRIES; + EXPECT_EQ(http_parser_feed(session, request, strlen(request)), HTTP_PARSER_NO_MEMORY); + EXPECT_TRUE(session->ua_allocation_failed); + session->ua_entries_overflow.n = 0; + session->ua_entries_overflow.i = 0; +} + +TEST(HttpParserStandaloneTest, ReleasesGrownEntriesWithoutStateMutex) { + struct http_session session{}; + http_parser_init_session(&session); + std::string request = "GET / HTTP/1.1\r\n"; + for (size_t i = 0; i < 33; ++i) { + request += "User-Agent: Original\r\n"; + } + request += "\r\n"; + session_reset_per_packet(&session, request.data()); + EXPECT_EQ(http_parser_feed(&session, request.data(), request.size()), 0); + EXPECT_FALSE(session.state_lock_initialized); + EXPECT_NE(session.ua_entries_overflow.d, nullptr); + session_state_destroy(&session); + EXPECT_EQ(session.ua_entries_overflow.d, nullptr); + EXPECT_EQ(session.ua_entry_count, 0u); + session_state_destroy(&session); // repeated destruction is harmless +} + +TEST_F(HttpParserUATest, AllocationFailurePersistsAcrossPayloads) { + const char *request = "GET / HTTP/1.1\r\nUser-Agent: Original\r\n\r\n"; + session_reset_per_packet(session, request); + session->ua_entries_overflow.n = std::numeric_limits::max(); + session->ua_entries_overflow.i = session->ua_entries_overflow.n; + session->ua_entry_count = UA_INLINE_ENTRIES; + ASSERT_EQ(http_parser_feed(session, request, strlen(request)), HTTP_PARSER_NO_MEMORY); + session->ua_entries_overflow.n = 0; + session->ua_entries_overflow.i = 0; + + const auto stale_time = time(nullptr) - 301; + session->last_active = stale_time; + EXPECT_EQ(feed("Original\r\n\r\n"), HTTP_PARSER_NO_MEMORY); + EXPECT_TRUE(session->ua_allocation_failed); + EXPECT_GT(session->last_active, stale_time); + EXPECT_EQ(feed(request), HTTP_PARSER_NO_MEMORY); + + http_parser_init_session(session); + EXPECT_EQ(feed(request), 0); + EXPECT_FALSE(session->ua_allocation_failed); + EXPECT_EQ(session->ua_entry_count, 1u); } diff --git a/test/http_session_test.cc b/test/http_session_test.cc index c33cbf80..cdbdfd44 100644 --- a/test/http_session_test.cc +++ b/test/http_session_test.cc @@ -3,6 +3,7 @@ extern "C" { #include +#include } class HttpSessionTest : public ::testing::Test { @@ -210,3 +211,74 @@ TEST_F(HttpSessionTest, ResetPerMessage) { EXPECT_FALSE(s->last_was_value); EXPECT_FALSE(s->in_ua_value); } + +class HttpSessionActivityTest : public HttpSessionTest { +protected: + void expect_fragment_keeps_session(const char *start, const char *fragment) { + session_wrlock(); + const auto active_key = session_key_from_connid(50); + const auto idle_key = session_key_from_connid(51); + auto *active = session_create(&active_key); + auto *idle = session_create(&idle_key); + session_wrunlock(); + ASSERT_NE(active, nullptr); + ASSERT_NE(idle, nullptr); + http_parser_init_session(active); + + session_state_lock(active); + session_reset_per_packet(active, start); + const int start_result = http_parser_feed(active, start, strlen(start)); + session_state_unlock(active); + ASSERT_EQ(start_result, 0); + + const auto stale_time = time(nullptr) - 301; + idle->last_active = stale_time; + session_state_lock(active); + active->last_active = stale_time; + session_reset_per_packet(active, fragment); + const int fragment_result = http_parser_feed(active, fragment, strlen(fragment)); + const auto last_active = active->last_active; + session_state_unlock(active); + ASSERT_EQ(fragment_result, 0); + EXPECT_GT(last_active, stale_time); + + session_wrlock(); + EXPECT_EQ(session_cleanup_expired(300), 1); + EXPECT_EQ(session_find(&active_key), active); + EXPECT_EQ(session_find(&idle_key), nullptr); + session_wrunlock(); + } +}; + +TEST_F(HttpSessionActivityTest, ContentLengthBodyProgressRefreshesIdleTtl) { + expect_fragment_keeps_session("POST / HTTP/1.1\r\nContent-Length: 100000\r\n\r\n", "body fragment"); +} + +TEST_F(HttpSessionActivityTest, ChunkedBodyProgressRefreshesIdleTtl) { + expect_fragment_keeps_session("POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n10000\r\n", "body fragment"); +} + +TEST_F(HttpSessionActivityTest, HeaderFieldProgressRefreshesIdleTtl) { + expect_fragment_keeps_session("GET / HTTP/1.1\r\nUser-", "Agent"); +} + +TEST_F(HttpSessionActivityTest, HeaderValueProgressRefreshesIdleTtl) { + expect_fragment_keeps_session("GET / HTTP/1.1\r\nUser-Agent: Original", "Agent"); +} + +TEST_F(HttpSessionActivityTest, EmptyFeedDoesNotRefreshIdleTtl) { + session_wrlock(); + const auto key = session_key_from_connid(52); + auto *session = session_create(&key); + session_wrunlock(); + ASSERT_NE(session, nullptr); + http_parser_init_session(session); + const auto stale_time = time(nullptr) - 301; + session->last_active = stale_time; + session_reset_per_packet(session, ""); + EXPECT_EQ(http_parser_feed(session, "", 0), 0); + EXPECT_EQ(session->last_active, stale_time); + session_wrlock(); + EXPECT_EQ(session_cleanup_expired(300), 1); + session_wrunlock(); +}