Skip to content

Commit 259b4bb

Browse files
etrclaude
andcommitted
TASK-004: fix use-after-free in copy ctor, header hygiene, test improvements
- Delete copy constructor and copy assignment on iovec_response to close CWE-416 use-after-free: the owning constructor stores entries_ as raw void* into owned_buffers_ strings; a defaulted copy would shallow-copy entries_ while deep-copying owned_buffers_ to new addresses, making entries_ dangle after source destruction. Move semantics are safe and kept. Static asserts in iovec_response_test.cpp guard this invariant. - Remove the spurious '#include "httpserver/iovec_entry.hpp"' from http_response.hpp; http_response itself never uses iovec_entry, and iovec_response.hpp already includes it directly. - Add @attention Doxygen contract to the non-owning iovec_response constructor documenting that caller buffers must outlive MHD_destroy_response. - Remove duplicate offsetof/sizeof/alignof layout-pinning static_asserts from iovec_entry_test.cpp; authoritative copies live in iovec_response.cpp where the reinterpret_cast actually occurs. - Add iovec_response_test.cpp (was untracked) with content-type forwarding tests and move-semantics tests for both constructor variants. - Commit iovec_response.hpp, iovec_response.cpp, and test/Makefile.am that were modified/added in iter-1 but never staged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 74c1726 commit 259b4bb

7 files changed

Lines changed: 296 additions & 60 deletions

File tree

src/httpserver/http_response.hpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
#include <string>
3131
#include "httpserver/http_arg_value.hpp"
3232
#include "httpserver/http_utils.hpp"
33-
#include "httpserver/iovec_entry.hpp"
3433

3534
struct MHD_Connection;
3635
struct MHD_Response;

src/httpserver/iovec_response.hpp

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include <vector>
3131
#include "httpserver/http_utils.hpp"
3232
#include "httpserver/http_response.hpp"
33+
#include "httpserver/iovec_entry.hpp"
3334

3435
struct MHD_Response;
3536

@@ -39,25 +40,66 @@ class iovec_response : public http_response {
3940
public:
4041
iovec_response() = default;
4142

43+
// Owning constructor: the response takes ownership of the string buffers.
44+
// The iovec_entry array is built eagerly at construction so get_raw_response()
45+
// allocates nothing on the hot dispatch path.
4246
explicit iovec_response(
43-
std::vector<std::string> buffers,
47+
std::vector<std::string> owned_buffers,
4448
int response_code = http::http_utils::http_ok,
45-
const std::string& content_type = http::http_utils::text_plain):
46-
http_response(response_code, content_type),
47-
buffers(std::move(buffers)) { }
49+
const std::string& content_type = http::http_utils::text_plain);
50+
51+
/**
52+
* Non-owning constructor: the caller supplies pre-built iovec_entry pairs.
53+
* This is TASK-004's genuine zero-copy path: no heap allocation or data
54+
* copy is performed.
55+
*
56+
* @attention The caller is responsible for keeping the pointed-to buffers
57+
* alive at least until MHD_destroy_response() returns for the response
58+
* produced by get_raw_response(). libmicrohttpd holds a reference to the
59+
* buffer pointers until MHD_destroy_response() is called in the dispatch
60+
* path (webserver.cpp). Freeing any backing buffer before that point
61+
* causes a use-after-free inside libmicrohttpd (CWE-416). In practice
62+
* this means the buffers must outlive the iovec_response object AND the
63+
* MHD response lifecycle, which ends at MHD_destroy_response().
64+
*
65+
* @note This API surface is transitional (see PRD-RSP-REQ-006 /
66+
* TASK-010); it will be removed or replaced in a future v2.0 revision.
67+
*/
68+
explicit iovec_response(
69+
std::vector<iovec_entry> caller_entries,
70+
int response_code = http::http_utils::http_ok,
71+
const std::string& content_type = http::http_utils::text_plain);
72+
73+
// Copy construction and copy assignment are deleted: the owning constructor
74+
// stores void* pointers (entries_) into owned_buffers_ string storage.
75+
// A defaulted copy would shallow-copy entries_ while deep-copying
76+
// owned_buffers_ to new addresses, making entries_ dangle as soon as the
77+
// source is destroyed (CWE-416). Deletion forces callers onto move
78+
// semantics, which are safe because std::vector move transfers the heap
79+
// block and keeps string addresses stable.
80+
iovec_response(const iovec_response&) = delete;
81+
iovec_response& operator=(const iovec_response&) = delete;
4882

