13544 Commits

Author SHA1 Message Date
Niels Beier 444622f8f4 test(encryption): check the Basic128Rsa15 SecureChannel nonce length 2026-10-06 18:20:56 +02:00
Niels Beier c67baebcb1 fix(core): use the SecurityPolicy nonce length for the SecureChannel 2026-10-06 18:20:56 +02:00
StalderT 15af9a7618 fix(client): don't reset the outstanding Publish counter in cleanupSession
When a PublishResponse comes back with BadSessionClosed or BadSessionIdInvalid, processMSGResponse takes the request off the list and cleans up the Session before calling the response callback. cleanupSession cancels the other PublishRequests, whose callbacks decrement the counter, and then resets it to zero. The callback of the response that triggered the cleanup runs afterwards and decrements it once more, so the UInt16 wraps to 65535. From then on backgroundPublish never sends a PublishRequest, also not for the next Session, until the client disconnects.

Every PublishRequest already decrements the counter exactly once, when its callback runs for the response or for the cancellation, so the reset is not needed. Remove it.
2026-10-06 12:48:20 +02:00
StalderT ed5d86df4d fix(server): drop links to deleted MonitoredItems in SetTriggering
Links to a deleted MonitoredItem were only removed when the triggering item reported. If it never reports (Disabled mode, or a value that does not change), they stay forever, and since MonitoredItem ids are never reused a client can grow the link array of one item without bound by creating, linking and deleting items in a loop. Every addLink scans that array, so SetTriggering gets slower with each round.

Part 4, 5.13.1.6 says that the link goes away with the triggered item. SetTriggering now prunes the links of the triggering item to MonitoredItems that no longer exist before applying the request, which keeps the array bounded by the items of the Subscription.
2026-10-06 12:47:29 +02:00
StalderT 889e445178 fix(core): don't accept TokenId zero as the old SecurityToken
After the first OpenSecureChannel the server keeps a placeholder in securityToken (TokenId zero, no keys) and waits for the client to use the issued token. In the NEWTOKEN_SERVER state checkSymHeader took a message with TokenId zero as "old token still in use", so until the client's first message it was verified with keys that were never derived. In Sign mode that is an HMAC with an empty key, which anyone can compute. The client side has the same placeholder in altSecurityToken after its first OPN.

After an ISSUE there is no old token (Part 4, 5.6.2), and a TokenId that was never issued is unknown (Part 6, 6.7.2.3). TokenId zero is no longer accepted as the old token on either side, and the server skips zero when lastTokenId wraps around.
2026-10-06 09:21:43 +02:00
Niels Beier bc51e62537 test(server): check the end of the first publishing cycle 2026-10-06 09:18:05 +02:00
Niels Beier e8848c7a56 fix(server): do not batch the Subscription publish timer
Batching moved the end of the first publishing cycle up to a
quarter interval earlier.
2026-10-06 09:18:05 +02:00
Julius Pfrommer 88e72ab86f test(server): wait for async request delivery with lwIP 2026-10-06 07:37:13 +02:00
Niels Beier edc16fb6f2 test(server): cover transfer of detached Subscriptions 2026-10-06 07:37:13 +02:00
Niels Beier d7dedf2750 fix(server): allow the owner to transfer a detached Subscription
Remember the identity of the Session on detach. Only a Session
with the same identity can transfer the Subscription.
2026-10-06 07:37:13 +02:00
Marvin Gnad ed84b82963 fix(pubsub): release the server lock when enableAll cannot start the manager
UA_Server_enableAllPubSubComponents() returned the error of
UA_PubSubManager_start() without releasing the server lock. Release it,
like the psm == NULL path above already does, and add a regression test.

Signed-off-by: Marvin Gnad <marvin.gnad@gmail.com>
2026-10-06 00:10:05 +02:00
StalderT 783b0bd287 fix(client): look up the Subscription again after synchronous MonitoredItem calls
The synchronous CreateMonitoredItems and ModifyMonitoredItems keep the Subscription and MonitoredItem pointers across the service call. If the Session is closed meanwhile (BadSessionClosed on a queued Publish, or BadSessionIdInvalid on the request itself), the client removes its Subscriptions before the call returns and these pointers are used after free. For CreateMonitoredItems the MonitoredItem is freed twice and its delete callback runs twice. Look them up again by id and client handle after the call, as the async handlers already do.
2026-10-05 23:36:06 +02:00
StalderT b2eae285f9 fix(server): limit the number of links in SetTriggering
CreateMonitoredItems, ModifyMonitoredItems, SetMonitoringMode and DeleteMonitoredItems all check maxMonitoredItemsPerCall, but SetTriggering accepted any number of links. Apply the same limit there, counting the links to add and the links to remove together, and return Bad_TooManyOperations above it (Part 4, 5.13.5).

