|
19 | 19 | */ |
20 | 20 |
|
21 | 21 | #include "httpserver/iovec_response.hpp" |
| 22 | +#include "httpserver/iovec_entry.hpp" |
| 23 | + |
| 24 | +#include <cstddef> |
| 25 | +#include <limits> |
22 | 26 | #include <microhttpd.h> |
| 27 | +#include <sys/uio.h> |
| 28 | +#include <type_traits> |
23 | 29 | #include <vector> |
24 | 30 |
|
25 | 31 | struct MHD_Response; |
26 | 32 |
|
27 | 33 | namespace httpserver { |
28 | 34 |
|
| 35 | +// --------------------------------------------------------------------------- |
| 36 | +// TASK-004: layout-pinning static_asserts. |
| 37 | +// |
| 38 | +// httpserver::iovec_entry is the public scatter/gather POD; libmicrohttpd's |
| 39 | +// MHD_IoVec is the actual cast target on the dispatch path. POSIX struct |
| 40 | +// iovec is asserted in parallel because the spec mandates it and because |
| 41 | +// every platform we ship to defines all three with identical layout |
| 42 | +// (glibc, musl, macOS, FreeBSD, NetBSD, OpenBSD, illumos). |
| 43 | +// |
| 44 | +// LIBHTTPSERVER_TODO_TASK004_MEMCPY_FALLBACK: if any of the asserts below |
| 45 | +// ever fires on a divergent-layout platform, the fix is to replace the |
| 46 | +// reinterpret_cast in the dispatch path with an element-by-element copy |
| 47 | +// into a stack/heap MHD_IoVec[]. Until such a platform appears the |
| 48 | +// asserts are the gate — a build failure on the divergent platform is |
| 49 | +// the desired outcome (loud, immediate, with the assert string naming |
| 50 | +// what diverged). |
| 51 | +// --------------------------------------------------------------------------- |
| 52 | +static_assert(sizeof(::httpserver::iovec_entry) == sizeof(struct iovec), |
| 53 | + "iovec_entry size must match POSIX struct iovec — divergent platform; " |
| 54 | + "implement memcpy fallback (see TASK-004)"); |
| 55 | +static_assert(offsetof(::httpserver::iovec_entry, base) == |
| 56 | + offsetof(struct iovec, iov_base), |
| 57 | + "iovec_entry::base offset must match struct iovec::iov_base"); |
| 58 | +static_assert(offsetof(::httpserver::iovec_entry, len) == |
| 59 | + offsetof(struct iovec, iov_len), |
| 60 | + "iovec_entry::len offset must match struct iovec::iov_len"); |
| 61 | + |
| 62 | +static_assert(sizeof(::httpserver::iovec_entry) == sizeof(MHD_IoVec), |
| 63 | + "iovec_entry size must match libmicrohttpd MHD_IoVec — MHD layout drift"); |
| 64 | +static_assert(offsetof(::httpserver::iovec_entry, base) == |
| 65 | + offsetof(MHD_IoVec, iov_base), |
| 66 | + "iovec_entry::base offset must match MHD_IoVec::iov_base"); |
| 67 | +static_assert(offsetof(::httpserver::iovec_entry, len) == |
| 68 | + offsetof(MHD_IoVec, iov_len), |
| 69 | + "iovec_entry::len offset must match MHD_IoVec::iov_len"); |
| 70 | + |
| 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 | + |
29 | 108 | MHD_Response* iovec_response::get_raw_response() { |
30 | | - // MHD_create_response_from_iovec makes an internal copy of the iov array, |
31 | | - // so the local vector is safe. The buffer data pointed to by iov_base must |
32 | | - // remain valid until the response is destroyed — this is guaranteed because |
33 | | - // the buffers are owned by this iovec_response object. |
34 | | - std::vector<MHD_IoVec> iov(buffers.size()); |
35 | | - for (size_t i = 0; i < buffers.size(); ++i) { |
36 | | - iov[i].iov_base = buffers[i].data(); |
37 | | - iov[i].iov_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; |
38 | 118 | } |
| 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. |
39 | 126 | return MHD_create_response_from_iovec( |
40 | | - iov.data(), |
41 | | - static_cast<unsigned int>(iov.size()), |
| 127 | + reinterpret_cast<const MHD_IoVec*>(entries_.data()), |
| 128 | + static_cast<unsigned int>(entries_.size()), |
42 | 129 | nullptr, |
43 | 130 | nullptr); |
44 | 131 | } |
|
0 commit comments