49-
iovec_response(const iovec_response& other) = default;
5083
iovec_response(iovec_response&& other) noexcept = default;
51-
52-
iovec_response& operator=(const iovec_response& b) = default;
53-
iovec_response& operator=(iovec_response&& b) = default;
84+
iovec_response& operator=(iovec_response&& b) noexcept = default;
5485

5586
~iovec_response() = default;
5687

88+
// Returns a new MHD_Response* or nullptr on error (e.g. buffer count
89+
// exceeds MHD's unsigned-int limit). The caller does not own the returned
90+
// pointer; MHD manages its lifetime. May return nullptr; all callers on
91+
// the dispatch path must check before use.
5792
MHD_Response* get_raw_response();
5893

5994
private:
60-
std::vector<std::string> buffers;
95+
// Owned string buffers (populated by the owning constructor).
96+
std::vector<std::string> owned_buffers_;
97+
98+
// Flattened iovec_entry array ready for the MHD cast. For the owning
99+
// constructor this is populated at construction time (zero allocation on
100+
// dispatch). For the non-owning constructor the caller-supplied entries
101+
// are stored directly.
102+
std::vector<iovec_entry> entries_;
61103
};
62104

63105
} // namespace httpserver

src/iovec_response.cpp

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222
#include "httpserver/iovec_entry.hpp"
2323

2424
#include <cstddef>
25+
#include <limits>
2526
#include <microhttpd.h>
2627
#include <sys/uio.h>
28+
#include <type_traits>
2729
#include <vector>
2830

2931
struct MHD_Response;
@@ -66,25 +68,64 @@ static_assert(offsetof(::httpserver::iovec_entry, len) ==
6668
offsetof(MHD_IoVec, iov_len),
6769
"iovec_entry::len offset must match MHD_IoVec::iov_len");
6870