Also free removeResults with its own size in the out-of-memory path, it was using linksToAddSize.
2026-10-05 23:35:14 +02:00
Niels Beier 8f7d39b798 test(server): cover Write status codes of non-writable attributes 2026-10-05 23:33:13 +02:00
Niels Beier 247e0e058d fix(server): return BadNotWritable and BadAttributeIdInvalid on Write
Bad_NotWritable if the WriteMask/AccessLevel forbids the write,
Bad_AttributeIdInvalid if the NodeClass lacks the attribute.
2026-10-05 23:33:13 +02:00
Niels Beier 32c514b4c7 test(server): check the StatusChange after a transfer without Publish 2026-10-05 23:30:06 +02:00
Niels Beier 0e3bc3b9bb fix(server): send the transfer StatusChange with the next Publish
Without a queued PublishRequest, another late Subscription could
answer the next Publish of the old Session first.
2026-10-05 23:30:06 +02:00
o6-jpfr 05c2b389b6 refactor(build): Bump version to v1.5.9 v1.5.9 2026-10-04 21:21:20 +02:00
o6-jpfr 19f5a2f153 Merge pull request #8522 from o6-automation/merge_15_master
Merge 1.5 into master
2026-10-04 20:07:08 +02:00
Julius Pfrommer 7cc1abc1ce test: Use atomic stores in browse access-control regression
Match the server loop atomic reads when starting and stopping the test server. This fixes the ThreadSanitizer race introduced by the 1.5 merge.

Validation: reproduced the race locally before the fix; check_accesscontrol passes with ThreadSanitizer after the fix.
2026-10-04 18:45:43 +02:00
Julius Pfrommer 6901aedc07 Merge remote-tracking branch 'origin/1.5' into merge_15_master
Port keyless certificate validation into the GDS receiver driver instead
of restoring the removed server implementation. Preserve master's timer
batching, PubSub lifecycle and client HTTP configuration handling.

Adapt Publish cancellation to response tokens and the HEL/fuzz harnesses
to the explicit TCP driver. Keep SKS policy history and custom datatype
ownership intact, including failed replacement configuration cleanup.
Retain TPM error cleanup while enforcing key-handle and nonce lengths.

Validation:
- Full mbedTLS unit-test build with full namespace, GDS, SKS and Query.
- 21 affected suites passed with ASan/UBSan, plus 9 isolated SKS ownership
  and lifecycle checks.
- OpenSSL certificate update, GDS information model and client encryption
  suites passed with sanitizers.
- OPN, MSG and binary-message fuzzers each completed 100 sanitizer runs.
- Both TPM PubSub policy sources passed compilation checks.

The full SKS client suite timed out after multicast UDP sends reported
"No route to host" on this machine; its affected ownership and lifecycle
cases passed separately. No live TPM hardware validation was performed.
2026-10-04 16:49:17 +02:00
Julius Pfrommer f13a4eb686 Merge pull request #8521 from o6-automation/merge_14_15
Merge 1.4 to 1.5
2026-10-04 16:32:38 +02:00
Julius Pfrommer 2a70e31f17 Merge remote-tracking branch 'origin/1.4' into merge_14_15
Adapt the incoming fixes and regressions to the 1.5 encoding options,
nodestore, connection-manager, PubSub and session APIs. Preserve the
strict HEL EndpointUrl limit of less than 4096 bytes.

Cover cancellation of interleaved queued Publish requests, including
response handles, cancellation counts and a successful CancelResponse.
Extend the nodestore mask regression to 1.5's derived attributes and
invalid attribute identifiers.

