Coverage Report

Created: 2026-10-23 07:11

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}