71+
// Alignment pinning: ensures the reinterpret_cast array stride is safe on
72+
// architectures that trap on misaligned loads (SPARC, some ARM configs).
73+
// CWE-704: without alignof equality the cast is UB even when size/offset match.
74+
static_assert(alignof(::httpserver::iovec_entry) == alignof(struct iovec),
75+
"iovec_entry alignment must match POSIX struct iovec — divergent platform; "
76+
"implement memcpy fallback (see TASK-004)");
77+
static_assert(alignof(::httpserver::iovec_entry) == alignof(MHD_IoVec),
78+
"iovec_entry alignment must match MHD_IoVec — MHD layout drift");
79+
80+
// Standard-layout guarantee: required so that reinterpret_cast between
81+
// pointer-interconvertible types is well-defined under -fstrict-aliasing.
82+
static_assert(std::is_standard_layout_v<::httpserver::iovec_entry>,
83+
"iovec_entry must be standard layout for reinterpret_cast to MHD_IoVec");
84+
85+
iovec_response::iovec_response(
86+
std::vector<std::string> owned_buffers,
87+
int response_code,
88+
const std::string& content_type)
89+
: http_response(response_code, content_type),
90+
owned_buffers_(std::move(owned_buffers)) {
91+
// Build the iovec_entry array eagerly so get_raw_response() is
92+
// allocation-free on the hot dispatch path.
93+
entries_.reserve(owned_buffers_.size());
94+
for (const auto& b : owned_buffers_) {
95+
entries_.push_back({b.data(), b.size()});
96+
}
97+
}
98+
99+
iovec_response::iovec_response(
100+
std::vector<iovec_entry> caller_entries,
101+
int response_code,
102+
const std::string& content_type)
103+
: http_response(response_code, content_type),
104+
entries_(std::move(caller_entries)) {
105+
// owned_buffers_ is empty — buffer ownership stays with the caller.
106+
}
107+
69108
MHD_Response* iovec_response::get_raw_response() {
70-
// MHD_create_response_from_iovec makes an internal copy of the iov array,
71-
// so the local vector is safe. The buffer data pointed to by iov_base must
72-
// remain valid until the response is destroyed — this is guaranteed because
73-
// the buffers are owned by this iovec_response object.
74-
//
75-
// The dispatch path builds a contiguous std::vector<iovec_entry> from the
76-
// owned std::strings, then reinterpret_casts it to const MHD_IoVec* when
77-
// calling MHD. The cast is well-defined because the layout-pinning
78-
// static_asserts above guarantee identical size and field offsets. This
79-
// same cast bridge will move into details/body.hpp when TASK-009 lands.
80-
std::vector<iovec_entry> entries(buffers.size());
81-
for (size_t i = 0; i < buffers.size(); ++i) {
82-
entries[i].base = buffers[i].data();
83-
entries[i].len = buffers[i].size();
109+
// Guard against integer narrowing: MHD_create_response_from_iovec takes
110+
// an unsigned int count. A vector with more than UINT_MAX entries would
111+
// silently truncate, causing MHD to read only part of the array while the
112+
// reported body length diverges from the actual allocation (CWE-190,
113+
// CWE-125). Return nullptr (the documented MHD "error" sentinel) instead.
114+
if (entries_.size() >
115+
static_cast<std::size_t>(
116+
std::numeric_limits<unsigned int>::max())) {
117+
return nullptr;
84118
}
119+
120+
// The reinterpret_cast is well-defined because the layout-pinning
121+
// static_asserts above guarantee identical size, field offsets, and
122+
// alignment between iovec_entry and MHD_IoVec (C++ [basic.align],
123+
// CWE-704). entries_ was populated at construction time: no heap
124+
// allocation occurs on this path. The cast bridge will move into
125+
// details/body.hpp when TASK-009 lands.
85126
return MHD_create_response_from_iovec(
86-
reinterpret_cast<const MHD_IoVec*>(entries.data()),
87-
static_cast<unsigned int>(entries.size()),
127+
reinterpret_cast<const MHD_IoVec*>(entries_.data()),
128+
static_cast<unsigned int>(entries_.size()),
88129
nullptr,
89130
nullptr);
90131
}

test/Makefile.am

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ LDADD += -lcurl
2626

2727
AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/ -DHTTPSERVER_COMPILATION
2828
METASOURCES = AUTO
29-
check_PROGRAMS = basic file_upload http_utils threaded nodelay string_utilities http_endpoint ban_system ws_start_stop authentication deferred http_resource http_response create_webserver new_response_types daemon_info uri_log feature_unavailable header_hygiene_iovec iovec_entry
29+
check_PROGRAMS = basic file_upload http_utils threaded nodelay string_utilities http_endpoint ban_system ws_start_stop authentication deferred http_resource http_response create_webserver new_response_types daemon_info uri_log feature_unavailable header_hygiene_iovec iovec_entry iovec_response
3030

3131
MOSTLYCLEANFILES = *.gcda *.gcno *.gcov
3232

@@ -54,6 +54,7 @@ uri_log_LDADD = $(LDADD) -lmicrohttpd
5454
feature_unavailable_SOURCES = unit/feature_unavailable_test.cpp
5555
header_hygiene_iovec_SOURCES = unit/header_hygiene_iovec_test.cpp
5656
iovec_entry_SOURCES = unit/iovec_entry_test.cpp
57+
iovec_response_SOURCES = unit/iovec_response_test.cpp
5758

5859
noinst_HEADERS = littletest.hpp
5960
AM_CXXFLAGS += -Wall -fPIC -Wno-overloaded-virtual

test/unit/header_hygiene_iovec_test.cpp

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,36 @@
2121
// Header-hygiene sentinel for TASK-004:
2222
//
2323
// AC #4 of TASK-004 ("public header must not include <sys/uio.h>") is
24-
// scoped to the new iovec_entry header itself; the broader umbrella-leak
25-
// concern (current umbrella transitively pulls <sys/uio.h> via gnutls/
26-
// <sys/socket.h>) is the remit of TASK-007's header-hygiene CI gate.
24+
// enforced by including iovec_entry.hpp in isolation, then checking the
25+
// well-known include-guard macros that <sys/uio.h> defines on every
26+
// supported platform:
2727
//
28-
// To enforce the local guarantee, this TU declares a colliding
29-
// `struct iovec` BEFORE including iovec_entry.hpp directly. If the
30-
// header (or anything it pulls in) pulls <sys/uio.h>, the system
31-
// definition collides with this sentinel and the build fails with a
32-
// redefinition error. The TU compiling at all is the assertion.
33-
struct iovec {
34-
int libhttpserver_hygiene_sentinel;
35-
};
36-
37-
// Include the new POD header in isolation to verify it pulls no
38-
// surprise dependencies. HTTPSERVER_COMPILATION is already defined by
39-
// AM_CPPFLAGS in test/Makefile.am, so the gate is satisfied.
28+
// Linux/glibc: _SYS_UIO_H (set by <sys/uio.h>)
29+
// macOS/BSD: _SYS_UIO_H_ (set by <sys/uio.h>)
30+
// musl: _SYS_UIO_H (same as glibc)
31+
//
32+
// If any of those macros is defined after including iovec_entry.hpp, the
33+
// header has leaked <sys/uio.h> and the build fails with a descriptive
34+
// #error message. The TU compiling at all (and none of those macros being
35+
// defined) is the assertion — no runtime test is needed for this guarantee.
36+
//
37+
// HTTPSERVER_COMPILATION is defined by AM_CPPFLAGS in test/Makefile.am
38+
// so the inclusion guard in iovec_entry.hpp is satisfied.
39+
4040
#include "httpserver/iovec_entry.hpp"
4141

42+
// --- preprocessor-based leak detection ------------------------------------
43+
44+
#ifdef _SYS_UIO_H
45+
# error "<sys/uio.h> was pulled in transitively by httpserver/iovec_entry.hpp (glibc/musl guard _SYS_UIO_H)"
46+
#endif
47+
48+
#ifdef _SYS_UIO_H_
49+
# error "<sys/uio.h> was pulled in transitively by httpserver/iovec_entry.hpp (macOS/BSD guard _SYS_UIO_H_)"
50+
#endif
51+
52+
// --------------------------------------------------------------------------
53+
4254
#include "./littletest.hpp"
4355

4456
LT_BEGIN_SUITE(header_hygiene_iovec_suite)
@@ -49,10 +61,18 @@ LT_BEGIN_SUITE(header_hygiene_iovec_suite)
4961
}
5062
LT_END_SUITE(header_hygiene_iovec_suite)
5163