Validation: full OpenSSL test build; 252 checks passed across the builtin
types, server, attribute services, client subscriptions and mbedTLS PubSub
decryption suites, with sanitizers enabled.
2026-10-04 15:02:49 +02:00
Julius Pfrommer 1cb019544a fix(types): normalize reserved Variant ids to ByteString
Decode wire type ids 26 through 31 with the existing ByteString handling for scalars and arrays. Preserve payload and dimensions while discarding the reserved tag; subsequent encoding uses ordinary ByteString id 15. No public API changes.

Add regression coverage in the existing datatype harness for all reserved ids, null and empty values, arrays and matrices, normalized re-encoding, truncated input, and rejection of ids 32 through 63.
2026-10-04 13:06:33 +02:00
Julius Pfrommer dcc3d80b81 fix(server): reject trailing data in HEL messages
Require decoding the HEL body to consume the entire message. Add handshake regressions for one-byte and eight-byte suffixes, checking BadDecodingError and connection closure.
2026-10-04 13:06:33 +02:00
Julius Pfrommer e74c32ef29 fix(server): enforce the HEL EndpointUrl length limit
Reject EndpointUrls longer than 4096 bytes with BadTcpEndpointUrlInvalid and propagate allocation errors when copying the accepted URL. Add handshake tests for the boundary, oversized URLs, connection closure, and negotiation of a higher requested protocol version down to zero.
2026-10-04 13:06:33 +02:00
Julius Pfrommer 304fb74b09 fix(types): reject array dimensions on scalar Variants
Reject the dimensions flag when the array flag is absent, including inputs without trailing bytes. Add malformed-input regression tests and a valid scalar control.
2026-10-04 13:06:33 +02:00
Julius Pfrommer 3fe49b7e63 fix(types): keep null binary encoding identifiers opaque
Do not resolve the null encoding identifier to Boolean or to a custom datatype. Preserve opaque ExtensionObject bodies in scalar and array Variants, while allowing numeric identifier zero in other namespaces. Add regression tests for these cases and ordinary null ExtensionObjects.
2026-10-04 13:06:33 +02:00
Julius Pfrommer 461c1ff7d6 fix(server): Use the AuditEntryId supplied by the client in the RequestHeader 2026-10-04 10:57:33 +02:00
Julius Pfrommer 038a2cf55d refactor(server): Keep a server-wide pointer to the RequestHeader of the current request 2026-10-04 10:57:33 +02:00
Christoffer Lind acad45bdfb fix(server): pass the mask of the read attribute to the nodestore
attr2mask is indexed by the AttributeId, but the table starts at index
0 while AttributeIds start at 1. Each Read therefore asks the
Nodestore for the attribute that follows the requested one, for
example DataType instead of Value.
2026-10-02 20:19:56 +02:00
StalderT e786522fd5 fix(pubsub): confine the PromotedFields and follow the declared DataSetMessage offsets
The UADP decoder read the PromotedFields length and the Sizes array of the PayloadHeader but held the decoding to neither. A PromotedField Variant could run past the declared length into the security header or the payload, and a DataSetMessage started wherever the previous one happened to stop rather than at the offset the sizes imply, so a subscriber could attribute fields to the wrong DataSetWriterId and accept a DataSetMessage that the declared layout does not contain. Every read stayed inside the receive buffer, so this is a conformance defect and not a memory safety one.

The PromotedFields now decode against an end position set to their declared length, and a DataSetMessage whose size came from the PayloadHeader is followed at the declared offset. The size inferred for a single DataSetMessage is deliberately left out of this: it is an upper bound that still holds the SecurityFooter, so moving to it would swallow the footer.

The loop over the DataSetMessages now stops at the first failure instead of accumulating statuses with |=, which merges two error codes into a third one that nothing ever returned.

Part 14 v1.05, 7.2.4.4.2 and 7.2.4.5.3. Refs #8509.
2026-10-02 20:17:56 +02:00
Andreas Ebner f8a962278d fix(types): UA_Variant_setArray with length zero sets an empty array
A variant with length zero and a data pointer is a scalar. With an
array of length zero, UA_Variant_setArray kept the pointer, so the
variant became a scalar. Writing it to an array variable failed, and
clearing it freed the array as a single element.
UA_Variant_setArrayCopy already uses UA_EMPTY_ARRAY_SENTINEL in this
case. UA_Variant_setArray now does the same and does not take over the
array. NULL with length zero remains a null array.

