/* 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 #include #include #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; }