/src/openssh/regress/misc/fuzz-harness/ssh_channels_fuzz.cc
Line | Count | Source |
1 | | /* Copyright 2026 Google LLC |
2 | | Licensed under the Apache License, Version 2.0 (the "License"); |
3 | | you may not use this file except in compliance with the License. |
4 | | You may obtain a copy of the License at |
5 | | http://www.apache.org/licenses/LICENSE-2.0 |
6 | | Unless required by applicable law or agreed to in writing, software |
7 | | distributed under the License is distributed on an "AS IS" BASIS, |
8 | | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
9 | | See the License for the specific language governing permissions and |
10 | | limitations under the License. |
11 | | */ |
12 | | |
13 | | /* |
14 | | * Fuzzer for OpenSSH channel dispatch (channels.c) |
15 | | * |
16 | | * Target: channels.c |
17 | | * |
18 | | * Drives the eight public channel_input_* handlers declared in |
19 | | * channels.h through ssh_dispatch. After an in-process KEX, a set of |
20 | | * scenarios is iterated for each fuzz input. Each scenario sets up a |
21 | | * fresh channel (varying type, starting state, and fd backing) with |
22 | | * dummy open_confirm and status_confirm callbacks registered, then |
23 | | * sequences the same fuzz body as nine CHANNEL_* message types in |
24 | | * realistic order so state machine transitions and callback paths |
25 | | * are exercised. |
26 | | * |
27 | | * Primary functions exercised: |
28 | | * - channel_input_data / extended_data / ieof / oclose |
29 | | * - channel_input_open_confirmation / open_failure |
30 | | * - channel_input_window_adjust / status_confirm |
31 | | * - channel state machine in nchan.c via sequenced messages |
32 | | * |
33 | | * Threat model: a peer SSH endpoint sending crafted CHANNEL_* |
34 | | * messages over an established session. Pre-auth attacker bytes |
35 | | * after KEX completes. |
36 | | * |
37 | | * Linker --wrap on ssh_packet_disconnect and sshpkt_fatal: |
38 | | * channel_input_* call those on malformed channel ids and similar |
39 | | * invariants. The real implementations free packet state before |
40 | | * cleanup_exit longjmp gets control; the wrappers longjmp directly |
41 | | * so per-iteration state survives. |
42 | | */ |
43 | | |
44 | | #include <fcntl.h> |
45 | | #include <setjmp.h> |
46 | | #include <signal.h> |
47 | | #include <stddef.h> |
48 | | #include <stdint.h> |
49 | | #include <stdlib.h> |
50 | | #include <string.h> |
51 | | #include <unistd.h> |
52 | | |
53 | | extern "C" { |
54 | | #include "includes.h" |
55 | | #include "ssherr.h" |
56 | | #include "ssh_api.h" |
57 | | #include "sshbuf.h" |
58 | | #include "packet.h" |
59 | | #include "myproposal.h" |
60 | | #include "kex.h" |
61 | | #include "xmalloc.h" |
62 | | #include "authfile.h" |
63 | | #include "sshkey.h" |
64 | | #include "log.h" |
65 | | #include "ssh2.h" |
66 | | #include "channels.h" |
67 | | } |
68 | | |
69 | | |
70 | | #include "fixed-keys.h" |
71 | | |
72 | | static jmp_buf fuzz_env; |
73 | | static volatile int in_fuzz = 0; |
74 | | |
75 | | static char *g_open_failure_msg = NULL; |
76 | | |
77 | | extern "C" int |
78 | | my_channel_input_open_failure(int type, uint32_t seq, struct ssh *ssh) |
79 | 2.09k | { |
80 | 2.09k | (void)type; |
81 | 2.09k | (void)seq; |
82 | 2.09k | uint32_t reason; |
83 | 2.09k | int r; |
84 | 2.09k | if ((r = sshpkt_get_u32(ssh, &reason)) != 0) |
85 | 45 | ssh_packet_disconnect(ssh, "Invalid open failure message"); |
86 | 2.05k | if ((r = sshpkt_get_cstring(ssh, &g_open_failure_msg, NULL)) != 0 || |
87 | 580 | (r = sshpkt_get_string_direct(ssh, NULL, NULL)) != 0 || |
88 | 180 | (r = sshpkt_get_end(ssh)) != 0) |
89 | 2.02k | ssh_packet_disconnect(ssh, "Invalid open failure message"); |
90 | 25 | return 0; |
91 | 2.05k | } |
92 | | |
93 | | extern "C" void |
94 | | cleanup_exit(int code) |
95 | 9.61k | { |
96 | 9.61k | if (in_fuzz) |
97 | 9.61k | longjmp(fuzz_env, code ? code : 1); |
98 | 0 | _exit(code); |
99 | 9.61k | } |
100 | | |
101 | | extern "C" void |
102 | | __wrap_ssh_packet_disconnect(struct ssh *ssh, const char *fmt, ...) |
103 | 13.9k | { |
104 | 13.9k | (void)ssh; |
105 | 13.9k | (void)fmt; |
106 | 13.9k | if (in_fuzz) |
107 | 13.9k | longjmp(fuzz_env, 1); |
108 | 0 | _exit(1); |
109 | 13.9k | } |
110 | | |
111 | | extern "C" int |
112 | | __wrap_sshpkt_fatal(struct ssh *ssh, int r, const char *fmt, ...) |
113 | 34 | { |
114 | 34 | (void)ssh; |
115 | 34 | (void)fmt; |
116 | 34 | if (in_fuzz) |
117 | 34 | longjmp(fuzz_env, 1); |
118 | 0 | _exit(r ? r : 1); |
119 | 34 | } |
120 | | |
121 | | static struct sshkey * |
122 | | load_private(const char *str) |
123 | 1 | { |
124 | 1 | struct sshbuf *b = sshbuf_from(str, strlen(str)); |
125 | 1 | struct sshkey *k = NULL; |
126 | 1 | if (b == NULL || sshkey_parse_private_fileblob(b, "", &k, NULL) != 0) |
127 | 0 | abort(); |
128 | 1 | sshbuf_free(b); |
129 | 1 | return k; |
130 | 1 | } |
131 | | |
132 | | static struct sshkey * |
133 | | load_public(const char *str) |
134 | 1 | { |
135 | 1 | char *tmp, *cp; |
136 | 1 | struct sshkey *k = sshkey_new(KEY_UNSPEC); |
137 | 1 | if (k == NULL) |
138 | 0 | abort(); |
139 | 1 | tmp = cp = strdup(str); |
140 | 1 | if (sshkey_read(k, &cp) != 0) |
141 | 0 | abort(); |
142 | 1 | free(tmp); |
143 | 1 | return k; |
144 | 1 | } |
145 | | |
146 | | static int |
147 | | pump(struct ssh *from, struct ssh *to, size_t *transferred) |
148 | 18.8k | { |
149 | 18.8k | const u_char *buf; |
150 | 18.8k | size_t len; |
151 | 18.8k | u_char type; |
152 | 18.8k | int r; |
153 | 18.8k | *transferred = 0; |
154 | 18.8k | if ((r = ssh_packet_next(from, &type)) != 0) |
155 | 0 | return r; |
156 | 18.8k | buf = ssh_output_ptr(from, &len); |
157 | 18.8k | if (len == 0) |
158 | 2 | return 0; |
159 | 18.8k | if ((r = ssh_input_append(to, buf, len)) != 0) |
160 | 0 | return r; |
161 | 18.8k | if ((r = ssh_output_consume(from, len)) != 0) |
162 | 0 | return r; |
163 | 18.8k | *transferred = len; |
164 | 18.8k | return 0; |
165 | 18.8k | } |
166 | | |
167 | | static int |
168 | | run_kex(struct ssh *client, struct ssh *server) |
169 | 1 | { |
170 | 1 | int r; |
171 | 1 | size_t cn, sn; |
172 | 4 | for (int i = 0; i < 100; i++) { |
173 | 4 | if ((r = pump(server, client, &sn)) != 0) return r; |
174 | 4 | if ((r = pump(client, server, &cn)) != 0) return r; |
175 | 4 | if (server->kex->done && client->kex->done) |
176 | 1 | return 0; |
177 | 3 | if (cn == 0 && sn == 0) |
178 | 0 | return SSH_ERR_PROTOCOL_ERROR; |
179 | 3 | } |
180 | 0 | return SSH_ERR_PROTOCOL_ERROR; |
181 | 1 | } |
182 | | |
183 | | static struct ssh *g_client = NULL; |
184 | | static struct ssh *g_server = NULL; |
185 | | static int g_init = 0; |
186 | | static int g_pipe[2] = { -1, -1 }; |
187 | | |
188 | | static void |
189 | | dummy_open_confirm(struct ssh *ssh, int id, int success, void *ctx) |
190 | 8 | { |
191 | 8 | (void)ssh; |
192 | 8 | (void)id; |
193 | 8 | (void)success; |
194 | 8 | (void)ctx; |
195 | 8 | } |
196 | | |
197 | | static void |
198 | | dummy_status_confirm(struct ssh *ssh, int type, Channel *c, void *ctx) |
199 | 5 | { |
200 | 5 | (void)ssh; |
201 | 5 | (void)type; |
202 | 5 | (void)c; |
203 | 5 | (void)ctx; |
204 | 5 | } |
205 | | |
206 | | static void |
207 | | dummy_status_abandon(struct ssh *ssh, Channel *c, void *ctx) |
208 | 2.08k | { |
209 | 2.08k | (void)ssh; |
210 | 2.08k | (void)c; |
211 | 2.08k | (void)ctx; |
212 | 2.08k | } |
213 | | |
214 | | static int |
215 | | init_once(void) |
216 | 423 | { |
217 | 423 | if (g_init) |
218 | 422 | return 0; |
219 | 1 | signal(SIGPIPE, SIG_IGN); |
220 | 1 | log_init("ssh_channels_fuzz", SYSLOG_LEVEL_ERROR, |
221 | 1 | SYSLOG_FACILITY_AUTH, 0); |
222 | | |
223 | 1 | int pfd[2]; |
224 | 1 | if (pipe(pfd) != 0) |
225 | 0 | return -1; |
226 | 1 | g_pipe[0] = fcntl(pfd[0], F_DUPFD_CLOEXEC, 100); |
227 | 1 | g_pipe[1] = fcntl(pfd[1], F_DUPFD_CLOEXEC, 100); |
228 | 1 | close(pfd[0]); |
229 | 1 | close(pfd[1]); |
230 | 1 | if (g_pipe[0] < 0 || g_pipe[1] < 0) |
231 | 0 | return -1; |
232 | 1 | int fl0 = fcntl(g_pipe[0], F_GETFL, 0); |
233 | 1 | int fl1 = fcntl(g_pipe[1], F_GETFL, 0); |
234 | 1 | (void)fcntl(g_pipe[0], F_SETFL, fl0 | O_NONBLOCK); |
235 | 1 | (void)fcntl(g_pipe[1], F_SETFL, fl1 | O_NONBLOCK); |
236 | | |
237 | 1 | struct sshkey *priv = load_private(PRIV_ED25519); |
238 | 1 | struct sshkey *pub = load_public(PUB_ED25519); |
239 | 1 | char *keyname = xstrdup(sshkey_ssh_name(priv)); |
240 | | |
241 | 1 | const char *defaults[PROPOSAL_MAX] = { KEX_CLIENT }; |
242 | 1 | char *proposal[PROPOSAL_MAX] = { 0 }; |
243 | 1 | int i; |
244 | 11 | for (i = 0; i < PROPOSAL_MAX; i++) { |
245 | 10 | const char *p = defaults[i]; |
246 | 10 | if (i == PROPOSAL_SERVER_HOST_KEY_ALGS) |
247 | 1 | p = keyname; |
248 | 9 | else if (i == PROPOSAL_KEX_ALGS) |
249 | 1 | p = "curve25519-sha256"; |
250 | 8 | else if (i == PROPOSAL_ENC_ALGS_CTOS || |
251 | 7 | i == PROPOSAL_ENC_ALGS_STOC) |
252 | 2 | p = "aes128-ctr"; |
253 | 6 | else if (i == PROPOSAL_MAC_ALGS_CTOS || |
254 | 5 | i == PROPOSAL_MAC_ALGS_STOC) |
255 | 2 | p = "hmac-sha2-256"; |
256 | 10 | proposal[i] = strdup(p); |
257 | 10 | } |
258 | 1 | struct kex_params kp; |
259 | 1 | memcpy(kp.proposal, proposal, sizeof(proposal)); |
260 | | |
261 | 1 | if (ssh_init(&g_client, 0, &kp) != 0 || |
262 | 1 | ssh_init(&g_server, 1, &kp) != 0) |
263 | 0 | return -1; |
264 | 1 | if (ssh_add_hostkey(g_server, priv) != 0 || |
265 | 1 | ssh_add_hostkey(g_client, pub) != 0) |
266 | 0 | return -1; |
267 | 1 | if (run_kex(g_client, g_server) != 0) |
268 | 0 | return -1; |
269 | | |
270 | 1 | channel_init_channels(g_client); |
271 | | |
272 | 1 | ssh_dispatch_init(g_client, &dispatch_protocol_ignore); |
273 | 1 | ssh_dispatch_set(g_client, SSH2_MSG_CHANNEL_DATA, |
274 | 1 | &channel_input_data); |
275 | 1 | ssh_dispatch_set(g_client, SSH2_MSG_CHANNEL_EXTENDED_DATA, |
276 | 1 | &channel_input_extended_data); |
277 | 1 | ssh_dispatch_set(g_client, SSH2_MSG_CHANNEL_EOF, |
278 | 1 | &channel_input_ieof); |
279 | 1 | ssh_dispatch_set(g_client, SSH2_MSG_CHANNEL_CLOSE, |
280 | 1 | &channel_input_oclose); |
281 | 1 | ssh_dispatch_set(g_client, SSH2_MSG_CHANNEL_OPEN_CONFIRMATION, |
282 | 1 | &channel_input_open_confirmation); |
283 | 1 | ssh_dispatch_set(g_client, SSH2_MSG_CHANNEL_OPEN_FAILURE, |
284 | 1 | &my_channel_input_open_failure); |
285 | 1 | ssh_dispatch_set(g_client, SSH2_MSG_CHANNEL_WINDOW_ADJUST, |
286 | 1 | &channel_input_window_adjust); |
287 | 1 | ssh_dispatch_set(g_client, SSH2_MSG_CHANNEL_SUCCESS, |
288 | 1 | &channel_input_status_confirm); |
289 | 1 | ssh_dispatch_set(g_client, SSH2_MSG_CHANNEL_FAILURE, |
290 | 1 | &channel_input_status_confirm); |
291 | | |
292 | 1 | free(keyname); |
293 | 11 | for (i = 0; i < PROPOSAL_MAX; i++) |
294 | 10 | free(proposal[i]); |
295 | 1 | g_init = 1; |
296 | 1 | return 0; |
297 | 1 | } |
298 | | |
299 | | struct chan_scenario { |
300 | | const char *ctype; |
301 | | int state; |
302 | | int use_pipe; |
303 | | }; |
304 | | |
305 | | static const struct chan_scenario k_scenarios[] = { |
306 | | { "session", SSH_CHANNEL_OPEN, 0 }, |
307 | | { "session", SSH_CHANNEL_OPENING, 1 }, |
308 | | { "x11", SSH_CHANNEL_OPEN, 1 }, |
309 | | { "forwarded-tcpip", SSH_CHANNEL_OPEN, 0 }, |
310 | | { "direct-tcpip", SSH_CHANNEL_OPENING, 1 }, |
311 | | }; |
312 | | |
313 | | static int g_channel_id = -1; |
314 | | |
315 | | static void |
316 | | drain_pipe(void) |
317 | 2.09k | { |
318 | 2.09k | char buf[4096]; |
319 | 2.09k | if (g_pipe[0] < 0) |
320 | 0 | return; |
321 | 2.09k | while (read(g_pipe[0], buf, sizeof(buf)) > 0) { } |
322 | 2.09k | } |
323 | | |
324 | | static void |
325 | | reset_channel(struct ssh *ssh, const struct chan_scenario *s) |
326 | 2.09k | { |
327 | 2.09k | if (g_channel_id >= 0) { |
328 | 2.09k | Channel *c = channel_lookup(ssh, g_channel_id); |
329 | 2.09k | if (c != NULL) |
330 | 2.09k | channel_free(ssh, c); |
331 | 2.09k | g_channel_id = -1; |
332 | 2.09k | } |
333 | 2.09k | drain_pipe(); |
334 | | |
335 | 2.09k | int rfd = s->use_pipe ? g_pipe[1] : -1; |
336 | 2.09k | int wfd = s->use_pipe ? g_pipe[1] : -1; |
337 | 2.09k | Channel *c = channel_new(ssh, (char *)s->ctype, s->state, |
338 | 2.09k | rfd, wfd, -1, 32768, 32768, 0, (char *)"fuzz", 0); |
339 | 2.09k | if (c == NULL) |
340 | 0 | return; |
341 | 2.09k | g_channel_id = c->self; |
342 | 2.09k | channel_register_open_confirm(ssh, g_channel_id, |
343 | 2.09k | &dummy_open_confirm, NULL); |
344 | 2.09k | channel_register_status_confirm(ssh, g_channel_id, |
345 | 2.09k | &dummy_status_confirm, &dummy_status_abandon, NULL); |
346 | 2.09k | } |
347 | | |
348 | | static const uint8_t k_msg_types[] = { |
349 | | SSH2_MSG_CHANNEL_OPEN_CONFIRMATION, |
350 | | SSH2_MSG_CHANNEL_OPEN_FAILURE, |
351 | | SSH2_MSG_CHANNEL_WINDOW_ADJUST, |
352 | | SSH2_MSG_CHANNEL_DATA, |
353 | | SSH2_MSG_CHANNEL_EXTENDED_DATA, |
354 | | SSH2_MSG_CHANNEL_SUCCESS, |
355 | | SSH2_MSG_CHANNEL_FAILURE, |
356 | | SSH2_MSG_CHANNEL_EOF, |
357 | | SSH2_MSG_CHANNEL_CLOSE, |
358 | | }; |
359 | | |
360 | | extern "C" int |
361 | | LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) |
362 | 423 | { |
363 | 423 | if (init_once() != 0) |
364 | 0 | return 0; |
365 | 423 | if (size > 65000) |
366 | 4 | return 0; |
367 | | |
368 | 419 | const size_t n_scen = sizeof(k_scenarios) / sizeof(k_scenarios[0]); |
369 | 419 | const size_t n_msg = sizeof(k_msg_types) / sizeof(k_msg_types[0]); |
370 | | |
371 | 18.4E | for (size_t s = 0; s < n_scen; s++) { |
372 | 2.09k | reset_channel(g_client, &k_scenarios[s]); |
373 | 18.8k | for (size_t t = 0; t < n_msg; t++) { |
374 | 18.8k | in_fuzz = 1; |
375 | 18.8k | if (setjmp(fuzz_env) == 0) { |
376 | 18.8k | size_t transferred; |
377 | 18.8k | if (ssh_packet_put(g_server, k_msg_types[t], |
378 | 18.8k | data, size) != 0) |
379 | 0 | goto done; |
380 | 18.8k | if (pump(g_server, g_client, |
381 | 18.8k | &transferred) != 0) |
382 | 0 | goto done; |
383 | 18.8k | (void)ssh_dispatch_run(g_client, |
384 | 18.8k | DISPATCH_NONBLOCK, NULL); |
385 | 18.8k | done: |
386 | 4.35k | ; |
387 | 4.35k | } |
388 | 4.35k | in_fuzz = 0; |
389 | 4.35k | free(g_open_failure_msg); |
390 | 4.35k | g_open_failure_msg = NULL; |
391 | 4.35k | } |
392 | 2.09k | } |
393 | 18.4E | return 0; |
394 | 419 | } |