Files
open62541/tests/fuzz/fuzz_process_request.cc
2026-10-14 21:01:31 +02:00

257 lines
10 KiB
C++

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Copyright 2026 (c) o6 Automation GmbH (Author: Julius Pfrommer)
*/
/*
* Harness targeting the server-side service dispatch gate:
* processRequest() (src/server/ua_services.c)
* -> processServiceInternal() (src/server/ua_services.c)
*
* processServiceInternal() is the logic that runs *after* a request is decoded
* but *before* the concrete Service_* callback is invoked: the request
* timestamp check, the "discovery services only on an unencrypted channel"
* gate, the CreateSession/ActivateSession/CloseSession lifecycle dispatch, the
* anonymous-session setup, the "session required but not activated" rejection
* and finally the service dispatch. It is static, reachable only through its
* caller processRequest() (exported in ua_server_internal.h).
*
* This harness reproduces the small amount of processMSG() decoding logic
* (decode the request type NodeId, look up the ServiceDescription, decode the
* request body) and then calls processRequest() directly on an already-OPEN
* SecureChannel. Compared to fuzz_binary_message -- which drives the full
* TCP/secure-channel state machine and therefore has to synthesise a valid
* HEL+OPN handshake before any MSG is processed -- this removes the deep
* handshake prefix that prevents the dispatch gate from being reached in
* practice.
*
* A single fuzz input is split into a *sequence* of request payloads processed
* on the same channel. This lets the fuzzer build stateful sequences
* (CreateSession -> ActivateSession -> a session-required service); the
* FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION hooks in ua_services.c stash the
* CreateSession authentication token and replay it on the following requests.
*/
#include <open62541/plugin/log_stdout.h>
#include <open62541/server_config_default.h>
#include <open62541/types.h>
#include "ua_server_internal.h"
#include "ua_securechannel.h"
#include "ua_services.h"
#include "ua_session.h"
#include "ua_types_encoding_binary.h"
#include "testing_networklayers.h"
/* File-local global in ua_services.c used by the fuzzing-only token replay
* hooks. Only defined when the fuzzing build mode is active. */
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
extern "C" UA_NodeId unsafe_fuzz_authenticationToken;
#endif
/* Maximum number of request payloads processed for a single fuzz input. Bounds
* the per-input work so the fuzzer does not time out on pathological inputs. */
#define MAX_MESSAGES 32
/* Decode the request type NodeId + request body from a payload and run it
* through the shared decoded-service gate used by the transports. */
static void
processOnePayload(UA_Server *server, UA_SecureChannel *channel,
UA_UInt32 requestId, const UA_ByteString *msg) {
size_t offset = 0;
UA_DecodeBinaryOptions opt;
memset(&opt, 0, sizeof(opt));
opt.customTypes = serverCustomTypes(server);
/* Decode the request type NodeId */
UA_NodeId requestTypeId;
UA_NodeId_init(&requestTypeId);
if(UA_decodeBinaryInternal(msg, &offset, &requestTypeId,
&UA_TYPES[UA_TYPES_NODEID], &opt) != UA_STATUSCODE_GOOD) {
UA_NodeId_clear(&requestTypeId);
return;
}
if(requestTypeId.namespaceIndex != 0 ||
requestTypeId.identifierType != UA_NODEIDTYPE_NUMERIC) {
UA_NodeId_clear(&requestTypeId);
return;
}
/* Look up the service */
UA_ServiceDescription *sd = getServiceDescription(requestTypeId.identifier.numeric);
UA_NodeId_clear(&requestTypeId);
if(!sd)
return;
/* Decode the request body */
UA_Request request;
if(UA_decodeBinaryInternal(msg, &offset, &request,
sd->requestType, &opt) != UA_STATUSCODE_GOOD) {
UA_clear(&request, sd->requestType);
return;
}
UA_Response response;
UA_LOCK(&server->serviceMutex);
processDecodedServiceRequest(server, channel, requestId, sd, &request,
&response);
UA_UNLOCK(&server->serviceMutex);
UA_clear(&request, sd->requestType);
UA_clear(&response, sd->responseType);
}
extern "C" int
LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
/* 1 control byte + at least one 2-byte length prefix */
if(size < 3)
return 0;
/* ---- Control byte: drive the configuration branches of the gate ---- */
const uint8_t ctrl = data[0];
data++;
size--;
UA_ServerConfig config;
memset(&config, 0, sizeof(UA_ServerConfig));
UA_StatusCode retval = UA_ServerConfig_setDefault(&config);
if(retval != UA_STATUSCODE_GOOD) {
UA_ServerConfig_clean(&config);
return 0;
}
config.allowEmptyVariables = UA_RULEHANDLING_ACCEPT;
/* bits 0-1: how strictly a missing request timestamp is handled */
config.verifyRequestTimestamp = (UA_RuleHandling)(ctrl & 0x03);
/* bit 2: only allow discovery services on an unencrypted (#None) channel */
config.securityPolicyNoneDiscoveryOnly = (ctrl & 0x04) ? true : false;
UA_Server *server = UA_Server_newWithConfig(&config);
if(!server) {
UA_ServerConfig_clean(&config);
return 0;
}
/* The default config always installs the #None SecurityPolicy first. */
if(server->config.securityPoliciesSize == 0) {
UA_Server_delete(server);
return 0;
}
/* A test ConnectionManager makes any send paths reachable from the services
* (e.g. async/publish responses) safe no-ops instead of touching a socket. */
UA_ConnectionManager *cm = TestConnectionManager_new("tcp", NULL);
if(!cm) {
UA_Server_delete(server);
return 0;
}
/* The harness bypasses transport startup, but delayed Session cleanup
* still needs a running EventLoop. */
UA_EventLoop *el = server->config.eventLoop;
if(el->start(el) != UA_STATUSCODE_GOOD) {
cm->eventSource.free(&cm->eventSource);
UA_Server_delete(server);
return 0;
}
/* ---- Build an already-OPEN SecureChannel bound to the #None policy ---- */
UA_SecureChannel channel;
UA_SecureChannel_init(&channel);
channel.connectionManager = cm;
channel.connectionId = 1;
/* Buffer sizes are normally negotiated during the HEL/ACK handshake. We set
* sane values directly so any response encoding chunks correctly instead of
* tripping on the default zero-initialised config. 0 == unbounded. */
channel.config.protocolVersion = 0;
channel.config.recvBufferSize = 1 << 16;
channel.config.sendBufferSize = 1 << 16;
channel.config.localMaxMessageSize = 0;
channel.config.remoteMaxMessageSize = 0;
channel.config.localMaxChunkCount = 0;
channel.config.remoteMaxChunkCount = 0;
UA_ByteString noCertificate = UA_BYTESTRING_NULL;
retval = UA_SecureChannel_setSecurityPolicy(&channel,
&server->config.securityPolicies[0],
&noCertificate);
if(retval != UA_STATUSCODE_GOOD) {
cm->eventSource.free(&cm->eventSource);
el->stop(el);
while(el->state == UA_EVENTLOOPSTATE_STOPPING)
el->run(el, 0);
UA_Server_delete(server);
return 0;
}
channel.state = UA_SECURECHANNELSTATE_OPEN;
channel.securityToken.channelId = 1;
channel.securityToken.tokenId = 1;
/* Requests are dispatched directly without starting a listener. Model an
* active server so CreateSession follows the normal lifecycle rule. */
server->state = UA_LIFECYCLESTATE_STARTED;
/* Start each input from a clean token so message sequences are
* deterministic and do not leak across fuzzer iterations. */
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
UA_NodeId_clear(&unsafe_fuzz_authenticationToken);
#endif
/* ---- Feed a sequence of length-prefixed request payloads ----
* Layout (after the control byte): repeated records of
* [u16 big-endian length][request payload]
* Each payload begins at the request type NodeId. Processing several
* records on the same channel enables stateful sessions. */
UA_UInt32 requestId = 1;
size_t pos = 0;
int budget = MAX_MESSAGES;
while(pos + 2 <= size && budget-- > 0) {
size_t len = ((size_t)data[pos] << 8) | (size_t)data[pos + 1];
pos += 2;
if(len > size - pos)
len = size - pos; /* clamp the final, possibly truncated, record */
UA_ByteString msg;
msg.length = len;
msg.data = (UA_Byte *)(uintptr_t)(data + pos);
pos += len;
if(channel.state != UA_SECURECHANNELSTATE_OPEN)
break;
processOnePayload(server, &channel, requestId++, &msg);
}
/* ---- Teardown ---- */
UA_LOCK(&server->serviceMutex);
/* A successful CreateSession binds a Session to the channel. These must be
* detached (which also drops outstanding Publish requests) before clearing
* the channel -- UA_SecureChannel_clear asserts channel->sessions == NULL.
* This mirrors the server's own removeSecureChannel() teardown. */
while(channel.sessions)
UA_Session_detachFromSecureChannel(server, channel.sessions);
UA_SecureChannel_clear(&channel);
session_list_entry *entry;
while((entry = LIST_FIRST(&server->sessions)))
UA_Session_remove(server, &entry->session, UA_SHUTDOWNREASON_CLOSE);
UA_UNLOCK(&server->serviceMutex);
/* Finish delayed Session cleanup before deleting the unstarted server. */
while(server->sessionCleanupPending > 0)
UA_Server_run_iterate(server, false);
el->stop(el);
while(el->state == UA_EVENTLOOPSTATE_STOPPING)
el->run(el, 0);
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
UA_NodeId_clear(&unsafe_fuzz_authenticationToken);
#endif
cm->eventSource.free(&cm->eventSource);
server->state = UA_LIFECYCLESTATE_STOPPED;
UA_Server_delete(server);
return 0;
}