The GDS Receiver writes its CertificateTypes with UA_Variant_setArray.
Since #8506 the list is empty on a server without an RSA endpoint. The
write failed, the receiver did not start and its methods were unbound.
2026-10-02 20:16:50 +02:00
Andreas Ebner fc0bf5614e fix(drivers): let the owning session continue its transaction with Open
Open was refused while any transaction was pending, also for the session
that owns it, so UpdateCertificate followed by a TrustList write failed.
Part 12 §7.8.2.2 says a write-mode Open continues an existing transaction.
Allow that for the owner unless ApplyChanges is queued; OpenWithMasks and
other sessions still get BadTransactionPending. Close discards only the
written data and keeps the staged changes. ApplyChanges refuses to run
while any TrustList is open for writing (§7.10.9).
2026-10-01 16:09:59 +02:00
Andreas Ebner af94d97375 fix(drivers): advertise and accept only configured certificate types
CertificateTypes always listed RsaMin and RsaSha256. With no RsaMin policy
configured (the default), an RsaMin UpdateCertificate and ApplyChanges
answered Good without changing anything, and CreateSigningRequest returned
an empty CSR. Now only types with an endpoint policy in the
DefaultApplicationGroup are advertised. UpdateCertificate and
CreateSigningRequest refuse the others with BadInvalidArgument (Part 12
7.10.5, 7.10.10), and applying a type that no policy uses is an error.
2026-10-01 16:09:59 +02:00
Andreas Ebner 7395a9d1dd fix(drivers): read the SetPosition offset as UInt64
SetPosition read its UInt64 Position argument through a UInt32 pointer.
Offsets above 4 GiB were truncated, and big-endian targets read the wrong
half. Read the argument with its declared type.
2026-10-01 16:09:59 +02:00
Andreas Ebner e1a2e07cda fix(drivers): wipe staged private keys before freeing them
Private keys staged by UpdateCertificate were freed without zeroing, when
they were replaced, on allocation failures and when the transaction ended.
Zero them with UA_ByteString_memZero first, as the CSR path already does.
2026-10-01 16:09:59 +02:00
Andreas Ebner 128eb7868a fix(drivers): clear the transaction when RemoveCertificate fails
RemoveCertificate creates a transaction and applies it right away. When
the TrustList could not be copied into the transaction, it returned with
the transaction still pending and no session watchdog installed, so all
later TrustList changes were refused. Clear the transaction on that path.
2026-10-01 16:09:59 +02:00
Andreas Ebner b822aae1ff fix(drivers): refuse certificate updates while ApplyChanges is queued
ApplyChanges defers the completion of the transaction to a delayed
callback. An UpdateCertificate in between was accepted into the queued
transaction and then silently discarded with it. Refuse it with
BadTransactionPending until the transaction is completed.
2026-10-01 16:09:59 +02:00
Andreas Ebner 10fb09e051 fix(drivers): answer BadNothingToDo for ApplyChanges without a transaction
ApplyChanges compared the caller with the owner of the transaction before
checking its state. A fresh transaction has no owner, so every caller got
BadUserAccessDenied. Check the state first, as Part 12 §7.10.9 requires.
2026-10-01 16:09:59 +02:00
StalderT 228355a4fa fix(pubsub): clear the PromotedField that failed to decode
The error path of the PromotedFields loop deletes counter entries, but the Variant that just failed sits at index counter and keeps the members it had already allocated. Decoding the eight byte NetworkMessage 81 80 02 01 00 06 2a 00, where the Int32 Variant runs past the end of the receive buffer, leaks eight bytes, so a subscriber leaks memory for every malformed message carrying PromotedFields. The line dates from 6d923ff37. The array comes from a zeroing allocation and counter is always below the allocated space at that point, so clearing one more entry is safe even when the decoder wrote nothing into it.

The test asserts nothing about the allocation and the check suite stays green either way, with 42 checks passing before and after. It is the Valgrind and AddressSanitizer stages that turn red without the fix.