64+
// Verify that iovec_entry is accessible and sizeof/alignof are non-zero
65+
// without any POSIX headers in scope. This confirms that no system types
66+
// leaked in through iovec_entry.hpp and that the type is self-contained.
5267
LT_BEGIN_AUTO_TEST(header_hygiene_iovec_suite, iovec_entry_visible_without_sys_uio)
53-
httpserver::iovec_entry e{nullptr, 0};
54-
LT_CHECK_EQ(e.base, nullptr);
55-
LT_CHECK_EQ(e.len, 0u);
68+
// If any system header leaked in, alignof/sizeof would still be correct,
69+
// but the #error directives above ensure this test is only reached on a
70+
// clean TU. These checks confirm the type is truly self-contained.
71+
static_assert(sizeof(httpserver::iovec_entry) > 0,
72+
"iovec_entry must have non-zero size without sys/uio.h");
73+
static_assert(alignof(httpserver::iovec_entry) > 0,
74+
"iovec_entry must have non-zero alignment without sys/uio.h");
75+
LT_CHECK_EQ(true, true); // TU compiled clean: no sys/uio.h leak detected
5676
LT_END_AUTO_TEST(iovec_entry_visible_without_sys_uio)
5777

5878
LT_BEGIN_AUTO_TEST_ENV()

test/unit/iovec_entry_test.cpp

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@
1919
*/
2020

