/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 <stdint.h> |
20 | | #include <stdio.h> |
21 | | #include <stdlib.h> |
22 | | #include <string.h> |
23 | | #include <sys/socket.h> |
24 | | #include <unistd.h> |
25 | | |
26 | | #define LIBSSH_STATIC 1 |
27 | | #include <libssh/libssh.h> |
28 | | #include <libssh/scp.h> |
29 | | |
30 | | #include "nallocinc.c" |
31 | | #include "ssh_server_mock.h" |
32 | | |
33 | | static void _fuzz_finalize(void) |
34 | 4 | { |
35 | 4 | ssh_finalize(); |
36 | 4 | } |
37 | | |
38 | | int LLVMFuzzerInitialize(int *argc, char ***argv) |
39 | 40 | { |
40 | 40 | (void)argc; |
41 | 40 | nalloc_init(*argv[0]); |
42 | 40 | ssh_init(); |
43 | 40 | atexit(_fuzz_finalize); |
44 | 40 | ssh_mock_write_hostkey(SSH_MOCK_HOSTKEY_PATH); |
45 | 40 | return 0; |
46 | 40 | } |
47 | | |
48 | | /* Helper function to test one cipher/HMAC combination */ |
49 | | static int test_scp_with_cipher(const uint8_t *data, |
50 | | size_t size, |
51 | | const char *cipher, |
52 | | const char *hmac) |
53 | 17.0k | { |
54 | 17.0k | int socket_fds[2] = {-1, -1}; |
55 | 17.0k | ssh_session client_session = NULL; |
56 | 17.0k | ssh_scp scp = NULL, scp_recursive = NULL; |
57 | 17.0k | char buf[256] = {0}; |
58 | 17.0k | pthread_t srv_thread; |
59 | | |
60 | | /* Configure mock SSH server with fuzzer data */ |
61 | 17.0k | struct ssh_mock_server_config server_config = { |
62 | 17.0k | .protocol_data = data, |
63 | 17.0k | .protocol_data_size = size, |
64 | 17.0k | .exec_callback = ssh_mock_send_raw_data, |
65 | 17.0k | .subsystem_callback = NULL, |
66 | 17.0k | .callback_userdata = NULL, |
67 | 17.0k | .cipher = cipher, |
68 | 17.0k | .hmac = hmac, |
69 | 17.0k | .server_socket = -1, |
70 | 17.0k | .client_socket = -1, |
71 | 17.0k | .server_ready = false, |
72 | 17.0k | .server_error = false, |
73 | 17.0k | }; |
74 | | |
75 | 17.0k | if (socketpair(AF_UNIX, SOCK_STREAM, 0, socket_fds) != 0) { |
76 | 0 | goto cleanup; |
77 | 0 | } |
78 | | |
79 | 17.0k | server_config.server_socket = socket_fds[0]; |
80 | 17.0k | server_config.client_socket = socket_fds[1]; |
81 | | |
82 | 17.0k | if (ssh_mock_server_start(&server_config, &srv_thread) != 0) { |
83 | 1 | goto cleanup; |
84 | 1 | } |
85 | | |
86 | 17.0k | client_session = ssh_new(); |
87 | 17.0k | if (client_session == NULL) { |
88 | 4.33k | goto cleanup_thread; |
89 | 4.33k | } |
90 | | |
91 | | /* Configure client with specified cipher/HMAC */ |
92 | 12.6k | ssh_options_set(client_session, SSH_OPTIONS_FD, &socket_fds[1]); |
93 | 12.6k | ssh_options_set(client_session, SSH_OPTIONS_HOST, "localhost"); |
94 | 12.6k | ssh_options_set(client_session, SSH_OPTIONS_USER, "fuzz"); |
95 | 12.6k | ssh_options_set(client_session, SSH_OPTIONS_CIPHERS_C_S, cipher); |
96 | 12.6k | ssh_options_set(client_session, SSH_OPTIONS_CIPHERS_S_C, cipher); |
97 | 12.6k | ssh_options_set(client_session, SSH_OPTIONS_HMAC_C_S, hmac); |
98 | 12.6k | ssh_options_set(client_session, SSH_OPTIONS_HMAC_S_C, hmac); |
99 | | |
100 | | /* Set timeout for operations (1 second) */ |
101 | 12.6k | long timeout = 1; |
102 | 12.6k | ssh_options_set(client_session, SSH_OPTIONS_TIMEOUT, &timeout); |
103 | | |
104 | 12.6k | if (ssh_connect(client_session) != SSH_OK) { |
105 | 0 | goto cleanup_thread; |
106 | 0 | } |
107 | | |
108 | 12.6k | if (ssh_userauth_none(client_session, NULL) != SSH_AUTH_SUCCESS) { |
109 | 0 | goto cleanup_thread; |
110 | 0 | } |
111 | | |
112 | 12.6k | scp = ssh_scp_new(client_session, SSH_SCP_READ, "/tmp/fuzz"); |
113 | 12.6k | if (scp == NULL) { |
114 | 0 | goto cleanup_thread; |
115 | 0 | } |
116 | | |
117 | 12.6k | if (ssh_scp_init(scp) != SSH_OK) { |
118 | 12.6k | goto cleanup_thread; |
119 | 12.6k | } |
120 | | |
121 | 8 | if (size > 0) { |
122 | 8 | size_t copy_size = size < sizeof(buf) ? size : sizeof(buf); |
123 | 8 | memcpy(buf, data, copy_size); |
124 | 8 | } |
125 | | |
126 | | /* Fuzz all SCP API functions in read mode */ |
127 | 8 | ssh_scp_pull_request(scp); |
128 | 8 | ssh_scp_request_get_filename(scp); |
129 | 8 | ssh_scp_request_get_permissions(scp); |
130 | 8 | ssh_scp_request_get_size64(scp); |
131 | 8 | ssh_scp_request_get_size(scp); |
132 | 8 | ssh_scp_request_get_warning(scp); |
133 | 8 | ssh_scp_accept_request(scp); |
134 | 8 | ssh_scp_deny_request(scp, "Denied by fuzzer"); |
135 | 8 | ssh_scp_read(scp, buf, sizeof(buf)); |
136 | | |
137 | | /* Final fuzz of scp pull request after all the calls */ |
138 | 8 | ssh_scp_pull_request(scp); |
139 | | |
140 | | /* Fuzz SCP in write/upload + recursive directory mode. */ |
141 | 8 | scp_recursive = ssh_scp_new(client_session, |
142 | 8 | SSH_SCP_WRITE | SSH_SCP_RECURSIVE, |
143 | 8 | "/tmp/fuzz-recursive"); |
144 | 8 | if (scp_recursive != NULL) { |
145 | 8 | if (ssh_scp_init(scp_recursive) == SSH_OK) { |
146 | 0 | ssh_scp_push_directory(scp_recursive, "fuzz-dir", 0755); |
147 | 0 | ssh_scp_push_file(scp_recursive, "fuzz-file", sizeof(buf), 0644); |
148 | 0 | ssh_scp_write(scp_recursive, buf, sizeof(buf)); |
149 | 0 | ssh_scp_leave_directory(scp_recursive); |
150 | 0 | } |
151 | 8 | } |
152 | | |
153 | 17.0k | cleanup_thread: |
154 | 17.0k | shutdown(socket_fds[0], SHUT_RDWR); |
155 | 17.0k | shutdown(socket_fds[1], SHUT_RDWR); |
156 | 17.0k | pthread_join(srv_thread, NULL); |
157 | | |
158 | 17.0k | cleanup: |
159 | 17.0k | if (scp_recursive != NULL) { |
160 | 8 | ssh_scp_close(scp_recursive); |
161 | 8 | ssh_scp_free(scp_recursive); |
162 | 8 | } |
163 | 17.0k | if (scp) { |
164 | 12.6k | ssh_scp_close(scp); |
165 | 12.6k | ssh_scp_free(scp); |
166 | 12.6k | } |
167 | 17.0k | if (client_session) { |
168 | 12.6k | ssh_disconnect(client_session); |
169 | 12.6k | ssh_free(client_session); |
170 | 12.6k | } |
171 | 17.0k | if (socket_fds[0] >= 0) |
172 | 17.0k | close(socket_fds[0]); |
173 | 17.0k | if (socket_fds[1] >= 0) |
174 | 17.0k | close(socket_fds[1]); |
175 | | |
176 | 17.0k | return 0; |
177 | 17.0k | } |
178 | | |
179 | | int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) |
180 | 1.41k | { |
181 | 1.41k | assert(nalloc_start(data, size) > 0); |
182 | | |
183 | | /* Test all cipher/HMAC combinations exhaustively */ |
184 | 1.41k | const char *ciphers[] = { |
185 | 1.41k | "none", |
186 | 1.41k | "aes128-ctr", |
187 | 1.41k | "aes256-ctr", |
188 | 1.41k | "aes128-cbc", |
189 | 1.41k | }; |
190 | | |
191 | 1.41k | const char *hmacs[] = { |
192 | 1.41k | "none", |
193 | 1.41k | "hmac-sha1", |
194 | 1.41k | "hmac-sha2-256", |
195 | 1.41k | }; |
196 | | |
197 | 1.41k | int num_ciphers = sizeof(ciphers) / sizeof(ciphers[0]); |
198 | 1.41k | int num_hmacs = sizeof(hmacs) / sizeof(hmacs[0]); |
199 | | |
200 | 7.09k | for (int i = 0; i < num_ciphers; i++) { |
201 | 22.7k | for (int j = 0; j < num_hmacs; j++) { |
202 | 17.0k | test_scp_with_cipher(data, size, ciphers[i], hmacs[j]); |
203 | 17.0k | } |
204 | 5.67k | } |
205 | | |
206 | 1.41k | nalloc_end(); |
207 | 1.41k | return 0; |
208 | 1.41k | } |