Found while working on #8514, which made this error path reachable from the byte sequences reported in #8509.
2026-10-01 15:27:03 +02:00
Andreas Ebner 461e46182c fix(pubsub): clean up failed writer creation and deferred deletion
Roll back writers whose information model representation fails. A deferred
connection or WriterGroup delete asks the lifecycle callback only once,
retries vetoed writers, rejects new ones and completes a manager stop.
2026-09-30 23:55:58 +02:00
Andreas Ebner 2810e1ec1b fix(pubsub): never free a PublishedDataSet with connected writers
The removal freed the PDS even when removing a connected writer failed and left
a dangling connectedDataSet. Check the writer groups first and stop on a failed
writer removal, so the PDS is only freed without writers.
2026-09-30 23:55:58 +02:00
Andreas Ebner 2ecedb825c fix(drivers): update the None endpoint certificate in UpdateCertificate
After a certificate push the SecurityPolicy#None endpoint kept presenting
the replaced certificate, while the encrypted endpoints showed the new one.
A None SecurityPolicy with a replaced certificate is now updated as well.
2026-09-29 23:11:55 +02:00
Andreas Ebner a34a68357e fix(drivers): accept a null CertificateGroupId in UpdateCertificate
Part 12 says a null CertificateGroupId selects the DefaultApplicationGroup.
The GDS receiver rejected it with Bad_NotSupported, so GDS pushes failed.
An unsupported group or type now returns Bad_InvalidArgument as specified.
2026-09-29 20:31:27 +02:00
StalderT ecbdf7c263 fix(server): reject a HEL whose EndpointUrl is 4096 bytes or longer
OPC UA Part 6, 7.1.2.3 requires the EndpointUrl of a Hello message to be less than 4096 bytes, and the server to answer Bad_TcpEndpointUrlInvalid and close the connection otherwise. processHEL copied the URL into the channel without any check, so any length up to the receive buffer got an ACK and stayed in the channel until it closed. The result of UA_String_copy was ignored as well.

Return BadTcpEndpointUrlInvalid for 4096 bytes or more, which the existing error path in processSecureChannelMessage turns into an ERR and a closed connection, and check the copy.

Refs #8499
2026-09-29 09:58:19 +02:00
Andreas Ebner 918f2cfd4b fix(docs): typos in comments, docs and messages
Fix misspellings found with codespell across headers, sources, examples,
tests and the documentation, including "propably" in the nodeset
compiler docs. Identifiers, companion spec names and generated code are
left untouched.
2026-09-28 16:20:44 +02:00
ChiouYouJia 5ae7657e58 feat(server): add SourceNode to the write audit notification payload
The AUDIT_UPDATE_WRITE application notification carries the AttributeId,
IndexRange, NewValue and OldValue of a write, but not the NodeId of the written
node. That NodeId is only set as the SourceNode of the generated
AuditWriteUpdateEvent, so an auditNotificationCallback cannot tell which node
was written without additionally subscribing to the events.

Add /SourceNode (the BaseEventType field name) to the payload. The generated
events are unchanged: the event-field lookup already resolves /SourceNode to
the same NodeId by default, and the RBAC check uses the EventDescription.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:41:58 +02:00
StalderT e23ad2f4ee feat(plugins): support OpenSSL 4
OpenSSL 4 makes ASN1_STRING opaque, returns const pointers from the X509_NAME, X509_CRL and X509_EXTENSION getters, and deprecates the X509_cmp_*time family and X509_NAME_get_text_by_NID. The plugin no longer built: the ASN1_IA5STRING access is a hard error, and -Werror turns the rest into errors too.

Read ASN1 strings through ASN1_STRING_length and ASN1_STRING_get0_data, keep the getter results const, compare the validity period with ASN1_TIME_cmp_time_t and read the common name through its name entry. The self-signed certificate gets its subject from a fresh X509_NAME instead of editing the one X509_get_subject_name returns, and the CSR duplicates the subjectAltName extension instead of pushing one it does not own. All of this exists since OpenSSL 1.1.1, which the plugin already requires.

Add a CI job that builds OpenSSL 4.0.2 from source like the LibreSSL job does. The runner ships OpenSSL 3, so the job sets OPENSSL_ROOT_DIR to make sure it cannot quietly fall back to the system library.

Refs #8493
2026-09-27 17:40:24 +02:00
Andreas Ebner 44b527efb7 fix(plugins): bound the key handle and nonce copies of the PKCS#11 PubSub policies
The TPM AES-CTR policies copied sizeof(pointer) bytes regardless of the key length,
and setKeys read 8 bytes from the 4-byte key nonce, past the end of the SKS key
buffer. Check the lengths and copy the size of the destination.
2026-09-26 19:17:01 +02:00