2121
// Layout / POD-trait verification for `httpserver::iovec_entry`.
22-
// This TU is allowed to include <sys/uio.h> directly — it is an internal
23-
// test, not a header-hygiene sentinel. The library-side guarantee that
24-
// downstream code does NOT see <sys/uio.h> via the umbrella is asserted
25-
// separately by `header_hygiene_iovec_test.cpp`.
22+
// This TU is allowed to include <sys/uio.h> and <microhttpd.h> directly —
23+
// it is an internal test, not a header-hygiene sentinel. The library-side
24+
// guarantee that downstream code does NOT see <sys/uio.h> via the umbrella
25+
// is asserted separately by `header_hygiene_iovec_test.cpp`.
2626

2727
#include <cstddef>
28+
#include <microhttpd.h>
2829
#include <sys/uio.h>
2930
#include <type_traits>
3031

@@ -46,17 +47,6 @@ static_assert(std::is_same_v<decltype(httpserver::iovec_entry::len),
4647
std::size_t>,
4748
"iovec_entry::len must be std::size_t");
4849

49-
// Layout pinning duplicated from the consumer perspective: defense in depth
50-
// against a future change to <sys/uio.h> on a divergent platform.
51-
static_assert(sizeof(httpserver::iovec_entry) == sizeof(struct iovec),
52-
"iovec_entry size must match POSIX struct iovec");
53-
static_assert(offsetof(httpserver::iovec_entry, base) ==
54-
offsetof(struct iovec, iov_base),
55-
"iovec_entry::base offset must match struct iovec::iov_base");
56-
static_assert(offsetof(httpserver::iovec_entry, len) ==
57-
offsetof(struct iovec, iov_len),
58-
"iovec_entry::len offset must match struct iovec::iov_len");
59-
6050
LT_BEGIN_SUITE(iovec_entry_suite)
6151
void set_up() {
6252
}
@@ -97,6 +87,34 @@ LT_BEGIN_AUTO_TEST(iovec_entry_suite, reinterpret_cast_to_struct_iovec_preserves
9787
LT_CHECK_EQ(posix[1].iov_len, 4u);
9888
LT_END_AUTO_TEST(reinterpret_cast_to_struct_iovec_preserves_data)
9989

90+
// Runtime bridge test for the actual production cast path: iovec_entry →
91+
// MHD_IoVec. Mirrors the struct iovec test above but exercises the type
92+
// used at dispatch time in iovec_response::get_raw_response().
93+
LT_BEGIN_AUTO_TEST(iovec_entry_suite, reinterpret_cast_to_MHD_IoVec_preserves_data)
94+
const char* a = "hello";
95+
const char* b = "world";
96+
httpserver::iovec_entry entries[2] = {
97+
{a, 5},
98+
{b, 5},
99+
};
100+
const MHD_IoVec* mhd =
101+
reinterpret_cast<const MHD_IoVec*>(&entries[0]);
102+
LT_CHECK_EQ(mhd[0].iov_base, static_cast<const void*>(a));
103+
LT_CHECK_EQ(mhd[0].iov_len, 5u);
104+
LT_CHECK_EQ(mhd[1].iov_base, static_cast<const void*>(b));
105+
LT_CHECK_EQ(mhd[1].iov_len, 5u);
106+
LT_END_AUTO_TEST(reinterpret_cast_to_MHD_IoVec_preserves_data)
107+
108+
// Verify trivially-copyable guarantee has observable runtime effect:
109+
// a copy-constructed iovec_entry must preserve both members.
110+
LT_BEGIN_AUTO_TEST(iovec_entry_suite, copy_constructed_iovec_entry_preserves_members)
111+
const char* payload = "data";
112+
httpserver::iovec_entry original{payload, 4};
113+
httpserver::iovec_entry copy = original; // copy construction
114+
LT_CHECK_EQ(copy.base, static_cast<const void*>(payload));
115+
LT_CHECK_EQ(copy.len, 4u);
116+
LT_END_AUTO_TEST(copy_constructed_iovec_entry_preserves_members)
117+
100118
LT_BEGIN_AUTO_TEST_ENV()
101119
AUTORUN_TESTS()
102120
LT_END_AUTO_TEST_ENV()

0 commit comments

Comments
 (0)