/src/curl_fuzzer/nghttp2_contract_shim.cc
Line | Count | Source |
1 | | /* |
2 | | * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al. |
3 | | * |
4 | | * SPDX-License-Identifier: curl |
5 | | */ |
6 | | |
7 | | #include <nghttp2/nghttp2.h> |
8 | | |
9 | | #include <array> |
10 | | #include <atomic> |
11 | | #include <cstdio> |
12 | | #include <cstdlib> |
13 | | #include <mutex> |
14 | | #include <type_traits> |
15 | | |
16 | | namespace { |
17 | | |
18 | | // Fatal nghttp2 errors invalidate a session. Keep this assertion state out of |
19 | | // the heap so allocation-failure fuzzing cannot disable or perturb the check. |
20 | | struct PoisonedSession { |
21 | | nghttp2_session *session = nullptr; |
22 | | const char *fatal_api = nullptr; |
23 | | long long fatal_result = 0; |
24 | | }; |
25 | | |
26 | | constexpr std::size_t kMaximumPoisonedSessions = 256; |
27 | | std::array<PoisonedSession, kMaximumPoisonedSessions> poisoned_sessions{}; |
28 | | std::mutex poisoned_sessions_mutex; |
29 | | std::atomic<std::size_t> poisoned_session_count{0}; |
30 | | |
31 | | [[noreturn]] void AbortContractViolation(const char *attempted_api, |
32 | 0 | const PoisonedSession &poisoned) { |
33 | 0 | std::fprintf( |
34 | 0 | stderr, |
35 | 0 | "nghttp2 contract assertion failed: %s called on session %p after " |
36 | 0 | "%s returned %lld (< NGHTTP2_ERR_FATAL); only " |
37 | 0 | "nghttp2_session_del is allowed\n", |
38 | 0 | attempted_api, static_cast<void *>(poisoned.session), poisoned.fatal_api, |
39 | 0 | poisoned.fatal_result); |
40 | 0 | std::fflush(stderr); |
41 | 0 | std::abort(); |
42 | 0 | } |
43 | | |
44 | | [[noreturn]] void AbortTrackingOverflow(nghttp2_session *session, |
45 | | const char *fatal_api, |
46 | 0 | long long fatal_result) { |
47 | 0 | std::fprintf(stderr, |
48 | 0 | "nghttp2 contract assertion failed: cannot track session %p " |
49 | 0 | "after %s returned %lld; poisoned-session table is full\n", |
50 | 0 | static_cast<void *>(session), fatal_api, fatal_result); |
51 | 0 | std::fflush(stderr); |
52 | 0 | std::abort(); |
53 | 0 | } |
54 | | |
55 | 0 | void CheckUsable(nghttp2_session *session, const char *attempted_api) { |
56 | 0 | if (session == nullptr || |
57 | 0 | poisoned_session_count.load(std::memory_order_acquire) == 0) { |
58 | 0 | return; |
59 | 0 | } |
60 | | |
61 | 0 | PoisonedSession poisoned; |
62 | 0 | { |
63 | 0 | std::lock_guard<std::mutex> lock(poisoned_sessions_mutex); |
64 | 0 | for (const PoisonedSession &entry : poisoned_sessions) { |
65 | 0 | if (entry.session == session) { |
66 | 0 | poisoned = entry; |
67 | 0 | break; |
68 | 0 | } |
69 | 0 | } |
70 | 0 | } |
71 | 0 | if (poisoned.session != nullptr) { |
72 | 0 | AbortContractViolation(attempted_api, poisoned); |
73 | 0 | } |
74 | 0 | } |
75 | | |
76 | 0 | void ForgetSession(nghttp2_session *session) { |
77 | 0 | if (session == nullptr || |
78 | 0 | poisoned_session_count.load(std::memory_order_acquire) == 0) { |
79 | 0 | return; |
80 | 0 | } |
81 | | |
82 | 0 | std::lock_guard<std::mutex> lock(poisoned_sessions_mutex); |
83 | 0 | for (PoisonedSession &entry : poisoned_sessions) { |
84 | 0 | if (entry.session == session) { |
85 | 0 | entry = PoisonedSession{}; |
86 | 0 | poisoned_session_count.fetch_sub(1, std::memory_order_release); |
87 | 0 | return; |
88 | 0 | } |
89 | 0 | } |
90 | 0 | } |
91 | | |
92 | 0 | void RecordResult(nghttp2_session *session, const char *api, long long result) { |
93 | 0 | if (session == nullptr || result >= NGHTTP2_ERR_FATAL) { |
94 | 0 | return; |
95 | 0 | } |
96 | | |
97 | 0 | bool table_full = false; |
98 | 0 | { |
99 | 0 | std::lock_guard<std::mutex> lock(poisoned_sessions_mutex); |
100 | 0 | PoisonedSession *empty_entry = nullptr; |
101 | 0 | for (PoisonedSession &entry : poisoned_sessions) { |
102 | 0 | if (entry.session == session) { |
103 | | // Preserve the first fatal result; it is the point at which the |
104 | | // application was required to stop using this session. |
105 | 0 | return; |
106 | 0 | } |
107 | 0 | if (entry.session == nullptr && empty_entry == nullptr) { |
108 | 0 | empty_entry = &entry; |
109 | 0 | } |
110 | 0 | } |
111 | 0 | if (empty_entry != nullptr) { |
112 | 0 | empty_entry->session = session; |
113 | 0 | empty_entry->fatal_api = api; |
114 | 0 | empty_entry->fatal_result = result; |
115 | 0 | poisoned_session_count.fetch_add(1, std::memory_order_release); |
116 | 0 | } else { |
117 | 0 | table_full = true; |
118 | 0 | } |
119 | 0 | } |
120 | 0 | if (table_full) { |
121 | 0 | AbortTrackingOverflow(session, api, result); |
122 | 0 | } |
123 | 0 | } |
124 | | |
125 | | } // namespace |
126 | | |
127 | | // GNU ld and lld provide each __real_* symbol when the final link uses the |
128 | | // matching --wrap option. decltype keeps these declarations tied to the |
129 | | // installed nghttp2 headers and makes signature drift a compile-time failure. |
130 | | extern "C" decltype(nghttp2_session_client_new3) |
131 | | __real_nghttp2_session_client_new3; |
132 | | extern "C" decltype(nghttp2_session_del) __real_nghttp2_session_del; |
133 | | extern "C" decltype(nghttp2_session_check_request_allowed) |
134 | | __real_nghttp2_session_check_request_allowed; |
135 | | extern "C" decltype(nghttp2_session_consume) __real_nghttp2_session_consume; |
136 | | extern "C" decltype(nghttp2_session_get_local_window_size) |
137 | | __real_nghttp2_session_get_local_window_size; |
138 | | extern "C" decltype(nghttp2_session_get_remote_settings) |
139 | | __real_nghttp2_session_get_remote_settings; |
140 | | extern "C" decltype(nghttp2_session_get_remote_window_size) |
141 | | __real_nghttp2_session_get_remote_window_size; |
142 | | extern "C" decltype(nghttp2_session_get_stream_effective_local_window_size) |
143 | | __real_nghttp2_session_get_stream_effective_local_window_size; |
144 | | extern "C" decltype(nghttp2_session_get_stream_effective_recv_data_length) |
145 | | __real_nghttp2_session_get_stream_effective_recv_data_length; |
146 | | extern "C" decltype(nghttp2_session_get_stream_remote_window_size) |
147 | | __real_nghttp2_session_get_stream_remote_window_size; |
148 | | extern "C" decltype(nghttp2_session_get_stream_user_data) |
149 | | __real_nghttp2_session_get_stream_user_data; |
150 | | extern "C" decltype(nghttp2_session_mem_recv) __real_nghttp2_session_mem_recv; |
151 | | extern "C" decltype(nghttp2_session_resume_data) |
152 | | __real_nghttp2_session_resume_data; |
153 | | extern "C" decltype(nghttp2_session_send) __real_nghttp2_session_send; |
154 | | extern "C" decltype(nghttp2_session_set_local_window_size) |
155 | | __real_nghttp2_session_set_local_window_size; |
156 | | extern "C" decltype(nghttp2_session_set_stream_user_data) |
157 | | __real_nghttp2_session_set_stream_user_data; |
158 | | extern "C" decltype(nghttp2_session_upgrade2) __real_nghttp2_session_upgrade2; |
159 | | extern "C" decltype(nghttp2_session_want_read) __real_nghttp2_session_want_read; |
160 | | extern "C" decltype(nghttp2_session_want_write) |
161 | | __real_nghttp2_session_want_write; |
162 | | extern "C" decltype(nghttp2_submit_goaway) __real_nghttp2_submit_goaway; |
163 | | extern "C" decltype(nghttp2_submit_ping) __real_nghttp2_submit_ping; |
164 | | extern "C" decltype(nghttp2_submit_priority) __real_nghttp2_submit_priority; |
165 | | extern "C" decltype(nghttp2_submit_request) __real_nghttp2_submit_request; |
166 | | extern "C" decltype(nghttp2_submit_rst_stream) __real_nghttp2_submit_rst_stream; |
167 | | extern "C" decltype(nghttp2_submit_settings) __real_nghttp2_submit_settings; |
168 | | extern "C" decltype(nghttp2_submit_window_update) |
169 | | __real_nghttp2_submit_window_update; |
170 | | |
171 | | extern "C" int __wrap_nghttp2_session_client_new3( |
172 | | nghttp2_session **session_ptr, const nghttp2_session_callbacks *callbacks, |
173 | 0 | void *user_data, const nghttp2_option *option, nghttp2_mem *mem) { |
174 | 0 | const int result = __real_nghttp2_session_client_new3(session_ptr, callbacks, |
175 | 0 | user_data, option, mem); |
176 | 0 | if (result == 0 && session_ptr != nullptr) { |
177 | | // A custom allocator may deliberately reuse a recently deleted address. |
178 | 0 | ForgetSession(*session_ptr); |
179 | 0 | } |
180 | 0 | return result; |
181 | 0 | } |
182 | | static_assert(std::is_same<decltype(nghttp2_session_client_new3), |
183 | | decltype(__wrap_nghttp2_session_client_new3)>::value, |
184 | | "nghttp2_session_client_new3 signature changed"); |
185 | | |
186 | 0 | extern "C" void __wrap_nghttp2_session_del(nghttp2_session *session) { |
187 | | // Deallocation is the sole operation permitted after a fatal return. |
188 | 0 | ForgetSession(session); |
189 | 0 | __real_nghttp2_session_del(session); |
190 | 0 | } |
191 | | static_assert(std::is_same<decltype(nghttp2_session_del), |
192 | | decltype(__wrap_nghttp2_session_del)>::value, |
193 | | "nghttp2_session_del signature changed"); |
194 | | |
195 | | #define DEFINE_CHECKED_WRAPPER(name, parameters, arguments) \ |
196 | | extern "C" auto __wrap_##name parameters \ |
197 | 0 | ->decltype(__real_##name arguments) { \ |
198 | 0 | CheckUsable(session, #name); \ |
199 | 0 | return __real_##name arguments; \ |
200 | 0 | } \ Unexecuted instantiation: __wrap_nghttp2_session_check_request_allowed Unexecuted instantiation: __wrap_nghttp2_session_get_local_window_size Unexecuted instantiation: __wrap_nghttp2_session_get_remote_settings Unexecuted instantiation: __wrap_nghttp2_session_get_remote_window_size Unexecuted instantiation: __wrap_nghttp2_session_get_stream_effective_local_window_size Unexecuted instantiation: __wrap_nghttp2_session_get_stream_effective_recv_data_length Unexecuted instantiation: __wrap_nghttp2_session_get_stream_remote_window_size Unexecuted instantiation: __wrap_nghttp2_session_get_stream_user_data Unexecuted instantiation: __wrap_nghttp2_session_want_read Unexecuted instantiation: __wrap_nghttp2_session_want_write |
201 | | static_assert(std::is_same<decltype(name), decltype(__wrap_##name)>::value, \ |
202 | | #name " signature changed"); |
203 | | |
204 | | #define DEFINE_RESULT_WRAPPER(name, parameters, arguments) \ |
205 | | extern "C" auto __wrap_##name parameters \ |
206 | 0 | ->decltype(__real_##name arguments) { \ |
207 | 0 | CheckUsable(session, #name); \ |
208 | 0 | const auto result = __real_##name arguments; \ |
209 | 0 | RecordResult(session, #name, static_cast<long long>(result)); \ |
210 | 0 | return result; \ |
211 | 0 | } \ Unexecuted instantiation: __wrap_nghttp2_session_consume Unexecuted instantiation: __wrap_nghttp2_session_mem_recv Unexecuted instantiation: __wrap_nghttp2_session_resume_data Unexecuted instantiation: __wrap_nghttp2_session_send Unexecuted instantiation: __wrap_nghttp2_session_set_local_window_size Unexecuted instantiation: __wrap_nghttp2_session_set_stream_user_data Unexecuted instantiation: __wrap_nghttp2_session_upgrade2 Unexecuted instantiation: __wrap_nghttp2_submit_goaway Unexecuted instantiation: __wrap_nghttp2_submit_ping Unexecuted instantiation: __wrap_nghttp2_submit_priority Unexecuted instantiation: __wrap_nghttp2_submit_request Unexecuted instantiation: __wrap_nghttp2_submit_rst_stream Unexecuted instantiation: __wrap_nghttp2_submit_settings Unexecuted instantiation: __wrap_nghttp2_submit_window_update |
212 | | static_assert(std::is_same<decltype(name), decltype(__wrap_##name)>::value, \ |
213 | | #name " signature changed"); |
214 | | |
215 | | DEFINE_CHECKED_WRAPPER(nghttp2_session_check_request_allowed, |
216 | | (nghttp2_session * session), (session)) |
217 | | DEFINE_RESULT_WRAPPER(nghttp2_session_consume, |
218 | | (nghttp2_session * session, int32_t stream_id, |
219 | | size_t size), |
220 | | (session, stream_id, size)) |
221 | | DEFINE_CHECKED_WRAPPER(nghttp2_session_get_local_window_size, |
222 | | (nghttp2_session * session), (session)) |
223 | | DEFINE_CHECKED_WRAPPER(nghttp2_session_get_remote_settings, |
224 | | (nghttp2_session * session, nghttp2_settings_id id), |
225 | | (session, id)) |
226 | | DEFINE_CHECKED_WRAPPER(nghttp2_session_get_remote_window_size, |
227 | | (nghttp2_session * session), (session)) |
228 | | DEFINE_CHECKED_WRAPPER(nghttp2_session_get_stream_effective_local_window_size, |
229 | | (nghttp2_session * session, int32_t stream_id), |
230 | | (session, stream_id)) |
231 | | DEFINE_CHECKED_WRAPPER(nghttp2_session_get_stream_effective_recv_data_length, |
232 | | (nghttp2_session * session, int32_t stream_id), |
233 | | (session, stream_id)) |
234 | | DEFINE_CHECKED_WRAPPER(nghttp2_session_get_stream_remote_window_size, |
235 | | (nghttp2_session * session, int32_t stream_id), |
236 | | (session, stream_id)) |
237 | | DEFINE_CHECKED_WRAPPER(nghttp2_session_get_stream_user_data, |
238 | | (nghttp2_session * session, int32_t stream_id), |
239 | | (session, stream_id)) |
240 | | DEFINE_RESULT_WRAPPER(nghttp2_session_mem_recv, |
241 | | (nghttp2_session * session, const uint8_t *input, |
242 | | size_t input_length), |
243 | | (session, input, input_length)) |
244 | | DEFINE_RESULT_WRAPPER(nghttp2_session_resume_data, |
245 | | (nghttp2_session * session, int32_t stream_id), |
246 | | (session, stream_id)) |
247 | | DEFINE_RESULT_WRAPPER(nghttp2_session_send, (nghttp2_session * session), |
248 | | (session)) |
249 | | DEFINE_RESULT_WRAPPER(nghttp2_session_set_local_window_size, |
250 | | (nghttp2_session * session, uint8_t flags, |
251 | | int32_t stream_id, int32_t window_size), |
252 | | (session, flags, stream_id, window_size)) |
253 | | DEFINE_RESULT_WRAPPER(nghttp2_session_set_stream_user_data, |
254 | | (nghttp2_session * session, int32_t stream_id, |
255 | | void *stream_user_data), |
256 | | (session, stream_id, stream_user_data)) |
257 | | DEFINE_RESULT_WRAPPER(nghttp2_session_upgrade2, |
258 | | (nghttp2_session * session, |
259 | | const uint8_t *settings_payload, |
260 | | size_t settings_payload_length, int head_request, |
261 | | void *stream_user_data), |
262 | | (session, settings_payload, settings_payload_length, |
263 | | head_request, stream_user_data)) |
264 | | DEFINE_CHECKED_WRAPPER(nghttp2_session_want_read, (nghttp2_session * session), |
265 | | (session)) |
266 | | DEFINE_CHECKED_WRAPPER(nghttp2_session_want_write, (nghttp2_session * session), |
267 | | (session)) |
268 | | DEFINE_RESULT_WRAPPER(nghttp2_submit_goaway, |
269 | | (nghttp2_session * session, uint8_t flags, |
270 | | int32_t last_stream_id, uint32_t error_code, |
271 | | const uint8_t *opaque_data, size_t opaque_data_length), |
272 | | (session, flags, last_stream_id, error_code, opaque_data, |
273 | | opaque_data_length)) |
274 | | DEFINE_RESULT_WRAPPER(nghttp2_submit_ping, |
275 | | (nghttp2_session * session, uint8_t flags, |
276 | | const uint8_t *opaque_data), |
277 | | (session, flags, opaque_data)) |
278 | | DEFINE_RESULT_WRAPPER(nghttp2_submit_priority, |
279 | | (nghttp2_session * session, uint8_t flags, |
280 | | int32_t stream_id, |
281 | | const nghttp2_priority_spec *priority_spec), |
282 | | (session, flags, stream_id, priority_spec)) |
283 | | DEFINE_RESULT_WRAPPER(nghttp2_submit_request, |
284 | | (nghttp2_session * session, |
285 | | const nghttp2_priority_spec *priority_spec, |
286 | | const nghttp2_nv *headers, size_t header_count, |
287 | | const nghttp2_data_provider *data_provider, |
288 | | void *stream_user_data), |
289 | | (session, priority_spec, headers, header_count, |
290 | | data_provider, stream_user_data)) |
291 | | DEFINE_RESULT_WRAPPER(nghttp2_submit_rst_stream, |
292 | | (nghttp2_session * session, uint8_t flags, |
293 | | int32_t stream_id, uint32_t error_code), |
294 | | (session, flags, stream_id, error_code)) |
295 | | DEFINE_RESULT_WRAPPER(nghttp2_submit_settings, |
296 | | (nghttp2_session * session, uint8_t flags, |
297 | | const nghttp2_settings_entry *settings, |
298 | | size_t settings_count), |
299 | | (session, flags, settings, settings_count)) |
300 | | DEFINE_RESULT_WRAPPER(nghttp2_submit_window_update, |
301 | | (nghttp2_session * session, uint8_t flags, |
302 | | int32_t stream_id, int32_t window_size_increment), |
303 | | (session, flags, stream_id, window_size_increment)) |
304 | | |
305 | | #undef DEFINE_RESULT_WRAPPER |
306 | | #undef DEFINE_CHECKED_WRAPPER |