Skip to content

Fix Core/Types audit findings and conform them to the OPC UA spec - #4573

Merged
marcschier merged 220 commits into
masterfrom
romanett/opc-ua-core-bugs-0d1069
Sep 29, 2026
Merged

marcschier merged 220 commits into
masterfrom
romanett/opc-ua-core-bugs-0d1069

Conversation

@romanett

@romanett romanett commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Originally stacked on #4569; now targets master.

Summary

Bug audit of Opc.Ua.Core, Opc.Ua.Core.Types, Opc.Ua.Types and Opc.Ua.Core.Schema, with the findings fixed and the fixes grounded against the OPC UA specification (reference.opcfoundation.org). 133 commits in four groups:

  1. Audit fixes (Fix <ID>: ...): 116 findings from a read-only audit, each with a regression test. IDs follow the audit scopes: T1-T6 Opc.Ua.Types, C1-C4 Opc.Ua.Core, S1 Opc.Ua.Core.Types + Opc.Ua.Core.Schema.
  2. Review fixes (Fix review <ID>: ...): issues a code review of group 1 found, plus lower-severity leftovers.
  3. Spec fixes (Spec: #<n> ...): the 18 places where an earlier fix only partly matched the spec text.
  4. Test and analyzer follow-ups.

Where master (#4516, #4522, #4540, #4564) or #4569 already fixed the same issue, their version was kept and this PR only adds what they didn't cover.

Behaviour and user-facing changes

Wire format and interoperability (breaking)

  • PubSub AES-CTR (Aes128-CTR / Aes256-CTR) now follows Part 14 Tables 156/157:
    • The counter block is KeyNonce[4] | MessageNonce[8] | BlockCounter[4], and the block counter starts at 1.
    • The SecurityHeader nonce is 8 bytes: Random[4] | UInt32 SequenceNumber, with the sequence number starting at 1.
    • SKS key data is split with a 4-byte KeyNonce, giving 52 / 68 bytes.
    • Not interoperable with earlier releases of this stack, but matches the spec. There is a test against the independent RFC 3686 vector.
  • User token signature (Part 4 6.1.8):
    • The signed data is now built from the user token policy's SecurityPolicy on both client and server.
    • It also supports the SecurityMode None layout ServerNonce | HASH(ServerCertificate) | ClientNonce.
    • New overload SecurityPolicyInfo.GetUserTokenSignatureData(..., MessageSecurityMode).
  • Secure channel:
    • The ECC AEAD LastSequenceNumber is correct across the 32-bit sequence wrap (Part 6 6.8.1).
    • The incoming chunk counts against MaxChunkCount/MaxMessageSize.
    • The sender no longer lets a message through that is 24 bytes over MaxMessageSize.
    • The OPN response signature is verified without overrunning the buffer (RSA_DH with differing key sizes).

Sessions (server)

  • CreateSession nonce:
    • The client nonce is kept on None channels, because the None-mode user token signature covers it.
    • A nonce over 128 bytes returns Bad_NonceInvalid on every channel (Part 4 5.7.2.3).
    • The duplicate-nonce check runs only on secured channels.
    • A non-empty nonce under 32 bytes is still accepted on None, for compatibility.
  • ActivateSession: an embedded EccEncryptedSecret signing certificate is validated with the server's peer certificate validator (Part 6 6.8.3). Byte equality with the client certificate is only the fallback when no validator is available. New opt-in interface ICertificateValidatorProvider.
  • FindServers / GetEndpoints: GatewayServerUri and DiscoveryProfileUri are no longer swapped.

Write, Read and Call (NodeState / node managers)

  • UserWriteMask:
    • Enforced for non-value attributes and for VariableType Value writes (ValueForVariableType), returning Bad_UserAccessDenied.
    • An unconfigured UserWriteMask (0, no handler) is not enforced, and reads back as the WriteMask so Read and Write agree (Part 3 8.60).
    • A throwing or Bad OnReadUserWriteMask handler fails only that item.
  • Index-range writes:
    • Slices are merged into the cached value under the attribute lock, also for OnWriteValueAsync handlers.
    • A data-change notification is still produced when the cache cannot absorb the slice.
    • The slice must match the unclipped range, otherwise Bad_IndexRangeDataMismatch (new status code in Opc.Ua.Types).
    • A range past the target returns Bad_IndexRangeNoData.
    • A handler without index-range support returns Bad_WriteNotSupported (Part 4 7.27, 5.11.4.4).
  • Index-range reads on matrices return partial results clipped to the source, instead of allocating the requested size (a Read with "0:40000,0:40000" no longer allocates about 1.6e9 elements).
  • DataTypeDefinition writes:
    • A value that isn't a decoded DataTypeDefinition, or whose kind doesn't match the DataType category, returns Bad_TypeMismatch.
    • Null is rejected for concrete Structure/Enumeration/OptionSet types.
    • WriteMask is checked first (Part 3 5.8.3).
  • AnalogItem: a null write skips the InstrumentRange check again.
  • Method call: the async fallback passes the ObjectId to the method handlers.
  • Redundancy: DistributedValueParticipation merges index-range writes and applies the index range on reads.
  • Change masks: ClearChangeMasks takes the bits atomically, so concurrent changes are no longer lost.
  • AccessLevelEx: preserved by Clone/Export.

Alarms & Conditions (Part 9)

  • Acknowledge / Confirm / AddComment:
    • Accept any EventId the condition or its branches reported while that state is still pending (5.7.3).
    • An unknown id returns Bad_EventIdUnknown.
    • An already handled state returns Bad_ConditionBranchAlreadyAcked / ...AlreadyConfirmed.
    • AddComment on a branch no longer changes the trunk, and the branch table is re-keyed when a branch's EventId changes.
  • Shelving:
    • OneShotShelve without MaxTimeShelved no longer overflows the timer.
    • A timed shelve is measured on the monotonic clock, so wall-clock steps don't delay the unshelve.
    • The state is named OneShotShelved.
  • Audit events:
    • They report failure for rejected method calls.
    • SelectedResponse is a UInt32.
    • A rejected negative Respond leaves SelectedResponse unset.
  • ContentFilterElement.Validate: an unknown FilterOperator is reported per element as Bad_FilterOperatorInvalid, instead of failing the whole service call.

ContentFilter evaluation (Part 4 7.7.3, on top of #4564)

  • Bitwise operators:
    • The result takes the size of the larger operand.
    • Boolean, numeric String and one-element array operands are converted to integers implicitly.
    • Types without an implicit integer conversion give NULL.
  • Strings: ordering is ordinal, no longer culture dependent.
  • Cast: restricted to the Table 121 conversions, and QualifiedName to LocalizedText is added. The general Variant.ConvertTo keeps its wider set.
  • Null and empty operands:
    • Like with a NULL operand is NULL.
    • Empty arrays of the same DataType compare equal.
    • Null-string operands are NULL everywhere.

Encoders and built-in types

  • XML:
    • Strings keep leading, trailing and whitespace-only content (Part 6 5.3.1.5). NodeSet import opts back into treating whitespace-only strings as empty with the new XmlDecoder.TreatWhitespaceOnlyStringsAsEmpty.
    • Layout whitespace around <Identifier> values is ignored, and trailing control characters are stripped from s= ids.
    • XmlElement ignores DTDs without expanding entities.
  • JSON:
    • A DateTime without an offset is read as UTC.
    • An unmappable nsu= NodeId and an unknown svu= ExpandedNodeId decode to a namespace-0 String NodeId (5.4.2.10 / 5.4.2.11).
    • svr= is rejected in a NodeId field.
    • Bad matrix dimensions return BadDecodingError.
    • The non-strict body stack stays balanced after an error.
    • The nesting limits match between encoder and decoder.
  • Binary:
    • MaxByteStringLength no longer applies to strings.
    • ExtensionObject bodies count toward the nesting limit on seekable streams (and in the XML encoder).
  • Parsing:
    • The NodeId parser requires i=/s=/g=/b=.
    • ExpandedNodeId.Parse rejects an empty nsu=/svu=.
    • NumericRange rejects a trailing , with Bad_IndexRangeInvalid.
    • RelativePath escapes #.
  • Status codes: StatusCode.WithSubCode/WithCodeBits keep the severity bits and look up the symbolic id for the new code.
  • Variant / VariantHelper:
    • TryCastTo returns false on a type mismatch. So DataValue.GetValue<T>(default) returns the default, and GetValueOrDefault / CastTo throw BadTypeMismatch as documented.
    • System.DateTime casts are supported.
    • Concrete encodeable arrays (e.g. Argument[]) no longer throw.
    • ConvertTo keeps one-element and empty arrays as arrays and checks the conversion is legal for empty arrays.
    • CastFrom(object[]), CastFrom(Enum[]) and the obsolete Variant(object) constructors keep the array shape (previously a one-element array collapsed to a scalar and an empty one to Null).
    • TypeInfo.Unknown / Variant.Null.TypeInfo report ValueRanks.Any.
  • Equality and hashing contracts fixed:
    • Types: Matrix/MatrixOf, typed-null Variants, NodeId (the namespace index is now hashed), ExpandedNodeId, LocalizedText, DiagnosticInfo, StructureDefinition/EnumDefinition, CertificateIdentifier, UserIdentity and DateTimeUtc.
    • MatrixOf.Dimensions returns a copy.
    • Decimal equality and hashing now run in bounded time.
  • Localized text: a LocalizedText with locale "mul" and no usable translations, or null client locale ids, no longer throw.

NodeSet, address space and utilities

  • NodeSet export/import:
    • Server index 0 is the local server on import and export, so local ids are no longer written as svr=65535.
    • StructureDefinition.BaseDataType is filled from the supertype on import.
  • NodeTable.Import: replaces duplicates, keeping the references neighbouring nodes hold. TypeTable.Add detaches a re-parented type from its old supertype, and Node.Clone gets its own ReferenceTable.
  • StringTable / NamespaceTable: capped at UInt16 indexes (BadEncodingLimitsExceeded) for Append and Update, instead of silently wrapping.
  • Small fixes:
    • UnsecureRandom.Shared is seeded randomly.
    • The peer's RetryAfterMs is clamped to one day.
    • The CancelAsync and string.GetHashCode(StringComparison) polyfills match BCL behaviour.
    • CoreUtils.IsEqual compares strings ordinally.

Schema generation and ComplexTypeSystem

  • JSON schema: describes the 1.05 layout with the compact/verbose null rules, and now validates real JsonEncoder output.
  • XSD:
    • EncodingMask (xs:unsignedLong) only for StructureWithOptionalFields.
    • Guid and StatusCode are complex types.
    • Standard types are in the Types.xsd namespace.
  • Structure-backed OptionSets are described as {Value, ValidBits}. New UaTypeDescription.IsStructureOptionSet property and constructor overload.
  • ComplexTypeSystem:
    • A malformed type in a server dictionary is skipped per type instead of discarding the whole dictionary.
    • Cyclic supertype walks are bounded.
    • Declared definitions are no longer changed.
    • A dictionary validator is kept only if the dictionary parsed.

Certificates and trust lists (Part 4 6.1.3, Part 12)

  • Revocation:
    • Checked for issuers supplied in the peer's chain and for inline-trusted issuers, against the CRLs in the trusted and issuer stores.
    • A store without CRL support no longer hides "revocation unknown".
    • RejectUnknownRevocationStatus is honoured for inline-trusted issuers unless the entry sets SuppressRevocationStatusUnknown. This changes the strict case of ExplicitIssuerChainIsHonoredWithoutStorePathAsync.
  • Basic constraints / path length violations fail with Bad_CertificateInvalid.
  • TreatAsInvalid:
    • Now yields Bad_CertificateUntrusted, which can't be suppressed or auto-accepted.
    • It is honoured on individual trust-list entries even when the certificate is also in a store.
  • EccEncryptedSecret: enforces the result of validating the sender certificate.
  • TrustListTransaction:
    • Routes each CRL to the store holding its issuer (TrustedCrls/IssuerCrls).
    • Validates staged CRLs, including the store's CRL support, before changing anything.
    • Applies certificates before CRLs.
  • Other:
    • One failing CertificateChanges observer no longer stops the others.
    • InMemory: stores share their contents per path, and InMemoryStoreProvider is IDisposable.

WoT (Opc.Ua.Types/Wot)

  • Projection security: projection security schemes whose qualified names collide are refused. New diagnostic WotDiagnosticCode.ProjectionSecurityConflict = 6063.
  • Input handling:
    • Invalid Unicode (lone surrogates, bad UTF-8) is rejected as JsonException.
    • External-schema results are keyed per affordance.
    • A duplicate security-floor member is rejected.
    • nsu= ids are percent-encoded (Part 6 5.1.12).
    • The configured JSON depth is honoured on re-parse.
    • An oversized document is reported before it is parsed back.
  • Performance and limits:
    • Subtype-graph validation, componentOf linking and the namespace table are now linear.
    • Namespaces are capped at UInt16.

New or changed public API

  • ICertificateValidatorProvider (server)
  • SecurityPolicyInfo.GetUserTokenSignatureData(..., MessageSecurityMode) overload
  • XmlDecoder.TreatWhitespaceOnlyStringsAsEmpty
  • UaTypeDescription.IsStructureOptionSet plus a constructor overload
  • WotDiagnosticCode.ProjectionSecurityConflict
  • StatusCodes.BadIndexRangeDataMismatch in the Opc.Ua.Types list
  • InMemoryStoreProvider : IDisposable

Not changed / open

  • Deferred:
    • S1-14 (BSD OptionSet size needs the base type in UaTypeDescription, and BSD is deprecated).
    • C2-7 (restoring the own-certificate trust exemption would accept certificates the spec says to reject; removing the dead code is a separate decision).
    • A1-4 (config values keep surrounding whitespace, as the pre-April-2026 loader did).
    • H-2 (JsonDecoder has no real nesting counter).
    • A2-3 (the StringTable cap also applies to diagnostics tables; default MaxArrayLength already bounds them).
  • Needs a product decision:
    • The general ConvertToString writes a QualifiedName as ns:Name, while Table 121 drops the namespace.
    • Short (< 32 byte) client nonces are accepted on None channels.

Verification

🤖 Generated with Claude Code

romanett and others added 30 commits September 28, 2026 17:04
- V-1: reserve the NumericIds pinned in the design before allocating
  identifiers, so an auto-assigned id never duplicates a pinned one.
- V-2: order the namespace list with a stable topological sort of the
  imports instead of pairwise insert-before moves; the dependency load
  order no longer depends on the order of the dependency files.
- V-3: only create the InputArguments/OutputArguments properties of a
  child method when it has arguments of that direction (regression of
  #4124, both in ValidateInstance and LinkDependencyInstance). Methods
  without arguments no longer get empty Argument[] property nodes.
- V-4: the trailing "_<digits>" name convention only supplies an enum
  value when no Identifier/BitMask is authored.
- V-5: dependency instances keep an authored ValueRank and matrices keep
  their rank (mirrors ValidateInstance).
- V-6: a method with a TypeDefinition keeps its own authored description.
- V-7: IsExplicitlyDefined matches the explicit child or its descendants,
  not siblings sharing a name prefix.
- V-8: the child reference type check was inverted and never ran; unknown
  reference types are now reported.
- V-9: ImportField keeps one dimension per rank for matrix fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r resolution

- S-1: always key a NodeSet by the model URI it declares; a differing
  ModelUri item metadata is reported as a warning instead of silently
  dropping the model.
- S-4: newest-NodeSet selection uses a total order (versioned > unparseable
  > date-only > none), so the winner no longer depends on input order.
- S-5: seed the dependency walk with the root so mutually referencing
  NodeSets do not list the root twice.
- S-8: '+' build metadata no longer marks a version pre-release; the fourth
  (revision) component is compared.
- S-15: default Name/Prefix derived from the model URI are repaired only when
  they are not valid identifiers (leading digit, '.', invalid chars, keyword,
  empty path), identically on every OS; valid names are unchanged.
- S-2/S-3: identifier sidecar paths are lexically normalized (./, ../, mixed
  separators) and the suffix fallback matches only on a directory boundary.
- S-6: CSVs claimed by a NodeSet IdentifierFile are no longer offered to the
  ModelDesign pass as identifier files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- S-9: trailers have no length prefix, so an unknown trailer version cannot
  be skipped. The reader now stops after it instead of parsing the next
  trailer out of its middle (garbage ids or a dropped payload). The wire
  format is unchanged; old payloads and old readers are unaffected.
- S-10: an empty GuidId and a namespace-only method identity are written
  and round-trip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… names (S-7)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- S-11: non-Good StatusCode defaults emit new global::Opc.Ua.StatusCode(0x...u).
- S-12: NodeSet values keep their NodeSet-local namespace indexes together
  with that table (DecodedValueNamespaceUris). NodeId/QualifiedName defaults
  resolve the namespace by URI at run time (context.NamespaceUris), and
  ExpandedNodeId defaults are emitted in nsu= form. Without a namespace table
  in scope (structure fields) the literal id is emitted and compiles.
- S-13: double/float literals use the shortest of G15..G17 (G6..G9) that
  round-trips - identical on .NET Framework and .NET, and double.MaxValue no
  longer overflows.
- S-14: remove the dead BaseDataVariableState<...> special case in
  IsOverriddenWithSameClass (generated behaviour unchanged).
- S-16: ArrayDimensions is not emitted for Scalar/ScalarOrArray/
  ScalarOrOneDimension/Any ValueRanks (Part 3 5.6.2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Stack package carried no build\<PackageId>.props, so
StackSourceGeneratorMode never reached the compiler for package consumers
(the generator silently emitted nothing) and StackSourceGeneratorExclude was
not compiler-visible anywhere. Add the props (Mode + Exclude), pack it like the
model generator's props and import it from Opc.Ua.Core / Opc.Ua.Core.Types
instead of the hand-written CompilerVisibleProperty. (R-10)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Keep incremental pipeline values equatable: [NodeManager] discovery and
  [DataType] compilations carry EquatableArray lists and a LocationInfo
  value snapshot instead of arrays/lists/Location, and the Exclude option is
  an EquatableArray, so unrelated edits and analyzer-config re-evaluation
  leave model and [DataType] generation cached. (R-1, R-7, R-8)
- Let cancellation propagate instead of reporting it as MODELGEN003. (R-2)
- [DataType] on a struct, a generic type, a private/protected nested type or
  a type nested in a non-partial type reports MODELGEN037; nested [DataType]
  types are now generated inside partial declarations of their containing
  types with nesting-qualified activators. Nested/generic [NodeManager]
  classes report MODELGEN036. (R-3, R-4)
- An ignored ModelDesign input is no longer generated (it stays a
  dependency, like an ignored NodeSet2); document that URI/Name/Prefix/
  Version metadata applies to NodeSet2/WoT inputs only. (R-5)
- Only xml AdditionalFiles with a ModelDesign/UANodeSet root enter the model
  pipeline. (R-6)
- WoT virtual-path collisions and [DataType] hint names are compared
  case-insensitively. (R-9, R-11)

Generator diagnostics for [NodeManager]/[DataType] now carry a file location
(path + span) rather than a syntax-tree location.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tring resources

E-7: EndpointsGenerator dereferenced a null Request (|| instead of &&).
E-11: ResourceGenerator emitted "public const string X = ;" for AsUtf16
string resources; emit the escaped literal directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
E-9: check the cancellation token between models and between generators
in GenerateCode/GenerateStack (not inside the emitters' node loops), and
let OperationCanceledException escape the Roslyn host instead of turning
it into an exception diagnostic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…holders

E-2: generated ObjectType proxy accessors (and the Bad_MethodInvalid method
fallback) asked the server for the SymbolicName instead of the BrowseName,
so they never resolved when the two differ. Placeholder children
(<Name>, SymbolicName Name_Placeholder) got single-child accessors that
could never resolve anything; they are no longer emitted, neither on the
client proxies nor as fluent builder type accessors.

RoboticsClient relied on the broken GetMotorIdentifier_PlaceholderAsync /
GetGearIdentifier_PlaceholderAsync accessors and on a DriveIdentifier
placeholder browse, so MotorIds/GearIds/Drives were always empty against a
real server. Motors and gears are now the PowerTrain's HasComponent
children of (a subtype of) MotorType/GearType, drives the motor's forward
IsDrivenBy targets. The facade test harness no longer fakes children named
after the placeholders and uses an exact IsTypeOf answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ObjectTypes

E-4: no proxy is emitted for an excluded ObjectType (Purpose=Testing or a
configured exclusion such as the host's default "Draft"), yet child
accessors returned it and derived proxies inherited from it (CS0246).
Child accessors of excluded types are skipped, proxies derive from the
nearest emitted ancestor, and the fluent per-type accessors skip Object
children of excluded types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…anceBrowseName constant

E-1: a DefaultInstanceBrowseName ("Device Set" -> constant DeviceSet)
registered first made a later child with the real symbolic name DeviceSet
fail generation with BadTypeMismatch, while the opposite order succeeded.
Keys derived from DefaultInstanceBrowseName are now tracked and replaced
by a real symbolic name; two real symbolic names still clash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
E-6: StringLiteralEscaper did not escape NEL/LINE SEPARATOR/PARAGRAPH
SEPARATOR, which terminate a C# regular string literal (CS1010). The
ad-hoc escapers in the fluent builder, model dependency, constants and
server capabilities generators only handled backslash and quote, and the
Namespaces constants (URI and XmlNamespace) and the proxy child-accessor
namespace URI were emitted raw. All of them now go through
StringLiteralEscaper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…non-nullable

E-3 (false positive): GetDotNetTypeName only adds a nullable annotation
for string, DiagnosticInfo (a class) and structure classes; primitives,
DateTimeUtc and enumerations stay non-nullable, so the proxy never emits
Variant.From(int?). Add a guard test for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e emitted, and completely

E-5: the instance-wrapper collision check ran for every model, so a model
without a node manager failed over names no emitted code uses. It now
only runs with GenerateManagerWrappers, and additionally reports the
collisions that break compilation of the emitted wrappers: nested wrapper
types against sibling accessors, other nested types, the reserved members
(Builder, Node, Publish) and the enclosing class (CS0542), and top-level
accessors against the members the typed manager builder forwards
(Context, NodeManager, AddObject, ...).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
E-8: with a [NodeManager] binding the manager, factory and fluent builder
files were named after the class alone, so two bound managers sharing a
class name in different namespaces overwrote each other in the output
file system and only the last one was generated. The stem is now
{Namespace}.{Class}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ady have accessors

E-10: a [NodeManager] bound to a model a reference supplies went through
the fluent-accessors-only validation, and when a referenced assembly
already provided the accessors the whole pass was skipped - no manager,
only a fluent-accessors-only diagnostic. For such binding-driven runs the
manager and its typed wrappers are now emitted without the accessors,
silently; explicit FluentAccessorsOnly runs keep their diagnostics.

E-12: the ModelFluentAccessorProvider attribute recorded the manager
namespace of a binding instead of the model prefix, which is what the
provider detection compares, so downstream accessors-only builds emitted
a second ambiguous set (CS0121). It now records the model prefix, and is
only emitted when the accessors are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
E-9 follow-up: StackGeneration turned OperationCanceledException into an
exception diagnostic, like ModelCompilation did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…CallAsync fallback

The protected CallAsync(context, objectId, ...) fallback called
Call(context, default, ...) so a sync OnCallMethod2 handler invoked through
the async call path received NodeId.Null as objectId.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…method arguments

- NS-1: NodeSet categories were written into the generated Categories
  string array without escaping; a quote or backslash broke compilation.
  Use AsStringLiteral like every other text sink.
- NS-2: structure-typed method arguments with ValueRank ScalarOrArray,
  ScalarOrOneDimension or Any are typed as Variant, but Call/CallAsync
  used the IEncodeable-constrained TryGetValue/FromStructure overloads
  (CS0315). Route them through the plain Variant path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es and type method declarations

The ObjectType, VariableType, ReferenceType, DataType and child Method
factories wrapped the AccessRestrictions/RolePermissions assignments in
if (forInstance), but type factories are only invoked with
forInstance: false, so the attributes of type nodes (and of method
declarations on types) were never set. The generator already documents
emitting them on all nodes; enforcement on type hierarchy nodes is
bypassed at runtime via NodeState.IsPartOfTypeHierarchy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erty name

The {ClassName}Value class read and wrote m_value.{FieldName} with the
authored structure field name, while DataTypeGenerator renames fields that
collide with the enclosing type name, reserved members (TypeId, Encode,
...) or inherited properties. Use Parameter.GetPropertyName() for the
structure accessor and keep the authored name for the child variable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ke node attributes

Factories assigned node attributes through the `state` local, which is
typed as the generated class, so a child named DataType, ValueRank,
AccessLevel, EventNotifier, TypeDefinitionId, NumericId, ... bound the
assignment to the child property (CS0029). Assign attributes through a
`baseState` local typed as the runtime base (BaseVariableState,
BaseObjectState, MethodState, BaseVariableTypeState) and RolePermissions
through `nodeState`, and declare such child properties `public new`
when they hide a runtime base member (was CS0108).

Tests that matched the emitted `state.X =` text are updated to the new
`baseState.X =` / `nodeState.RolePermissions =` shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GetDefaultTypeDefinitionId/GetDefaultDataTypeId and the DataTypeDefinition
factory call emitted `{Prefix}.ObjectTypes/VariableTypes/DataTypes/
DataTypeDefinitions` without global::. Inside `namespace Acme.Opc.Ua.Pumps`
the reference `Opc.Ua.DataTypes.Double` binds `Opc` to `Acme.Opc` (CS0234,
or a silent wrong binding when that class exists).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e real root

A HierarchyReference with TargetPath "" names the root of the hierarchy
(the type or top-level instance), but GetReferences emitted it against
the source node's immediate parent. For a grandchild such as
MachineType/Components/Motor the reference pointed at Components while
the root's own factory emitted the inverse end from Motor. Walk up to
the hierarchy root instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l reset of generated data types

- D-1: CollectStructureDefinitionFields returned before walking the base
  chain when a type declared no own fields, so e.g. AnonymousIdentityToken
  published an empty StructureDefinition and a type deriving from a
  field-less type lost all fields above it.
- D-2: DerivedClassWithOptionalFields.IsEqual now compares EncodingMask;
  with a base without optional fields equality was asymmetric.
- D-3: GetHashCode hashes an optional field only when its mask bit is set
  and a union member only while it is the switch field, matching IsEqual.
- D-4: Reuse() now resets through a virtual ResetForReuse() that calls
  Initialize() (model defaults, new T() structure fields, EncodingMask,
  SwitchField) and chains to the pooled base; inherited fields of abstract
  or library ancestors are reset through their properties.
- D-6: OptionSet EnumDefinition bit index is computed through ulong so a
  UInt64 OptionSet using bit 63 no longer throws OverflowException.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… matrices and clone consistent

- D-7: the emitted StructureDefinition now describes what Encode() writes:
  nullable properties are no longer declared optional (no encoding mask
  is written), inline (sealed / StructureHandling.Inline) structure fields
  name the concrete structure instead of the abstract Structure, and a
  derived type whose base activator exposes a definition prepends the base
  fields, names the base data type and sets FirstExplicitFieldIndex.
- D-8: DefaultValueHandling.Exclude omits a value only when a missing field
  decodes back to it: the literal initializer (or the CLR default without
  one); a property with a non-literal initializer is always encoded.
- D-9: MatrixOf<T> properties emitted non-existent Write/Read<T>Matrix
  calls. Structure matrices use Write/ReadEncodeableMatrix, other element
  types travel in a Variant like the model driven generator does; element
  types without a Variant matrix accessor are reported as unsupported.
- D-10: Clone() deep copies ArrayOf/MatrixOf of structures, Variant,
  ExtensionObject and DataValue.
- D-11: [Flags] enum definitions use the bit position as EnumField value
  and skip the zero and combined members; ulong values above
  long.MaxValue no longer collapse to 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s and empty unions correctly in the schemas

- D-12 (bsd): a subtype of the OptionSet structure is emitted as a
  StructuredType with BaseType ua:OptionSet (Value and ValidBits) instead
  of a 32 bit EnumeratedType; LengthInBits of an integer OptionSet walks
  the base chain to the built-in integer; the synthetic "None" is only
  added when no zero member exists and no longer throws without fields.
- D-13 (bsd): fields the generated Encode writes as a Variant (ranks other
  than Scalar/Array, matrices of built-in types, enums, subtyped
  structures) are described as ua:Variant; a concrete structure matrix
  as its Int32 dimensions followed by the counted elements.
- D-14 (xsd): a structure with optional fields starts with the
  EncodingMask element the XmlEncoder writes; a derived type introducing
  optional fields (mask written before the base fields) is flattened;
  the union choice is minOccurs="0" for SwitchField 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…simple data types

- D-15: a companion model's event records now expose
  Decoder.GetStandardFields(NamespaceTable), which puts the browse names
  the model declares into the model's namespace index of the session;
  the generated registration uses it, so select clauses match (OPC
  10000-4 7.7.4). Standard (ns=0) fields stay in namespace 0 and
  StandardFields keeps its positions for existing callers.
- D-16: a property whose data type is a companion model simple subtype
  of a built-in type (e.g. Percent : Double) is typed as that built-in
  type instead of a class that is never generated; the name based switch
  of standard data types only applies to namespace 0, so a model data
  type named e.g. Duration is no longer mistaken for the standard one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… namespaces

- D-8: treat a string.Empty initializer as the literal "" so such
  properties keep being omitted when empty.
- D-15: emit the model namespace URI as a literal in GetStandardFields so
  event records do not depend on the model's Namespaces constants class.
- Tests: static readonly expectation arrays (CA1861).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#4569 hashes every empty MatrixOf alike and treats them as equal whatever
their dimensions; the T2-2 fix compared the shape first, so [0,5] and
[5,0] were unequal while hashing alike. Check emptiness before the shape,
which matches #4569 and OPC 10000-4 7.7.3 (empty arrays of the same
DataType are equal).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from romanett/practical-murdock-fd9c56 to master September 28, 2026 22:47
@marcschier

Copy link
Copy Markdown
Collaborator

@copilot resolve the merge conflicts in this pull request

…e-bugs-0d1069

# Conflicts:
#	tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesignValidator.cs
#	tools/Opc.Ua.SourceGeneration.Core/Schema/NodeSetToModelDesign.cs

Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com>

Copilot AI commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Merged master into this branch in db945a3, which resolves the conflicts. Only two files conflicted: ModelDesignValidator.cs and NodeSetToModelDesign.cs. In both, the NodeSet/default-value XmlDecoder keeps TreatWhitespaceOnlyStringsAsEmpty = true, and master's other changes to those files are kept. UA.slnx builds with no warnings on net10.0, and Opc.Ua.SourceGeneration.Core.Tests passes (4259 tests). Since the PR base is now master, its diff should show only this PR's changes.

@CLAassistant

CLAassistant commented Sep 29, 2026 •

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
2 out of 3 committers have signed the CLA.

✅ marcschier
✅ romanett
❌ Copilot
You have signed the CLA already but the status is still pending? Let us recheck it.

@marcschier

Copy link
Copy Markdown
Collaborator

@copilot fix ci breaks

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Code coverage

✅ Coverage gate passed.

Check Result Threshold
✅ Project line rate 87.96% (281698/320267 lines) >= 70.00%
✅ Project branch rate 78.45% >= 60.00%
✅ Patch coverage 92.96% (2059/2215 changed lines) >= 75.00% (> 100 changed lines)
ℹ️ Baseline delta (advisory) +14.36 pp 73.60% recorded
Uncovered changed lines
  • src/Opc.Ua.Core.Types/State/BaseInstanceStateSnapshot.cs: 263
  • src/Opc.Ua.Core/Security/Certificates/CertificateManager/TrustListTransaction.cs: 276
  • src/Opc.Ua.Redundancy.Server/Values/DistributedValueParticipation.cs: 139, 140, 141, 142, 143
  • src/Opc.Ua.Types/Wot/WotNodeSetConverter.Analog.cs: 1186
  • src/Opc.Ua.Core/Stack/Types/FilterEvaluator.cs: 511, 537, 538, 539, 541, 542, 543, 545, 546, 547, 549, 550, 551, 557, 558, 559, 561, 562, 563, 565, 566, 567, 573, 795, 949
  • src/Opc.Ua.Types/State/DataTypeState.cs: 339, 401
  • src/Opc.Ua.Core.Schema/ComplexTypes/ComplexTypeSystem.cs: 674, 677, 678, 751, 755, 756, 757
  • src/Opc.Ua.Core/Stack/Server/RetryAfterHeader.cs: 126
  • src/Opc.Ua.Types/BuiltIn/VariantHelper.cs: 160, 161, 162, 314, 315, 316, 699, 700, 729, 730
  • src/Opc.Ua.Core/Security/Certificates/CertificateManager/InMemoryStoreProvider.cs: 147, 149, 153, 173, 178, 188, 198, 206, 213, 221, 226, 231
  • src/Opc.Ua.Types/State/NodeStateCollection.cs: 566, 574, 578, 580, 581
  • src/Opc.Ua.Core/Security/Certificates/EncryptedSecret.cs: 246
  • src/Opc.Ua.Core/Security/Certificates/CertificateManager/CertificateValidationCore.cs: 1411, 1413, 1416, 1418, 1421, 1423, 1613, 1614, 1615, 1616, 1617, 1618, 1619, 1620, 1621, 1623, 1625, 1891, 1896
  • src/Opc.Ua.Types/Nodes/NodeTable.cs: 467
  • src/Opc.Ua.Types/State/NodeState.cs: 3308, 3320, 3346, 4703, 5020, 6065
  • src/Opc.Ua.Types/BuiltIn/Decimal.cs: 328, 461
  • src/Opc.Ua.PubSub/Security/AesCtrNonceLayout.cs: 176, 177, 178
  • src/Opc.Ua.Types/BuiltIn/Variant.cs: 6123, 6157, 6158, 6159, 6164, 6184, 6185, 6186, 6201, 6227, 6228, 6229, 6230, 6231, 6232, 6233, 6234, 6235, 6236, 6237, 6238, 6239, 6247, 6248, 6249, 6250, 6251, 6252, 6253, 6254, 6255, 6256, 7689, 8827, 8836
  • src/Opc.Ua.Core/Security/SecurityPolicyInfo.cs: 398, 399, 400
  • src/Opc.Ua.Types/BuiltIn/NodeId.cs: 389, 390
  • src/Opc.Ua.Types/Wot/WotNodeSetConverter.WoT.cs: 3484, 3546, 3663, 3664, 3665, 3698, 3719
  • src/Opc.Ua.Types/Encoders/XmlDecoder.cs: 3036
  • src/Opc.Ua.Types/Wot/WotProjectionResolver.cs: 1871, 1872, 1873, 1877
  • src/Opc.Ua.Server/Session/Session.cs: 1239, 1240

Coverage is above the recorded baseline - consider ratcheting coverage-thresholds.json.

Thresholds live in coverage-thresholds.json. Whole report before exclusions: line 87.05%, branch 77.61%.

@codecov

codecov Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.85553% with 269 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.14%. Comparing base (ea9a8a0) to head (d087bff).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
src/Opc.Ua.Types/BuiltIn/Variant.cs 69.75% 36 Missing and 13 partials ⚠️
src/Opc.Ua.Core/Stack/Types/FilterEvaluator.cs 65.34% 25 Missing and 10 partials ⚠️
...es/CertificateManager/CertificateValidationCore.cs 77.45% 19 Missing and 4 partials ⚠️
src/Opc.Ua.Types/Wot/WotNodeSetConverter.WoT.cs 87.74% 7 Missing and 12 partials ⚠️
src/Opc.Ua.Types/BuiltIn/VariantHelper.cs 94.94% 10 Missing and 7 partials ⚠️
...icates/CertificateManager/InMemoryStoreProvider.cs 72.54% 12 Missing and 2 partials ⚠️
src/Opc.Ua.Types/State/NodeState.cs 90.43% 6 Missing and 5 partials ⚠️
...c.Ua.Core.Schema/ComplexTypes/ComplexTypeSystem.cs 72.22% 7 Missing and 3 partials ⚠️
src/Opc.Ua.Types/BuiltIn/Decimal.cs 88.88% 2 Missing and 4 partials ⚠️
src/Opc.Ua.Types/Encoders/XmlDecoder.cs 87.50% 1 Missing and 5 partials ⚠️
... and 31 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #4573      +/-   ##
==========================================
+ Coverage   82.06%   82.14%   +0.07%     
==========================================
  Files        2096     2096              
  Lines      318898   320267    +1369     
  Branches    55673    55934     +261     
==========================================
+ Hits       261705   263080    +1375     
+ Misses      38468    38399      -69     
- Partials    18725    18788      +63     
Flag Coverage Δ
actions 82.14% <87.85%> (+0.07%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...Schema/ComplexTypes/DataTypeDefinitionExtension.cs 97.19% <100.00%> (+0.16%) ⬆️
.../Opc.Ua.Core.Schema/Json/JsonBuiltInTypeSchemas.cs 89.18% <100.00%> (+1.68%) ⬆️
src/Opc.Ua.Core.Schema/Json/JsonSchemaGenerator.cs 91.07% <100.00%> (+1.94%) ⬆️
...Opc.Ua.Core.Schema/Json/StandardJsonDefinitions.cs 96.82% <100.00%> (+10.57%) ⬆️
...ma/Resolution/EncodeableFactoryDefinitionSource.cs 93.65% <100.00%> (+0.43%) ⬆️
src/Opc.Ua.Core.Schema/Xsd/XsdSchemaGenerator.cs 84.87% <100.00%> (+4.02%) ⬆️
...ore/Security/Certificates/CertificateIdentifier.cs 69.32% <100.00%> (+7.40%) ⬆️
...tificates/CertificateManager/CertificateManager.cs 82.88% <100.00%> (ø)
...curity/Certificates/CertificatePasswordProvider.cs 72.09% <100.00%> (+27.90%) ⬆️
...Security/Crypto/PlatformSymmetricCryptoProvider.cs 89.04% <100.00%> (+0.07%) ⬆️
... and 96 more

... and 37 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com>

Copilot AI commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

@copilot fix ci breaks

Fixed the CI breaks in d087bff. Six causes:

  • net48 / netstandard2.0 build: CertificateValidationCoreTests called File.WriteAllBytesAsync, which .NET Framework doesn't have. It now uses File.WriteAllBytes.
  • StaleForeignLeaseReadCannotDiscardConcurrentTakeoverAsync: the C4-2 lease election fix was dropped during an earlier rebase. I restored it: when a replica is not the leader, a failed reply no longer cancels a takeover attempt running at the same time.
  • SuppressedCaWithoutRequiredIssuerKeyUsageIsAcceptedAsync (Linux): OpenSSL reports a CA without keyCertSign as InvalidBasicConstraints. That case now maps to the suppressible Bad_CertificateIssuerUseNotAllowed instead of Bad_CertificateInvalid.
  • EqualsTreatsEmptyArraysOfSameDataTypeAsEqual: master's null-operand rule treats empty arrays as NULL, which made the PR's empty-array equality unreachable. Equals and InList now check for empty arrays of the same DataType first.
  • PubSub SeparateSubscriberDecodesRawDataWithConfiguredMetaDataAsync (a test that came from master): it built the key nonce from policy.NonceLength (8 bytes). It now uses the 4-byte AesCtrNonceLayout.KeyNonceLength, as the spec-conformant AES-CTR layout requires.
  • AOT VariantStorageAotTests (also from master): this PR hashes a Variant with a null or empty value like Variant.Null (0). The two assertions that expected the payload's own hash now expect 0 for null/empty values.

Locally on net10.0, Core.Tests, PubSub.Tests, the AOT VariantStorage tests and the filter/certificate/redundancy Server tests pass, and Core.Tests builds for net48. The macOS AOT and Windows jobs weren't run locally.

@marcschier marcschier added the ready Ready to merge once CI Passes label Sep 29, 2026
@marcschier
marcschier enabled auto-merge (squash) September 29, 2026 09:31
@romanett romanett changed the title Fix Core/Types audit findings and conform them to the OPC UA spec (stacked on #4569) Fix Core/Types audit findings and conform them to the OPC UA spec Sep 29, 2026
@marcschier
marcschier merged commit 20c2d77 into master Sep 29, 2026
197 checks passed
marcschier pushed a commit that referenced this pull request Sep 29, 2026
…CTT regression from #4573) (#4593)

## Description

Fixes a CTT regression introduced by #4573, found in a full CTT run of
master after the merge.

**Attribute Write Index `Err-001.js` went Pass → Error.** The test
writes a range that starts beyond the end of each array
(`1073741824:1073741824+len`), with data whose size differs from the
range by one element. It expects `Bad_IndexRangeNoData` or
`Bad_WriteNotSupported`. #4573 checked the size of the written data
before it checked whether the range lies within the target, so the
server returned `Bad_IndexRangeDataMismatch`.

Part 4 §7.27 says no data exists within such a range, so
`Bad_IndexRangeNoData` is the correct result. `NumericRange.UpdateRange`
now checks the lower bounds against the target first, for arrays,
Strings, ByteStrings and matrices. A size mismatch within the target
still returns `Bad_IndexRangeDataMismatch` (Part 4 §5.11.4.4), and the
existing mismatch tests are unchanged.

Also in this PR: `plans/ctt-issues.md` C1 now lists Aggregate –
NumberOfTransitions `003-02.js` and `004-02.js`. Since #4545,
NumberOfTransitions honours TreatUncertainAsBad, and the harness always
sends `UseServerCapabilitiesDefaults = TRUE` (Mantis 11420).

## CTT run (scripts 1.05.513, all 23 parts)

The run compared master with #4573 merged (`20c2d77d5`) against the last
recorded full run (2026-09-24, `22d07a31e`) at test-case level. It also
compared against master just before the merge (`87c35b84b`).

| Change vs recorded run | Cause |
| --- | --- |
| Attribute Write Index `Err-001.js` Pass → Error | This PR fixes it;
with the fix: Pass, 0 errors in the group |
| NumberOfTransitions: 11 cases Error → Pass; `003-02`/`004-02` Pass →
Error | #4545 (intentional), CTT defect C1 |
| Subscription Basic `Err-002.js` | Local project still claimed 100
subscriptions while #4545 raised the server to 200; passes with the
checked-in template value |
| A & C Refresh2/Shelving/Confirm: 9 cases improved | — |

Every other part is identical at test-case level, apart from the usual
"Timestamp shows a delay" warnings. All remaining errors are documented
CTT defects.

## Tests

- 3 new `NumericRangeTests` (array, String, matrix: range beyond the
target with a wrong-size slice → `Bad_IndexRangeNoData`)
- `Opc.Ua.Types.Tests` (net10.0): 11006 passed, 0 failed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
marcschier pushed a commit that referenced this pull request Sep 29, 2026
# Description

Makes every push to `master` and `release/*` report both required
checks, so a release branch can be cut from `master`.

Two things stopped `master` commits from getting a green `images
summary`, which the `Release` ruleset requires, including when a branch
is created:

1. **Cancellation.** `CI` (`buildandtest.yml`) and `Images CI`
(`docker-image.yml`) used one concurrency group per ref with
`cancel-in-progress: true`. Each merge to `master` cancels the
still-running checks of the commit before it. A queued run in the same
group is replaced as well ("Canceling since a higher priority waiting
request … exists"). `Images CI` takes about 100 minutes, and `master`
currently gets a merge about every 90 minutes:

   | Commit | `build-and-test summary` | `images summary` |
   | --- | --- | --- |
| `87c35b84b` (#4572) | success | failure: `pumpserver` / `refserver`
cancelled after ~100 min by the #4573 push |
| `20c2d77d5` (#4573) | success | failure: `refserver` / `mcpserver` /
`pumpserver` cancelled by the #4577 push |

The ruleset has no bypass actors, so `git push origin
20c2d77:refs/heads/release/2.0` is rejected with `Required status
check "images summary" is failing`.

2. **Push path filter.** `Images CI`'s push trigger had `paths-ignore:
docs/**, tests/**`. A documentation- or test-only push to `master` or
`release/*` never started the workflow, so it never reported `images
summary` and could never be a cut point.

## Changes

- **Concurrency (both workflows):** pushes to protected lines (`master`,
`main`, `master378`, `release/*`) get one concurrency group per commit
(`github.sha`) and never cancel, so every commit runs its checks to
completion. Pull requests and other branches are unchanged: they group
by PR number or ref and cancel superseded runs.
- The per-commit group is needed as well as `cancel-in-progress: false`.
With a shared group GitHub still replaces a *queued* run.
- **`Images CI` push trigger:** the `paths-ignore` is gone. The
`discover` job now decides relevance for pushes the same way it already
did for pull requests.
- It diffs `github.event.before..HEAD` and skips the image builds for
docs/tests-only changes, while `images summary` still reports.
- A new branch (all-zero `before`) or a `before` commit the clone
doesn't have builds everything, and a failed diff still fails the gate.
- `main` and `master378` are not added to the push trigger: `main`
doesn't exist, and `master378` runs its own copy of this workflow.

## Verification

- The `discover` script was extracted and run locally for push events:

  | Push | `relevant_changes` |
  | --- | --- |
  | docs-only | `false` (images skipped, summary reports) |
  | workflow change | `true` |
  | new branch | `true` |
  | unknown `before` | `true` |

- The CI-related `Opc.Ua.Tools.Tests` (`~Ci`, 53 tests, including the
fail-closed diff pin) pass on net10.0.

## Cost

Overlapping merges to `master` now run CI and image builds in parallel
instead of cancelling each other, which uses more runner minutes on busy
days. Image pushes from overlapping `master` runs can finish out of
order, so `latest-master` reflects whichever finishes last. Version tags
are per build and unaffected.

## Related Issues

- Blocks cutting `release/2.0` for `2.0.0-preview.6` (see #4574).

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [ ] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@romanett
romanett deleted the romanett/opc-ua-core-bugs-0d1069 branch October 7, 2026 03:48
marcschier added a commit that referenced this pull request Oct 9, 2026
# Description

The first PubSub exchange in
[docs/PubSub.md](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/PubSub.md#run-a-first-publisher-and-subscriber)
delivers no DataSets on master or in the 2.0.0 source. The subscriber
only logs `DataSetReader Reader 1 faulted on MessageReceiveTimeout`,
while the publisher logs `UADP security wrap failed; dropping message`
with `ArgumentException: AES-CTR nonce must be exactly 12 bytes` for
every NetworkMessage.

Cause: the reference PubSub sample's demo keys use a 12-byte KeyNonce.
Since #4573, the AES-CTR counter block follows OPC 10000-14 Table 157
(`KeyNonce[4] || MessageNonce[8] || BlockCounter[4]`). With the sample's
keys, the publisher builds a 20-byte nonce, and `AesCtrTransform`
rejects it. The 2.0.0 release (release/2.0 at bc5da10) contains #4573
and the 12-byte sample, so it is affected too.

Fix: the sample sizes its KeyNonce with
`AesCtrNonceLayout.KeyNonceLength`. No SDK change.

Regression test:
`PubSubAotTests.SamplePublisherDeliversSecuredDataSetToSampleSubscriberAsync`
starts the sample publisher and subscriber with the sample keys over
unicast loopback and waits for a DataSet. The existing sample-based AOT
tests only built or started the applications, so nothing caught the
regression.

### Validation

| Command | Result |
| --- | --- |
| `tests\Opc.Ua.Aot.Tests\bin\Debug\net10.0\Opc.Ua.Aot.Tests.exe
--treenode-filter
"/*/*/PubSubAotTests/SamplePublisherDeliversSecuredDataSetToSampleSubscriberAsync"`
(JIT), before the fix | Failed: `TimeoutException`, no DataSet within 10
s |
| same, after the fix | Passed, 3 of 3 runs |
| `tests\Opc.Ua.Aot.Tests\bin\Debug\net10.0\Opc.Ua.Aot.Tests.exe
--treenode-filter "/*/*/PubSubAotTests/*"` | 8 of 8 passed |
| The two `dotnet run` commands of the docs/PubSub.md first exchange,
run verbatim | 15 DataSets in 15 s; the publisher logs no failures |

The release/2.0 branch has the same sample and would need a backport,
which is not part of this PR.

## Related Issues

- No tracking issue. Found while re-verifying the documented first
PubSub exchange after the 2.0.0 release.

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready Ready to merge once CI Passes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants