Skip to content

Conversation

RenderMichael
Copy link
Contributor

Contributes to #90400

@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Sep 19, 2025
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-diagnostics-eventlog
See info in area-owners.md if you want to be subscribed.


bool createLogKey = (logKey == null);
if (createLogKey)
if (logKey == null)
Copy link
Contributor

Choose a reason for hiding this comment

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

unnecessary change

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe this is a hole in Roslyn’s nullability analysis, but the analyzer warns about maybe-null logKey otherwise

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, in that case it would be clearer if we said bool createLogKey = false, then createLogKey = true inside the condition.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a great idea.

try
{
key = FindSourceRegistration(source, machineName, false);
key = FindSourceRegistration(source, machineName, false)!;
Copy link
Contributor

@xtqqczze xtqqczze Sep 19, 2025

Choose a reason for hiding this comment

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

there might be a bug here if key can be null

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We validate that it’s not null because we make this same call on line 450 (and throw if the value turned out null)

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure, the method makes a call to interop, the registry could change in the meantime. It seems like a simple fix (maybe not in this PR) to use null-conditional operator in line 470.

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 code as-is definitely makes the assumption that the registry doesn’t change. Given the validation just above, key == null here seems like an error state that we should throw in instead of tolerate. A NRE is an unpleasant version of just that, though we could certainly make the exception better.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think for the annotation PR the NRE is fine

if (!string.IsNullOrEmpty(assmLocation))
{
dllPath = Path.Combine(Path.GetDirectoryName(assmLocation), AltDllName);
dllPath = Path.Combine(Path.GetDirectoryName(assmLocation)!, AltDllName);
Copy link
Contributor

Choose a reason for hiding this comment

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

looks like a bug, maybe we should fix this in a separate PR?

string? directoryName = Path.GetDirectoryName(assmLocation);
if (directoryName is not null)
{
    dllPath = Path.Combine(directoryName, AltDllName);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Path.GetDirectoryName is documented to return null if the path denotes a root directory or is null. Is it possible to make Assembly.Location point to a root directory?

Since it seems like an impossible state to me, maybe we could Debug.Assert on it instead of creating a branch that's hard to validate?

}

public EventLogInternal(string logName, string machineName) : this(logName, machineName, "", null)
public EventLogInternal(string logName, string machineName) : this(logName, machineName, "", null! /* Special case with no parent */)
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this is correct

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 EventLogInternal(string, string) constructor is only called in EventLogInternal.AddListenerComponent and stored in an LogListeningInfo. Only the ReadHandle member is read on this EventLogInternal for the duration of its lifetime, until it is removed in EventLogInternal.RemoveListenerComponent. EventLogInternal.parent is never called.

The other "special" constructor is called on various overloads of EventLog.WriteEntry, where it is immediately used to call WriteEvent (a method that also doesn't touch parent) and then disposed.

Maybe this type could use some "separation of concerns" refactoring, or maybe we should assert that parent is not null (it's assumed to be not-null on each callsite). I'm happy to change this from a null-suppression at the ctor to some other shape.

Copy link
Contributor

Choose a reason for hiding this comment

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

I my opinion, the ctor should be refactored, so that parent is left uninitialised in the "special" cases. The majority of the logic would be moved to EventLogInternal(string logName, string machineName, string source). So the last ctor would look like:

        public EventLogInternal(string logName, string machineName, string source, EventLog parent)
            : this(logName, machineName, source)
        {
            this.parent = parent;
        }

The field can then be declared as:

        [DisallowNull]
        private readonly EventLog? parent;

We would need null-suppression at the call-sites though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can narrow this down even to a single constructor; added null-suppression at the call-sites like you mentioned.

}

public EventLogInternal(string logName, string machineName, string source) : this(logName, machineName, source, null)
public EventLogInternal(string logName, string machineName, string source) : this(logName, machineName, source, null! /* Special case with no parent */)
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this is correct

Copy link
Contributor Author

Choose a reason for hiding this comment

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

else
{
int resourceId = (int)logkey.GetValue("DisplayNameID");
int resourceId = (int)logkey.GetValue("DisplayNameID")!;
Copy link
Contributor

Choose a reason for hiding this comment

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

we should probably handle the null case

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a good point.

  • All other calls to RegistryKey.GetValue handle null, though they are all reference types.
  • The LogDisplayName property seems to be "best-effort", trying a bunch of different ways to get some decent display name. In other words, we likely shouldn't be throwing if DisplayNameID is not present.


info = new LogListeningInfo();

info = new LogListeningInfo(
Copy link
Contributor

Choose a reason for hiding this comment

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

unrelated changes

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This gives us definite not-null assignment.

EventLogEntry entry = GetEntryWithOldest(i);
if (this.SynchronizingObject != null && this.SynchronizingObject.InvokeRequired)
this.SynchronizingObject.BeginInvoke(this.onEntryWrittenHandler, new object[] { this, new EntryWrittenEventArgs(entry) });
this.SynchronizingObject.BeginInvoke(this.onEntryWrittenHandler!, new object[] { this, new EntryWrittenEventArgs(entry) });
Copy link
Contributor

@xtqqczze xtqqczze Sep 19, 2025

Choose a reason for hiding this comment

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

maybe this should be fixed

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 went into this not knowing how much leeway I have to change any behavior; I set out to faithfully represent any holes in nullability. This is definitely one of those holes, probably hidden by this nerve-racking line:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a null check. This event is ultimately exposed with the EventLog.EntryWritten event; handled the scenario where nobody subscribes to it.


private EventLogEntry GetEntryWithOldest(int index)
{
Debug.Assert(readHandle != null);
Copy link
Contributor

Choose a reason for hiding this comment

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

how do we know these invariants hold?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are two callsites: GetEntryAtNoThrow (which opens a readHandle first thing) and CompletionCallback (which calls OldestEntryNumber's getter earlier; OldestEntryNumber opens a readHandle unconditionally).

The assert on cache should be within the if (entryPos >= 0) block, fixing that.


private int GetNextEntryPos(int pos)
{
Debug.Assert(cache != null);
Copy link
Contributor

Choose a reason for hiding this comment

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

how do we know these invariants hold?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Both callsites for this method are within GetCachedEntryPos, which early-exits if cache == null

private int GetCachedEntryPos(int entryIndex)
{
if (cache == null || (boolFlags[Flag_forwards] && entryIndex < firstCachedEntry) ||
(!boolFlags[Flag_forwards] && entryIndex > firstCachedEntry) || firstCachedEntry == -1)
{
// the index falls before anything we have in the cache, or the cache
// is not yet valid
return -1;
}


private int GetPreviousEntryPos(int pos)
{
Debug.Assert(cache != null);
Copy link
Contributor

Choose a reason for hiding this comment

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

how do we know these invariants hold?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

All 3 callsites for this method are within GetCachedEntryPos, which early-exits if cache == null

private int GetCachedEntryPos(int entryIndex)
{
if (cache == null || (boolFlags[Flag_forwards] && entryIndex < firstCachedEntry) ||
(!boolFlags[Flag_forwards] && entryIndex > firstCachedEntry) || firstCachedEntry == -1)
{
// the index falls before anything we have in the cache, or the cache
// is not yet valid
return -1;
}

info.handleOwner.Dispose();
//Unregister the thread pool wait handle
info.registeredWaitHandle.Unregister(info.waitHandle);
info.registeredWaitHandle?.Unregister(info.waitHandle);
Copy link
Contributor

Choose a reason for hiding this comment

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

just thought I'd call out the added null check here

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 think I was being overly cautious here, registeredWaitHandle is set in the initial AddListenerComponent. Reverting.

strings[i] = values[i].ToString();
else
strings[i] = string.Empty;
strings[i] = values[i]?.ToString() ?? string.Empty;
Copy link
Contributor

Choose a reason for hiding this comment

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

this is a change in behavior, previously strings[i] could be null

Copy link
Contributor Author

Choose a reason for hiding this comment

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

strings[i] could technically be null by virtue of object.ToString() returning a nullable string. The original code does a null check on values[i] and assigns string.Empty

if (values[i] != null)
strings[i] = values[i].ToString();
else
strings[i] = string.Empty;

This change both simplifies the code and works around a potentially null-returning ToString() method. However, It seems to me that the original devs did not want a null value in this array.

// make sure the strings aren't too long. MSDN says each string has a limit of 32k (32768) characters, but
// experimentation shows that it doesn't like anything larger than 32766
if (strings[i].Length > 32766)
if (strings[i]!.Length > 32766)
Copy link
Contributor

Choose a reason for hiding this comment

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

related to above, we need to decide if strings is string?[]? or string[]?

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 original devs were overly cautious about deliberately preventing a string?[]? scenario, as per the line just above


private sealed class LogListeningInfo
{
public LogListeningInfo(EventLogInternal handleOwner, AutoResetEvent waitHandle)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is adding the constructor necessary for the null annotation work?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it's this or = null!; on each field. I prefer this approach because it works with Roslyn instead of around Roslyn.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-System.Diagnostics.EventLog community-contribution Indicates that the PR has been added by a community member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants