Fix missing big-endian conversions for multi-byte NTLM wire fields#125039
Fix missing big-endian conversions for multi-byte NTLM wire fields#125039
Conversation
Co-authored-by: rzikm <32671551+rzikm@users.noreply.github.com>
src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedNtlm.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedNtlm.cs
Outdated
Show resolved
Hide resolved
Co-authored-by: rzikm <32671551+rzikm@users.noreply.github.com>
…esses Co-authored-by: rzikm <32671551+rzikm@users.noreply.github.com>
|
@ShreyaLaxminarayan Can you test with this change? I don't have a Big-Endian machine available to me at the moment. |
|
Tagging subscribers to this area: @dotnet/ncl, @bartonjs, @vcsjones |
There was a problem hiding this comment.
Pull request overview
Fixes remaining big-endian issues in Managed NTLM wire-format overlay structs by making several multi-byte fields endianness-aware, aligning them with the existing pattern used for other fields.
Changes:
- Update
MessageField.Length/MaximumLengthto use private backing fields + endianness-aware properties; simplifyGetField/SetFieldaccordingly. - Update
ChallengeMessage.FlagsandAuthenticateMessage.Flagsto use private backing fields + endianness-aware properties (removing call-site byte swapping). - Update
NtChallengeResponse.Timeto use a private backing field + endianness-aware property.
src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedNtlm.cs
Show resolved
Hide resolved
|
@Dotnet-s390x build |
|
Build Queued.. To cancel the current build, please comment: |
|
Build Failed Build Error Summary |
|
Failures are un-related to the changes in the PR |
|
@copilot invoke the code-review skill and post the analysis/results as a comment on this PR |
🤖 Copilot Code Review — PR #125039Holistic AssessmentMotivation: Justified. PR #124598 added big-endian support to ManagedNtlm but inconsistently applied the pattern — Approach: Correct. The fix consistently applies the established Summary: ✅ LGTM. The change is correct, complete, and consistent with the existing codebase pattern. All multi-byte fields in NTLM wire-format overlay structs are now endianness-aware. Struct layouts are preserved (private backing fields occupy the same positions as the former public fields). The code simplifications ( Detailed Findings✅ Struct layout preservation verifiedAll ✅ Endianness coverage is completeAudited all struct fields in the file:
✅
|
|
Can this be merged now? |
Would you be able to test with this change? I don't have a Big-Endian machine available to me at the moment and would prefer to avoid another follow-up. |
|
@Dotnet-s390x build |
|
Build Queued.. To cancel the current build, please comment: |
|
Build Failed Build Error Summary |
|
The Socket tests seems to fail around UDP. Unlikely relevant to this change. |
|
yes, failures are unrelated to the changes. |
|
/ba-g test failures are unrelated |
|
@dotnet/ncl, this should be ready to merge, PTAL |
liveans
left a comment
There was a problem hiding this comment.
LGTM, is it possible to add some tests?
We should have some platforms running Ubuntu 26 on ARM that should cover this, but they might not be running as part of inner-build. |
I don't know if that is big endian. |
…125039) PR #124598 added big-endian support for ManagedNtlm but missed several multi-byte fields. On big-endian architectures, `MessageField.Length/MaximumLength`, `ChallengeMessage.Flags`, `AuthenticateMessage.Flags`, and `NtChallengeResponse.Time` were still read/written without byte-swap, corrupting the NTLM wire format. ## Description All changes follow the endianness-aware property pattern already established by `_payloadOffset`, `_productBuild`, and `NegotiateMessage.Flags`: ```csharp private T _field; public T Field { readonly get => BitConverter.IsLittleEndian ? _field : BinaryPrimitives.ReverseEndianness(_field); set => _field = BitConverter.IsLittleEndian ? value : BinaryPrimitives.ReverseEndianness(value); } ``` **`MessageField`** - `Length` and `MaximumLength` (`ushort`) converted from public fields to private backing fields with endianness-aware properties - `unsafe` removed from struct (no fixed arrays, no longer needed) - `GetFieldLength` and `GetFieldOffset` helpers removed; `GetField` now accesses `field.Length` and `field.PayloadOffset` directly - `SetField` → direct property assignments (was `MemoryMarshal.AsBytes` + `WriteInt16/32LittleEndian`) **`ChallengeMessage.Flags`** (`uint`) - Converted to private `_flags` + property; removed the inline `BitConverter.IsLittleEndian` conversion at the call site **`AuthenticateMessage.Flags`** (`uint`) - Same treatment as `ChallengeMessage.Flags` **`NtChallengeResponse.Time`** (`long`) - Converted to private `_time` + endianness-aware property All `[StructLayout(LayoutKind.Sequential)]` struct layouts are unchanged — backing fields remain in identical declaration positions. <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> ## Background PR #124598 added big-endian support for ManagedNtlm by introducing endianness-aware getters/setters on some struct fields. However, several multi-byte fields were missed and still need conversion. The NTLM wire protocol is little-endian, so all multi-byte fields in the overlay structs must be stored in little-endian format. On big-endian architectures, the fields that are accessed directly (without `BinaryPrimitives` conversion) will have the wrong byte order. The file to modify is: `src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedNtlm.cs` ## What PR #124598 already fixed The following fields already have proper endianness-aware getters/setters: - `MessageField._payloadOffset` (int) — has `PayloadOffset` property with `ReverseEndianness` - `Version._productBuild` (ushort) — has `ProductBuild` property with `ReverseEndianness` - `NegotiateMessage._flags` (Flags/uint) — has `Flags` property with `ReverseEndianness` Additionally, `ChallengeMessage.Flags` is read with an inline endianness conversion at the call site (line 592). ## What still needs to be fixed The following multi-byte fields are still directly accessed without endianness conversion and need the same getter/setter treatment: ### 1. `MessageField.Length` and `MessageField.MaximumLength` (both `ushort`) These are currently public fields (`public ushort Length; public ushort MaximumLength;`) accessed directly. They should be made private backing fields with endianness-aware property getters/setters, like `_payloadOffset` already is. **Note:** After adding properties, the helper functions `GetFieldLength`, `GetFieldOffset`, and `SetField` that currently use `BinaryPrimitives.ReadInt16LittleEndian` / `WriteInt16LittleEndian` / `WriteInt32LittleEndian` to read/write the raw bytes of MessageField should be refactored to simply use the new properties directly. This eliminates the redundant byte-level endianness handling since the properties now handle it. Similarly, `GetFieldOffset` already reads the offset via `ReadInt16LittleEndian` on raw bytes, but the `PayloadOffset` property getter already handles this — so `GetFieldOffset` should just use `field.PayloadOffset`. After this refactoring, the `unsafe` modifier can likely be removed from `GetFieldLength` and `GetFieldOffset`. ### 2. `ChallengeMessage.Flags` (Flags/uint) Currently a public field. The conversion is done inline at the call site (line 592) with: ```csharp Flags flags = BitConverter.IsLittleEndian ? challengeMessage.Flags : (Flags)BinaryPrimitives.ReverseEndianness((uint)challengeMessage.Flags); ``` This should be converted to a private backing field with an endianness-aware property (like `NegotiateMessage.Flags` already is), and the call site should simply read `challengeMessage.Flags` without the inline conversion. ### 3. `AuthenticateMessage.Flags` (Flags/uint) Currently a public field that is written to directly on line 646: ```csharp response.Flags = s_requiredFlags | (flags & Flags.NegotiateSeal); ``` This should be converted to a private backing field with an endianness-aware property getter/setter, like `NegotiateMessage.Flags` already is. ### 4. `NtChallengeResponse.Time` (long) Currently a public field written on line 424: ```csharp temp.Time = time.ToFileTimeUtc(); ``` This needs to be stored as little-endian on the wire. Should be converted to a private backing field with an endianness-aware property. ### 5. `NtChallengeResponse._reserved3` (int) and `NtChallengeResponse._reserved4` (int) These are private `int` fields. Although they are reserved (always zero-initialized via `Clear()`), they should still have endianness conversion for correctness and consistency. Since they're always zero, the conversion is a no-op in practice, but it's good form. However, since they are private and only ever zero, these can be left as-is if the team prefers — the key point is `Time` above. ## Summary of changes needed 1. **`MessageField`**: Make `Length` and `MaximumLength` private with endianness-aware properties. Simplify `GetFieldLength`, `GetFieldOffset`, and `SetField` to use the new properties. 2. **`ChallengeMessage`**: Make `Flags` a private backing field with endianness-aware property (same pattern as `NegotiateMessage.Flags`). Remove inline conversion at call site. 3. **`AuthenticateMessage`**: Make `Flags` a private backing field with endianness-aware property. 4. **`NtChallengeResponse`**: Make `Time` a private backing field with endianness-aware property. The pattern for all should be the same as the existing conversions in the file: ```csharp private T _backingField; public T Property { readonly get => BitConverter.IsLittleEndian ? _backingField : BinaryPrimitives.ReverseEndianness(_backingField); set => _backingField = BitConverter.IsLittleEndian ? value : BinaryPrimitives.ReverseEndianness(value); } ``` </details> <!-- START COPILOT CODING AGENT SUFFIX --> *This pull request was created from Copilot chat.* > <!-- START COPILOT CODING AGENT TIPS --> --- ✨ Let Copilot coding agent [set things up for you](https://github.com/dotnet/runtime/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rzikm <32671551+rzikm@users.noreply.github.com>
PR #124598 added big-endian support for ManagedNtlm but missed several multi-byte fields. On big-endian architectures,
MessageField.Length/MaximumLength,ChallengeMessage.Flags,AuthenticateMessage.Flags, andNtChallengeResponse.Timewere still read/written without byte-swap, corrupting the NTLM wire format.Description
All changes follow the endianness-aware property pattern already established by
_payloadOffset,_productBuild, andNegotiateMessage.Flags:MessageFieldLengthandMaximumLength(ushort) converted from public fields to private backing fields with endianness-aware propertiesunsaferemoved from struct (no fixed arrays, no longer needed)GetFieldLengthandGetFieldOffsethelpers removed;GetFieldnow accessesfield.Lengthandfield.PayloadOffsetdirectlySetField→ direct property assignments (wasMemoryMarshal.AsBytes+WriteInt16/32LittleEndian)ChallengeMessage.Flags(uint)_flags+ property; removed the inlineBitConverter.IsLittleEndianconversion at the call siteAuthenticateMessage.Flags(uint)ChallengeMessage.FlagsNtChallengeResponse.Time(long)_time+ endianness-aware propertyAll
[StructLayout(LayoutKind.Sequential)]struct layouts are unchanged — backing fields remain in identical declaration positions.Original prompt
Background
PR #124598 added big-endian support for ManagedNtlm by introducing endianness-aware getters/setters on some struct fields. However, several multi-byte fields were missed and still need conversion. The NTLM wire protocol is little-endian, so all multi-byte fields in the overlay structs must be stored in little-endian format. On big-endian architectures, the fields that are accessed directly (without
BinaryPrimitivesconversion) will have the wrong byte order.The file to modify is:
src/libraries/System.Net.Security/src/System/Net/NegotiateAuthenticationPal.ManagedNtlm.csWhat PR #124598 already fixed
The following fields already have proper endianness-aware getters/setters:
MessageField._payloadOffset(int) — hasPayloadOffsetproperty withReverseEndiannessVersion._productBuild(ushort) — hasProductBuildproperty withReverseEndiannessNegotiateMessage._flags(Flags/uint) — hasFlagsproperty withReverseEndiannessAdditionally,
ChallengeMessage.Flagsis read with an inline endianness conversion at the call site (line 592).What still needs to be fixed
The following multi-byte fields are still directly accessed without endianness conversion and need the same getter/setter treatment:
1.
MessageField.LengthandMessageField.MaximumLength(bothushort)These are currently public fields (
public ushort Length; public ushort MaximumLength;) accessed directly. They should be made private backing fields with endianness-aware property getters/setters, like_payloadOffsetalready is.Note: After adding properties, the helper functions
GetFieldLength,GetFieldOffset, andSetFieldthat currently useBinaryPrimitives.ReadInt16LittleEndian/WriteInt16LittleEndian/WriteInt32LittleEndianto read/write the raw bytes of MessageField should be refactored to simply use the new properties directly. This eliminates the redundant byte-level endianness handling since the properties now handle it. Similarly,GetFieldOffsetalready reads the offset viaReadInt16LittleEndianon raw bytes, but thePayloadOffsetproperty getter already handles this — soGetFieldOffsetshould just usefield.PayloadOffset. After this refactoring, theunsafemodifier can likely be removed fromGetFieldLengthandGetFieldOffset.2.
ChallengeMessage.Flags(Flags/uint)Currently a public field. The conversion is done inline at the call site (line 592) with:
This should be converted to a private backing field with an endianness-aware property (like
NegotiateMessage.Flagsalready is), and the call site should simply readchallengeMessage.Flagswithout the inline conversion.3.
AuthenticateMessage.Flags(Flags/uint)Currently a public field that is written to directly on line 646:
This should be converted to a private backing field with an endianness-aware property getter/setter, like
NegotiateMessage.Flagsalready is.4.
NtChallengeResponse.Time(long)Currently a public field written on line 424:
This needs to be stored as little-endian on the wire. Should be converted to a private backing field with an endianness-aware property.
5.
NtChallengeResponse._reserved3(int) andNtChallengeResponse._reserved4(int)These are private
intfields. Although they are reserved (always zero-initialized viaClear()), they should still have endianness conversion for correctness and consistency. Since they're always zero, the conversion is a no-op in practice, but it's good form. However, since they are private and only ever zero, these can be left as-is if the team prefers — the key point isTimeabove.Summary of changes needed
MessageField: MakeLengthandMaximumLengthprivate with endianness-aware properties. SimplifyGetFieldLength,GetFieldOffset, andSetFieldto use the new properties.ChallengeMessage: MakeFlagsa private backing field with endianness-aware property (same pattern asNegotiateMessage.Flags). Remove inline conversion at call site.AuthenticateMessage: MakeFlagsa private backing field with endianness-aware property.NtChallengeResponse: MakeTimea private backing field with endianness-aware property.The pattern for all should be the same as the existing conversions in the file:
This pull request was created from Copilot chat.
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.