Coverage Report

Created: 2026-10-17 07:08

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libssh/tests/fuzz/ssh_scp_fuzzer.c
Line
Count
Source
1
/*
2
 * Copyright 2026 libssh authors
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *      http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
17
#include <assert.h>
18
#include <pthread.h>
19
#include <stdbool.h>
20
#include <stdint.h>
21
#include <stdio.h>
22
#include <stdlib.h>
23
#include <string.h>
24
#include <sys/socket.h>
25
#include <sys/time.h>
26
#include <unistd.h>
27
28
#define LIBSSH_STATIC 1
29
#include <libssh/libssh.h>
30
#include <libssh/scp.h>
31
32
#include "nallocinc.c"
33
#include "ssh_server_mock.h"
34
35
static void _fuzz_finalize(void)
36
3
{
37
3
    ssh_finalize();
38
3
}
39
40
int LLVMFuzzerInitialize(int *argc, char ***argv)
41
43
{
42
43
    (void)argc;
43
43
    nalloc_init(*argv[0]);
44
43
    ssh_init();
45
43
    atexit(_fuzz_finalize);
46
43
    ssh_mock_write_hostkey(SSH_MOCK_HOSTKEY_PATH);
47
43
    return 0;
48
43
}
49
50
static const char *const k_ciphers[] = {
51
    "none",
52
    "aes128-ctr",
53
    "aes256-ctr",
54
    "aes128-cbc",
55
};
56
57
static const char *const k_hmacs[] = {
58
    "none",
59
    "hmac-sha1",
60
    "hmac-sha2-256",
61
};
62
63
/*
64
 * Wrap fuzzer bytes as a valid SCP server record stream so the client
65
 * survives ssh_scp_init's initial-ACK gate and reaches the deeper SCP
66
 * code paths (record parsing, accept/deny, transfer flow, recursive
67
 * push). Without this wrapping the fuzzer almost never satisfies the
68
 * "\x00C<mode> <size> <name>\n" prefix the SCP layer expects, and
69
 * coverage stalls in the early-protocol record-parser rejection paths.
70
 *
71
 * The 4 envelope bytes consumed here are still fuzzer-controlled, so
72
 * libFuzzer mutations explore C/D/T/E variants, mismatched sizes, and
73
 * unusual modes from inside the wrap:
74
 *
75
 *   data[0..1]  SCP mode (12 bits)
76
 *   data[2]     bit 0-1: variant select (0=C, 1=D, 2=T, 3=E)
77
 *               bit 2:   optional trailing server-ACK after payload
78
 *   data[3]     declared transfer size in the C-record header
79
 *   data[4..]   raw payload bytes appended after the SCP header
80
 *
81
 * Coverage of invalid SCP record parsing is NOT given up by this
82
 * shaping: ssh_server_fuzzer and ssh_client_fuzzer pump unstructured
83
 * bytes through the SSH transport and reach the SCP record parser's
84
 * rejection paths from that direction.
85
 */
86
static size_t
87
scp_wrap(const uint8_t *data, size_t size, uint8_t *out, size_t out_cap)
88
240
{
89
240
    uint16_t mode;
90
240
    uint8_t variant;
91
240
    uint8_t declared_size;
92
240
    size_t payload_sz;
93
240
    size_t cap_left;
94
240
    size_t total;
95
240
    int n;
96
97
240
    if (size < 4 || out_cap == 0) {
98
0
        return 0;
99
0
    }
100
240
    mode = ((uint16_t)data[0] << 8 | data[1]) & 07777;
101
240
    variant = data[2] & 0x03;
102
240
    declared_size = data[3];
103
104
240
    switch (variant) {
105
175
    case 0:
106
175
        n = snprintf((char *)out,
107
175
                     out_cap,
108
175
                     "%cC%04o %u f\n",
109
175
                     0,
110
175
                     mode,
111
175
                     declared_size);
112
175
        break;
113
21
    case 1:
114
21
        n = snprintf((char *)out, out_cap, "%cD%04o 0 d\n", 0, mode);
115
21
        break;
116
17
    case 2:
117
17
        n = snprintf((char *)out, out_cap, "%cT0 0 0 0\n", 0);
118
17
        break;
119
27
    default:
120
27
        n = snprintf((char *)out, out_cap, "%cE\n", 0);
121
27
        break;
122
240
    }
123
240
    if (n < 0 || (size_t)n >= out_cap) {
124
0
        return 0;
125
0
    }
126
127
240
    payload_sz = size - 4;
128
240
    cap_left = out_cap - (size_t)n;
129
240
    if (payload_sz > cap_left) {
130
52
        payload_sz = cap_left;
131
52
    }
132
240
    memcpy(out + n, data + 4, payload_sz);
133
240
    total = (size_t)n + payload_sz;
134
    /* Optional server final ACK; bit chosen by fuzzer to cover both paths */
135
240
    if (variant == 0 && (data[2] & 0x04) && total < out_cap) {
136
135
        out[total++] = '\x00';
137
135
    }
138
240
    return total;
139
240
}
140
141
/* Run one SCP fuzzing iteration against the mock server */
142
static int test_scp_with_cipher(const uint8_t *data,
143
                                size_t size,
144
                                const char *cipher,
145
                                const char *hmac)
146
240
{
147
240
    bool thread_started = false;
148
240
    int socket_fds[2] = {-1, -1};
149
240
    ssh_session client_session = NULL;
150
240
    ssh_scp scp = NULL, scp_recursive = NULL;
151
240
    char buf[256] = {0};
152
240
    pthread_t srv_thread;
153
240
    int rc;
154
240
    long timeout = 1;
155
240
    bool no = false;
156
240
    struct timeval tv = {.tv_sec = 2, .tv_usec = 0};
157
158
    /* Configure mock SSH server with fuzzer data */
159
240
    struct ssh_mock_server_config server_config = {
160
240
        .protocol_data = data,
161
240
        .protocol_data_size = size,
162
240
        .exec_callback = ssh_mock_send_raw_data,
163
240
        .subsystem_callback = NULL,
164
240
        .callback_userdata = NULL,
165
240
        .cipher = cipher,
166
240
        .hmac = hmac,
167
240
        .server_socket = -1,
168
240
        .client_socket = -1,
169
240
        .server_ready = false,
170
240
        .server_error = false,
171
240
        .shutdown_requested = false,
172
240
    };
173
174
240
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, socket_fds) != 0) {
175
0
        goto cleanup;
176
0
    }
177
178
    /* Set socket timeouts to prevent indefinite blocking */
179
240
    setsockopt(socket_fds[0], SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
180
240
    setsockopt(socket_fds[0], SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
181
240
    setsockopt(socket_fds[1], SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
182
240
    setsockopt(socket_fds[1], SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
183
184
240
    server_config.server_socket = socket_fds[0];
185
240
    server_config.client_socket = socket_fds[1];
186
187
240
    if (ssh_mock_server_start(&server_config, &srv_thread) != 0) {
188
0
        goto cleanup;
189
0
    }
190
240
    thread_started = true;
191
192
240
    client_session = ssh_new();
193
240
    if (client_session == NULL) {
194
0
        goto cleanup;
195
0
    }
196
197
    /* Configure client; bail on the first failing option-set so we don't
198
     * run the rest of the iteration in a partially-configured state.
199
     * SSH_OPTIONS_PROCESS_CONFIG is set to false so the fuzzer doesn't read
200
     * ~/.ssh/config or /etc/ssh/ssh_config — keeps results deterministic
201
     * across environments. SSH_OPTIONS_TIMEOUT is 1 second. */
202
240
    rc = ssh_options_set(client_session, SSH_OPTIONS_FD, &socket_fds[1]);
203
240
    if (rc != SSH_OK) {
204
0
        goto cleanup;
205
0
    }
206
240
    rc = ssh_options_set(client_session, SSH_OPTIONS_HOST, "localhost");
207
240
    if (rc != SSH_OK) {
208
0
        goto cleanup;
209
0
    }
210
240
    rc = ssh_options_set(client_session, SSH_OPTIONS_USER, "fuzz");
211
240
    if (rc != SSH_OK) {
212
0
        goto cleanup;
213
0
    }
214
240
    rc = ssh_options_set(client_session, SSH_OPTIONS_CIPHERS_C_S, cipher);
215
240
    if (rc != SSH_OK) {
216
0
        goto cleanup;
217
0
    }
218
240
    rc = ssh_options_set(client_session, SSH_OPTIONS_CIPHERS_S_C, cipher);
219
240
    if (rc != SSH_OK) {
220
0
        goto cleanup;
221
0
    }
222
240
    rc = ssh_options_set(client_session, SSH_OPTIONS_HMAC_C_S, hmac);
223
240
    if (rc != SSH_OK) {
224
0
        goto cleanup;
225
0
    }
226
240
    rc = ssh_options_set(client_session, SSH_OPTIONS_HMAC_S_C, hmac);
227
240
    if (rc != SSH_OK) {
228
0
        goto cleanup;
229
0
    }
230
240
    rc = ssh_options_set(client_session, SSH_OPTIONS_PROCESS_CONFIG, &no);
231
240
    if (rc != SSH_OK) {
232
0
        goto cleanup;
233
0
    }
234
240
    rc = ssh_options_set(client_session, SSH_OPTIONS_TIMEOUT, &timeout);
235
240
    if (rc != SSH_OK) {
236
0
        goto cleanup;
237
0
    }
238
239
240
    if (ssh_connect(client_session) != SSH_OK) {
240
0
        goto cleanup;
241
0
    }
242
243
240
    if (ssh_userauth_none(client_session, NULL) != SSH_AUTH_SUCCESS) {
244
0
        goto cleanup;
245
0
    }
246
247
240
    scp = ssh_scp_new(client_session, SSH_SCP_READ, "/tmp/fuzz");
248
240
    if (scp == NULL) {
249
0
        goto cleanup;
250
0
    }
251
252
240
    if (ssh_scp_init(scp) != SSH_OK) {
253
239
        goto cleanup;
254
239
    }
255
256
1
    if (size > 0) {
257
1
        size_t copy_size = size < sizeof(buf) ? size : sizeof(buf);
258
1
        memcpy(buf, data, copy_size);
259
1
    }
260
261
    /* Fuzz all SCP API functions in read mode */
262
1
    ssh_scp_pull_request(scp);
263
1
    ssh_scp_request_get_filename(scp);
264
1
    ssh_scp_request_get_permissions(scp);
265
1
    ssh_scp_request_get_size64(scp);
266
1
    ssh_scp_request_get_size(scp);
267
1
    ssh_scp_request_get_warning(scp);
268
1
    ssh_scp_accept_request(scp);
269
1
    ssh_scp_deny_request(scp, "Denied by fuzzer");
270
1
    ssh_scp_read(scp, buf, sizeof(buf));
271
272
    /* Final fuzz of scp pull request after all the calls */
273
1
    ssh_scp_pull_request(scp);
274
275
    /* Fuzz SCP in write/upload + recursive directory mode. */
276
1
    scp_recursive = ssh_scp_new(client_session,
277
1
                                SSH_SCP_WRITE | SSH_SCP_RECURSIVE,
278
1
                                "/tmp/fuzz-recursive");
279
1
    if (scp_recursive != NULL) {
280
1
        if (ssh_scp_init(scp_recursive) == SSH_OK) {
281
0
            ssh_scp_push_directory(scp_recursive, "fuzz-dir", 0755);
282
0
            ssh_scp_push_file(scp_recursive, "fuzz-file", sizeof(buf), 0644);
283
0
            ssh_scp_write(scp_recursive, buf, sizeof(buf));
284
0
            ssh_scp_leave_directory(scp_recursive);
285
0
        }
286
1
    }
287
288
240
cleanup:
289
    /* Signal server thread to exit */
290
240
    server_config.shutdown_requested = true;
291
292
    /* Close sockets */
293
240
    if (socket_fds[0] >= 0)
294
240
        close(socket_fds[0]);
295
240
    if (socket_fds[1] >= 0)
296
240
        close(socket_fds[1]);
297
298
    /* Cleanup client objects */
299
240
    if (scp_recursive != NULL) {
300
1
        ssh_scp_close(scp_recursive);
301
1
        ssh_scp_free(scp_recursive);
302
1
    }
303
240
    if (scp) {
304
240
        ssh_scp_close(scp);
305
240
        ssh_scp_free(scp);
306
240
    }
307
240
    if (client_session) {
308
240
        ssh_disconnect(client_session);
309
240
        ssh_free(client_session);
310
240
    }
311
312
    /* Server thread exits via shutdown_requested + 2s socket timeout */
313
240
    if (thread_started) {
314
240
        pthread_join(srv_thread, NULL);
315
240
    }
316
317
240
    return 0;
318
1
}
319
320
/*
321
 * Fuzzer input layout (not passed directly to the SSH transport; it is
322
 * decoded here, then fed through scp_wrap):
323
 *
324
 *   data[0]   cipher index, taken modulo length of k_ciphers
325
 *   data[1]   HMAC index,   taken modulo length of k_hmacs
326
 *   data[2..] handed to scp_wrap, which uses the first 4 bytes as the
327
 *             SCP envelope (mode, variant, declared size, optional ACK
328
 *             toggle) and the rest as the file-content payload.
329
 *
330
 * Inputs shorter than 6 bytes are rejected so every iteration has at
331
 * least the two selector bytes plus one full envelope for scp_wrap.
332
 */
333
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
334
245
{
335
245
    uint8_t wrapped[4096];
336
245
    size_t wrapped_size = 0;
337
245
    const char *cipher = NULL;
338
245
    const char *hmac = NULL;
339
340
245
    if (size < 6) {
341
5
        return 0;
342
5
    }
343
344
245
    assert(nalloc_start(data, size) > 0);
345
346
240
    cipher = k_ciphers[data[0] % (sizeof(k_ciphers) / sizeof(k_ciphers[0]))];
347
240
    hmac = k_hmacs[data[1] % (sizeof(k_hmacs) / sizeof(k_hmacs[0]))];
348
349
240
    wrapped_size = scp_wrap(data + 2, size - 2, wrapped, sizeof(wrapped));
350
240
    if (wrapped_size > 0) {
351
240
        test_scp_with_cipher(wrapped, wrapped_size, cipher, hmac);
352
240
    }
353
354
240
    nalloc_end();
355
240
    return 0;
356
240
}