Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spanify and cleanup System.Security.Principal.Windows #1637

Merged
merged 7 commits into from
Jan 17, 2020
Merged

Spanify and cleanup System.Security.Principal.Windows #1637

merged 7 commits into from
Jan 17, 2020

Conversation

Wraith2
Copy link
Contributor

@Wraith2 Wraith2 commented Jan 11, 2020

The code for and around SecurityIdentifier has a number of places where arrays are created only to be passed to a function which will copy their contents. A temporary array of known and small max length is also used when creating one. These can all be made cheaper using stackalloc and spans. This work is in the first commit.

The second commit is a general cleanup and formatting of some code which looks like it's quite old. There are no functional changes just renames and reformat to be within current guidelines and auto code fix applications.

@Wraith2
Copy link
Contributor Author

Wraith2 commented Jan 12, 2020

I'm not sure what build config is failing. building for frameworks netcoreapp and netfx both work locally but allconfigurations fails and I'm not sure how to track down what flags the cause of the failure are under. It looks like whatever build it is just doesn't have span so I'd add it if I knew.

There's also no specific code owner for this library so as a shot in the dark and knowing that span usage will need a security perspective review I'll @ you @bartonjs .

{
if (subAuthorities == null)
if (subAuthorities.IsEmpty)
{
throw new ArgumentNullException(nameof(subAuthorities));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this reachable? Wondering if it should be an assert instead.

If it is reachable, is it possible this is now throwing for an empty input whereas previously it would only throw for a null input?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does seem like it would be better as Debug.Assert(subAuthorities.Length > 0). All current callers are passing values downstream of new int[]. The only one that's possibly a concern is CreateFromBinaryForm, if it gets written to hybridize stack and array. But that just means that CreateFromBinaryForm needs to handle the zero case itself. (And probably needs a test that passes 0 sub-authorities, to make sure we don't regress behavior).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check is invalid, 0 is a valid and used length. The null check has been hoist out to the caller for the ctor and no other case can pass in null.

{
throw new ArgumentNullException(nameof(subAuthorities));
}

int subAuthoritiesLength = subAuthorities.Length;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you create a local for this? Seems unnecessary, and on top of that it can actually defeat JIT optimizations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought I was being helpful. If they JIT wants to do the work of identifying multiple uses of the value I'll let it. Changed.

_subAuthorities = new int[subAuthorities.Length];
subAuthorities.CopyTo(_subAuthorities, 0);
_subAuthorities = new int[subAuthoritiesLength];
subAuthorities.CopyTo(new Span<int>(_subAuthorities));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two lines can just be:

_subAuthorities = subAuthorities.ToArray();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

}

IdentifierAuthority Authority;
int[] SubAuthorities;
IdentifierAuthority authority;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be defined up here, or can it instead be moved down to where it's assigned about 30 lines later?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved.


//
// Subauthorities are represented in big-endian format
//

for (byte i = 0; i < binaryForm[offset + 1]; i++)
for (int i = 0; i < binaryForm[offset + 1]; i++)
{
unchecked
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unchecked can be removed. Nothing in dotnet/runtime has checked enabled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -479,17 +465,17 @@ private void CreateFromBinaryForm(byte[] binaryForm, int offset)
(((long)binaryForm[offset + 6]) << 8) +
(((long)binaryForm[offset + 7])));

SubAuthorities = new int[binaryForm[offset + 1]];
int subAuthoritiesLength = binaryForm[offset + 1];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible this is 0? If yes, then the change made in CreateFromParts is going to throw an exception because the span will be empty, whereas previously it would have accepted a non-null but empty array.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless something already would have caught it, it's entirely possible... because this is interpreting data from a public ctor (SecurityIdentifier..ctor(byte[], int)).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be 0 and valid. I've added a max length check and test.

@@ -439,8 +432,8 @@ public override bool IsAuthenticated
{
// This approach will not work correctly for domain guests (will return false
// instead of true). This is a corner-case that is not very interesting.
_isAuthenticated = CheckNtTokenForSid(new SecurityIdentifier(IdentifierAuthority.NTAuthority,
new int[] { Interop.SecurityIdentifier.SECURITY_AUTHENTICATED_USER_RID })) ? 1 : 0;
ReadOnlySpan<int> subAuthorities = stackalloc int[1] { Interop.SecurityIdentifier.SECURITY_AUTHENTICATED_USER_RID };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: you didn't need to separate this out into a local... you can just change the new int[] to stackalloc int[] in the argument.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like a better change is to make internal singletons for all of these comparison SIDs. If they happen to use stackalloc, fine, but it's way less important if they're only ever built once.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made the statically known ones static lazy initialized singletons. I don't see a point making everyone pay for creating them just because they touch the class i'd prefer that only people who call the methods that need them pay the cost.

}

_Identities[index] = value;
_identities[index] = value ?? throw new ArgumentNullException(nameof(value));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I hate this syntax, especially for argument validation. (Mainly because it doesn't compose well, because it leaks half-done work when used for field assignments).

Since there's not a second statement here, and there's unlikely to ever be one, I don't have a technical argument. So I'll just leave it at babbling.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was an autofix suggestion so I have no attachment to it. Informed babbling is reason enough for me, reverted.

nameof(identifierAuthority),
identifierAuthority,
SR.IdentityReference_IdentifierAuthorityTooLarge);
throw new ArgumentOutOfRangeException(nameof(identifierAuthority), identifierAuthority, SR.IdentityReference_IdentifierAuthorityTooLarge);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the nameof was previously indented wrong, I liked the previous, chopped, style better. This line now causes horizontal scrolling on the default github window width.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reformatted it. In general i'm not a fan of limiting things because github has a silly limitation though because it feels like limiting line lengths to 72 chars because of terminals.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitHub's limit is 130, and I usually chop around 120 . I think my code window is about 138 characters wide, which generally avoids the need for a horizontal scrollbar.

We don't actually have a repository-wide rule, but in general it's nice to passers-by to avoid h-scroll on GitHub.

{
unchecked
{
SubAuthorities[i] =
subAuthorities[i] =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see anything that complains if binaryForm[offset+1] exceeds MaxSubAuthorities. Please write a test that does so.

I think this is going to give a non-friendly out-of-bounds exception instead of an appropriate "your data is malformed"-type exception.

@@ -172,7 +171,7 @@ internal static byte[] ConvertIntPtrSidToByteArraySid(IntPtr binaryForm)
uint length = (uint)SecurityIdentifier.MaxBinaryLength;
resultSid = new byte[length];

if (FALSE != Interop.Advapi32.CreateWellKnownSid((int)sidType, domainSid == null ? null : domainSid.BinaryForm, resultSid, ref length))
if (FALSE != Interop.Advapi32.CreateWellKnownSid((int)sidType, domainSid?.BinaryForm, resultSid, ref length))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW: Of the changes in this file, I like this one (reduced the line length to not scroll) and dislike the other two (added horizontal scrolling).

@bartonjs
Copy link
Member

I'd definitely like to see tests (including just pointing out that they exist) for zero and too-many subidentifiers, to make sure that the exception model didn't change (or to codify that we intentionally change it, if we end up needing to).

@bartonjs
Copy link
Member

As for the flavor that's failing to build, I'm guessing either netstandard2.0 or net461:

<PackageConfigurations>
netstandard2.0;
netcoreapp2.0-Windows_NT;
netcoreapp2.0-Unix;
netcoreapp2.1-Windows_NT;
netcoreapp2.1-Unix;
net461-Windows_NT;
</PackageConfigurations>

(((uint)binaryForm[offset + 8 + 4 * i + 1]) << 8) +
(((uint)binaryForm[offset + 8 + 4 * i + 2]) << 16) +
(((uint)binaryForm[offset + 8 + 4 * i + 3]) << 24)
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically the chop style on this would be what it was. I understand the symmetry here with the bracing rules, so I'm more pointing it out than suggesting a change.

@Wraith2
Copy link
Contributor Author

Wraith2 commented Jan 13, 2020

I'd like to get rid of

private readonly object _claimsIntiailizedLock = new object();

because it's rarely used. It's used in one place:
private void InitializeClaims()
{
if (!_claimsInitialized)
{
lock (_claimsIntiailizedLock)
{

Would it be safe to drop the ctor initialization and add in

if (_claimsIntiailizedLock is null)
{
    Interlocked.CompareExchange(ref _claimsIntiailizedLock, new object(), null);
}

before the lock is taken?

@bartonjs
Copy link
Member

If it's really only locked in one place, and also using a boolean as a guard, then perhaps the method can/should be rewritten in terms of LazyInitializer.

private void InitializeClaims()
{
    bool discard = false;

    LazyInitializer.EnsureInitialized(
        ref discard,
        ref _claimsInitialized,
        () =>
        {
            // current lock body
            return true;
        });
}

@Wraith2
Copy link
Contributor Author

Wraith2 commented Jan 14, 2020

Evrything addressed and updated including the netcoreapp2.0 build issues.

Copy link
Member

@GrabYourPitchforks GrabYourPitchforks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall LGTM - thanks! One notable suggestion re: refactoring involving the subAuthorities local. I realize this suggestion will likely regress a single bounds check optimization, but IMO that's an acceptable tradeoff given the reasoning I listed in the comment.

@@ -361,21 +364,20 @@ private void CreateFromParts(IdentifierAuthority identifierAuthority, int[] subA
// } SID, *PISID;
//

byte i;
_binaryForm = new byte[1 + 1 + 6 + 4 * this.SubAuthorityCount];
_binaryForm = new byte[1 + 1 + 6 + 4 * subAuthorities.Length];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert all changes involving the subAuthorities local from this point onward. Essentially, pretend that once the _subAuthorities field is set, the subAuthorities local is set to nullptr. This explicit cut helps lessen the chances of introducing TOCTOU attacks in this code, since we don't want to risk performing operations on the [potentially mutated] subAuthorities local when the _subAuthorities field is instead the source of truth.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed the to use the field not the local variable. Would it have been viable to build everything in locals and only assign to fields at the end of the function once everything was known to be ready to move into the constructed state?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't necessarily care about when the field is set. But in particular, the recommendation is that very early on in the method we should make a copy of the data being passed in, and then from that point onward we should only be looking at the copy - not the original source of the data. Whether the copy is immediately assigned to a field or whether the field assignment is deferred to the end of the method isn't really that important IMO.

@Wraith2
Copy link
Contributor Author

Wraith2 commented Jan 15, 2020

Test failures seem unrelated.

@bartonjs
Copy link
Member

System\Security\Principal\SID.cs#L350
System\Security\Principal\SID.cs(350,31): error CS1525: Invalid expression term '='

That one seems like not a flaky test.

@bartonjs bartonjs merged commit 6ca0517 into dotnet:master Jan 17, 2020
@Wraith2
Copy link
Contributor Author

Wraith2 commented Jan 17, 2020

@David-Engel @cheenamalhotra this will help perf on .net 5 by reducing pool fetch allocations .

@Wraith2 Wraith2 deleted the perf-securityidentifier branch January 17, 2020 15:09
@ghost ghost locked as resolved and limited conversation to collaborators Dec 11, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants