/src/open62541_15/tests/fuzz/fuzz_process_request.cc
Line | Count | Source |
1 | | /* This Source Code Form is subject to the terms of the Mozilla Public |
2 | | * License, v. 2.0. If a copy of the MPL was not distributed with this |
3 | | * file, You can obtain one at http://mozilla.org/MPL/2.0/. |
4 | | */ |
5 | | |
6 | | /* |
7 | | * Harness targeting the server-side service dispatch gate: |
8 | | * processRequest() (src/server/ua_services.c) |
9 | | * -> processServiceInternal() (src/server/ua_services.c) |
10 | | * |
11 | | * processServiceInternal() is the logic that runs *after* a request is decoded |
12 | | * but *before* the concrete Service_* callback is invoked: the request |
13 | | * timestamp check, the "discovery services only on an unencrypted channel" |
14 | | * gate, the CreateSession/ActivateSession/CloseSession lifecycle dispatch, the |
15 | | * anonymous-session setup, the "session required but not activated" rejection |
16 | | * and finally the service dispatch. It is static, reachable only through its |
17 | | * caller processRequest() (exported in ua_server_internal.h). |
18 | | * |
19 | | * This harness reproduces the small amount of processMSG() decoding logic |
20 | | * (decode the request type NodeId, look up the ServiceDescription, decode the |
21 | | * request body) and then calls processRequest() directly on an already-OPEN |
22 | | * SecureChannel. Compared to fuzz_binary_message -- which drives the full |
23 | | * TCP/secure-channel state machine and therefore has to synthesise a valid |
24 | | * HEL+OPN handshake before any MSG is processed -- this removes the deep |
25 | | * handshake prefix that prevents the dispatch gate from being reached in |
26 | | * practice. |
27 | | * |
28 | | * A single fuzz input is split into a *sequence* of request payloads processed |
29 | | * on the same channel. This lets the fuzzer build stateful sequences |
30 | | * (CreateSession -> ActivateSession -> a session-required service); the |
31 | | * FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION hooks in ua_services.c stash the |
32 | | * CreateSession authentication token and replay it on the following requests. |
33 | | */ |
34 | | |
35 | | #include <open62541/plugin/log_stdout.h> |
36 | | #include <open62541/server_config_default.h> |
37 | | #include <open62541/types.h> |
38 | | |
39 | | #include "ua_server_internal.h" |
40 | | #include "ua_securechannel.h" |
41 | | #include "ua_services.h" |
42 | | #include "ua_session.h" |
43 | | #include "ua_types_encoding_binary.h" |
44 | | #include "testing_networklayers.h" |
45 | | |
46 | | /* File-local global in ua_services.c used by the fuzzing-only token replay |
47 | | * hooks. Only defined when the fuzzing build mode is active. */ |
48 | | #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION |
49 | | extern "C" UA_NodeId unsafe_fuzz_authenticationToken; |
50 | | #endif |
51 | | |
52 | | /* Maximum number of request payloads processed for a single fuzz input. Bounds |
53 | | * the per-input work so the fuzzer does not time out on pathological inputs. */ |
54 | 4.40k | #define MAX_MESSAGES 32 |
55 | | |
56 | | /* Decode the request type NodeId + request body from a payload and run it |
57 | | * through processRequest(). Mirrors processMSG() (src/server/ua_server_binary.c). */ |
58 | | static void |
59 | | processOnePayload(UA_Server *server, UA_SecureChannel *channel, |
60 | 16.2k | UA_UInt32 requestId, const UA_ByteString *msg) { |
61 | 16.2k | size_t offset = 0; |
62 | | |
63 | 16.2k | UA_DecodeBinaryOptions opt; |
64 | 16.2k | memset(&opt, 0, sizeof(opt)); |
65 | 16.2k | opt.customTypes = serverCustomTypes(server); |
66 | | |
67 | | /* Decode the request type NodeId */ |
68 | 16.2k | UA_NodeId requestTypeId; |
69 | 16.2k | UA_NodeId_init(&requestTypeId); |
70 | 16.2k | if(UA_decodeBinaryInternal(msg, &offset, &requestTypeId, |
71 | 16.2k | &UA_TYPES[UA_TYPES_NODEID], &opt) != UA_STATUSCODE_GOOD) { |
72 | 6.90k | UA_NodeId_clear(&requestTypeId); |
73 | 6.90k | return; |
74 | 6.90k | } |
75 | 9.37k | if(requestTypeId.namespaceIndex != 0 || |
76 | 8.82k | requestTypeId.identifierType != UA_NODEIDTYPE_NUMERIC) { |
77 | 605 | UA_NodeId_clear(&requestTypeId); |
78 | 605 | return; |
79 | 605 | } |
80 | | |
81 | | /* Look up the service */ |
82 | 8.77k | UA_ServiceDescription *sd = getServiceDescription(requestTypeId.identifier.numeric); |
83 | 8.77k | UA_NodeId_clear(&requestTypeId); |
84 | 8.77k | if(!sd) |
85 | 507 | return; |
86 | | |
87 | | /* Decode the request body */ |
88 | 8.26k | UA_Request request; |
89 | 8.26k | if(UA_decodeBinaryInternal(msg, &offset, &request, |
90 | 8.26k | sd->requestType, &opt) != UA_STATUSCODE_GOOD) { |
91 | 2.82k | UA_clear(&request, sd->requestType); |
92 | 2.82k | return; |
93 | 2.82k | } |
94 | | |
95 | | /* Initialise the response and dispatch through the gate */ |
96 | 5.44k | UA_Response response; |
97 | 5.44k | UA_init(&response, sd->responseType); |
98 | 5.44k | response.responseHeader.requestHandle = request.requestHeader.requestHandle; |
99 | | |
100 | 5.44k | UA_LOCK(&server->serviceMutex); |
101 | 5.44k | processRequest(server, channel, requestId, sd, &request, &response); |
102 | 5.44k | UA_UNLOCK(&server->serviceMutex); |
103 | | |
104 | 5.44k | UA_clear(&request, sd->requestType); |
105 | 5.44k | UA_clear(&response, sd->responseType); |
106 | 5.44k | } |
107 | | |
108 | | extern "C" int |
109 | 4.41k | LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { |
110 | | /* 1 control byte + at least one 2-byte length prefix */ |
111 | 4.41k | if(size < 3) |
112 | 4 | return 0; |
113 | | |
114 | | /* ---- Control byte: drive the configuration branches of the gate ---- */ |
115 | 4.40k | const uint8_t ctrl = data[0]; |
116 | 4.40k | data++; |
117 | 4.40k | size--; |
118 | | |
119 | 4.40k | UA_ServerConfig config; |
120 | 4.40k | memset(&config, 0, sizeof(UA_ServerConfig)); |
121 | 4.40k | UA_StatusCode retval = UA_ServerConfig_setDefault(&config); |
122 | 4.40k | if(retval != UA_STATUSCODE_GOOD) { |
123 | 0 | UA_ServerConfig_clean(&config); |
124 | 0 | return 0; |
125 | 0 | } |
126 | 4.40k | config.allowEmptyVariables = UA_RULEHANDLING_ACCEPT; |
127 | | /* bits 0-1: how strictly a missing request timestamp is handled */ |
128 | 4.40k | config.verifyRequestTimestamp = (UA_RuleHandling)(ctrl & 0x03); |
129 | | /* bit 2: only allow discovery services on an unencrypted (#None) channel */ |
130 | 4.40k | config.securityPolicyNoneDiscoveryOnly = (ctrl & 0x04) ? true : false; |
131 | | |
132 | 4.40k | UA_Server *server = UA_Server_newWithConfig(&config); |
133 | 4.40k | if(!server) { |
134 | 0 | UA_ServerConfig_clean(&config); |
135 | 0 | return 0; |
136 | 0 | } |
137 | | |
138 | | /* The default config always installs the #None SecurityPolicy first. */ |
139 | 4.40k | if(server->config.securityPoliciesSize == 0) { |
140 | 0 | UA_Server_delete(server); |
141 | 0 | return 0; |
142 | 0 | } |
143 | | |
144 | | /* A test ConnectionManager makes any send paths reachable from the services |
145 | | * (e.g. async/publish responses) safe no-ops instead of touching a socket. */ |
146 | 4.40k | UA_ConnectionManager *cm = TestConnectionManager_new("tcp", NULL); |
147 | 4.40k | if(!cm) { |
148 | 0 | UA_Server_delete(server); |
149 | 0 | return 0; |
150 | 0 | } |
151 | | |
152 | | /* ---- Build an already-OPEN SecureChannel bound to the #None policy ---- */ |
153 | 4.40k | UA_SecureChannel channel; |
154 | 4.40k | UA_SecureChannel_init(&channel); |
155 | 4.40k | channel.connectionManager = cm; |
156 | 4.40k | channel.connectionId = 1; |
157 | | /* Buffer sizes are normally negotiated during the HEL/ACK handshake. We set |
158 | | * sane values directly so any response encoding chunks correctly instead of |
159 | | * tripping on the default zero-initialised config. 0 == unbounded. */ |
160 | 4.40k | channel.config.protocolVersion = 0; |
161 | 4.40k | channel.config.recvBufferSize = 1 << 16; |
162 | 4.40k | channel.config.sendBufferSize = 1 << 16; |
163 | 4.40k | channel.config.localMaxMessageSize = 0; |
164 | 4.40k | channel.config.remoteMaxMessageSize = 0; |
165 | 4.40k | channel.config.localMaxChunkCount = 0; |
166 | 4.40k | channel.config.remoteMaxChunkCount = 0; |
167 | | |
168 | 4.40k | UA_ByteString noCertificate = UA_BYTESTRING_NULL; |
169 | 4.40k | retval = UA_SecureChannel_setSecurityPolicy(&channel, |
170 | 4.40k | &server->config.securityPolicies[0], |
171 | 4.40k | &noCertificate); |
172 | 4.40k | if(retval != UA_STATUSCODE_GOOD) { |
173 | 0 | cm->eventSource.free(&cm->eventSource); |
174 | 0 | UA_Server_delete(server); |
175 | 0 | return 0; |
176 | 0 | } |
177 | 4.40k | channel.state = UA_SECURECHANNELSTATE_OPEN; |
178 | 4.40k | channel.securityToken.channelId = 1; |
179 | 4.40k | channel.securityToken.tokenId = 1; |
180 | | |
181 | | /* Start each input from a clean token so message sequences are |
182 | | * deterministic and do not leak across fuzzer iterations. */ |
183 | 4.40k | #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION |
184 | 4.40k | UA_NodeId_clear(&unsafe_fuzz_authenticationToken); |
185 | 4.40k | #endif |
186 | | |
187 | | /* ---- Feed a sequence of length-prefixed request payloads ---- |
188 | | * Layout (after the control byte): repeated records of |
189 | | * [u16 big-endian length][request payload] |
190 | | * Each payload begins at the request type NodeId. Processing several |
191 | | * records on the same channel enables stateful sessions. */ |
192 | 4.40k | UA_UInt32 requestId = 1; |
193 | 4.40k | size_t pos = 0; |
194 | 4.40k | int budget = MAX_MESSAGES; |
195 | 20.6k | while(pos + 2 <= size && budget-- > 0) { |
196 | 16.2k | size_t len = ((size_t)data[pos] << 8) | (size_t)data[pos + 1]; |
197 | 16.2k | pos += 2; |
198 | 16.2k | if(len > size - pos) |
199 | 4.23k | len = size - pos; /* clamp the final, possibly truncated, record */ |
200 | | |
201 | 16.2k | UA_ByteString msg; |
202 | 16.2k | msg.length = len; |
203 | 16.2k | msg.data = (UA_Byte *)(uintptr_t)(data + pos); |
204 | 16.2k | pos += len; |
205 | | |
206 | 16.2k | if(channel.state != UA_SECURECHANNELSTATE_OPEN) |
207 | 0 | break; |
208 | | |
209 | 16.2k | processOnePayload(server, &channel, requestId++, &msg); |
210 | 16.2k | } |
211 | | |
212 | | /* ---- Teardown ---- */ |
213 | 4.40k | UA_LOCK(&server->serviceMutex); |
214 | | /* A successful CreateSession binds a Session to the channel. These must be |
215 | | * detached (which also drops outstanding Publish requests) before clearing |
216 | | * the channel -- UA_SecureChannel_clear asserts channel->sessions == NULL. |
217 | | * This mirrors the server's own removeSecureChannel() teardown. */ |
218 | 8.76k | while(channel.sessions) |
219 | 4.35k | UA_Session_detachFromSecureChannel(server, channel.sessions); |
220 | 4.40k | UA_SecureChannel_clear(&channel); |
221 | 4.40k | UA_UNLOCK(&server->serviceMutex); |
222 | | |
223 | 4.40k | #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION |
224 | 4.40k | UA_NodeId_clear(&unsafe_fuzz_authenticationToken); |
225 | 4.40k | #endif |
226 | | |
227 | 4.40k | cm->eventSource.free(&cm->eventSource); |
228 | 4.40k | UA_Server_delete(server); |
229 | 4.40k | return 0; |
230 | 4.40k | } |