Conversation
WalkthroughUpdated project comments in the MSBuild file and changed RegistryProcessor creation/lifecycle in ObjectProcessors to use a StrategyExecutor, register per-domain processors via GetOrAdd, and subscribe/detach ComputerStatusEvent handlers. Changes
Sequence Diagram(s)sequenceDiagram
participant Caller as ObjectProcessors
participant Map as RegistryProcessorMap
participant RP as RegistryProcessor
participant Exec as StrategyExecutor
Caller->>Map: GetOrAdd(domain)
alt processor exists
Map-->>Caller: existing RP
Caller->>RP: ReadRegistrySettings(...)
else create new
Map-->>RP: create RP(null, Exec, domain)
RP->>Exec: (held internally)
RP-->>Map: store RP
Caller->>RP: subscribe ComputerStatusEvent
Caller->>RP: ReadRegistrySettings(...)
end
Caller->>RP: Detach in ClearEventHandlers()
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/Runtime/ObjectProcessors.cs (1)
377-386:⚠️ Potential issue | 🟠 MajorTOCTOU race on
_registryProcessorMap: useGetOrAddto avoid leaked processors and duplicate event subscriptions.
TryGetValue+TryAddon aConcurrentDictionaryis not atomic. If two threads process computers with the sameDomainSidconcurrently, both can failTryGetValue, both create aRegistryProcessorwith a subscribed event handler, and only one winsTryAdd. The losing thread's processor is used but never tracked in the map, soClearEventHandlers(Line 89-91) will never unsubscribe it — leaking the event handler.Additionally,
new StrategyExecutor()is allocated per race loser unnecessarily.Replace with
GetOrAdd(or theLazy<T>variant ifRegistryProcessorconstruction has side effects you want to deduplicate):Proposed fix
if (_methods.HasFlag(CollectionMethod.NTLMRegistry)) { await _context.DoDelay(); - if (_registryProcessorMap.TryGetValue(resolvedSearchResult.DomainSid, out var processor)) { - ret.NTLMRegistryData = await processor.ReadRegistrySettings(resolvedSearchResult.DisplayName); - } else { - var newProcessor = new RegistryProcessor(null, new StrategyExecutor(), resolvedSearchResult.Domain); - newProcessor.ComputerStatusEvent += HandleCompStatusEvent; - _registryProcessorMap.TryAdd(resolvedSearchResult.DomainSid, newProcessor); - ret.NTLMRegistryData = await newProcessor.ReadRegistrySettings(resolvedSearchResult.DisplayName); - } + var processor = _registryProcessorMap.GetOrAdd(resolvedSearchResult.DomainSid, _ => { + var p = new RegistryProcessor(null, new StrategyExecutor(), resolvedSearchResult.Domain); + p.ComputerStatusEvent += HandleCompStatusEvent; + return p; + }); + ret.NTLMRegistryData = await processor.ReadRegistrySettings(resolvedSearchResult.DisplayName); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Runtime/ObjectProcessors.cs` around lines 377 - 386, The code has a TOCTOU on _registryProcessorMap around the NTLMRegistry path: replace the TryGetValue/TryAdd pattern with ConcurrentDictionary.GetOrAdd (or GetOrAdd with Lazy<RegistryProcessor>) so RegistryProcessor instances and their ComputerStatusEvent subscription (HandleCompStatusEvent) are created and attached atomically; ensure you only subscribe HandleCompStatusEvent once on the instance returned from GetOrAdd and avoid allocating new StrategyExecutor() for losers by creating the RegistryProcessor only inside the factory passed to GetOrAdd (or Lazy.Value), then call ReadRegistrySettings on the returned processor to populate ret.NTLMRegistryData.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@src/Runtime/ObjectProcessors.cs`:
- Around line 377-386: The code has a TOCTOU on _registryProcessorMap around the
NTLMRegistry path: replace the TryGetValue/TryAdd pattern with
ConcurrentDictionary.GetOrAdd (or GetOrAdd with Lazy<RegistryProcessor>) so
RegistryProcessor instances and their ComputerStatusEvent subscription
(HandleCompStatusEvent) are created and attached atomically; ensure you only
subscribe HandleCompStatusEvent once on the instance returned from GetOrAdd and
avoid allocating new StrategyExecutor() for losers by creating the
RegistryProcessor only inside the factory passed to GetOrAdd (or Lazy.Value),
then call ReadRegistrySettings on the returned processor to populate
ret.NTLMRegistryData.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/Runtime/ObjectProcessors.cs`:
- Around line 379-383: The current use of _registryProcessorMap.GetOrAdd with a
factory can create multiple RegistryProcessor instances that subscribe to
ComputerStatusEvent and leak if discarded; change the dictionary to store
Lazy<RegistryProcessor> instead, update the GetOrAdd call to add a new Lazy that
constructs the RegistryProcessor and subscribes to ComputerStatusEvent inside
the Lazy factory (using resolvedSearchResult.Domain and DomainSid), and update
any code that reads processors to use .Value (e.g., when accessing the
processor) as well as modify ClearEventHandlers to iterate the stored Lazy
values and unsubscribe from each Lazy.Value.ComputerStatusEvent (or otherwise
access Value once) so that construction and subscription happen exactly once per
key.
| var processor = _registryProcessorMap.GetOrAdd(resolvedSearchResult.DomainSid, _ => { | ||
| var newProcessor = new RegistryProcessor(null, new StrategyExecutor(), resolvedSearchResult.Domain); | ||
| newProcessor.ComputerStatusEvent += HandleCompStatusEvent; | ||
| return newProcessor; | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
git ls-files | grep -E "ObjectProcessors|RegistryProcessor"Repository: SpecterOps/SharpHound
Length of output: 96
🏁 Script executed:
head -50 src/Runtime/ObjectProcessors.cs | cat -nRepository: SpecterOps/SharpHound
Length of output: 2901
🏁 Script executed:
sed -n '35,55p' src/Runtime/ObjectProcessors.cs | cat -nRepository: SpecterOps/SharpHound
Length of output: 1728
🏁 Script executed:
sed -n '370,395p' src/Runtime/ObjectProcessors.cs | cat -nRepository: SpecterOps/SharpHound
Length of output: 1664
🏁 Script executed:
sed -n '85,100p' src/Runtime/ObjectProcessors.cs | cat -nRepository: SpecterOps/SharpHound
Length of output: 998
🏁 Script executed:
rg -n "class RegistryProcessor" --type=csRepository: SpecterOps/SharpHound
Length of output: 47
🏁 Script executed:
rg -n "class StrategyExecutor" --type=csRepository: SpecterOps/SharpHound
Length of output: 47
🏁 Script executed:
rg -n "public RegistryProcessor" --type=cs -A 5Repository: SpecterOps/SharpHound
Length of output: 47
🏁 Script executed:
fd -type f -name "*.cs" | xargs rg -l "class RegistryProcessor" 2>/dev/nullRepository: SpecterOps/SharpHound
Length of output: 237
🏁 Script executed:
fd -type f -name "*.cs" | xargs rg -l "class StrategyExecutor" 2>/dev/nullRepository: SpecterOps/SharpHound
Length of output: 237
🏁 Script executed:
find . -name "*.csproj" -o -name "*.sln" | head -5Repository: SpecterOps/SharpHound
Length of output: 101
🏁 Script executed:
cat .csproj 2>/dev/null || cat *.csproj 2>/dev/null | head -100Repository: SpecterOps/SharpHound
Length of output: 2726
🏁 Script executed:
git ls-files | grep -i "csproj\|packages"Repository: SpecterOps/SharpHound
Length of output: 82
🏁 Script executed:
rg -n "RegistryProcessor" --type-add 'src:*.cs'Repository: SpecterOps/SharpHound
Length of output: 980
🏁 Script executed:
rg -n "StrategyExecutor" --type-add 'src:*.cs'Repository: SpecterOps/SharpHound
Length of output: 221
🏁 Script executed:
rg -n "GetOrAdd" --type-add 'src:*.cs'Repository: SpecterOps/SharpHound
Length of output: 202
🏁 Script executed:
rg -n "ClearEventHandlers" --type-add 'src:*.cs'Repository: SpecterOps/SharpHound
Length of output: 297
🏁 Script executed:
rg -n "_registryProcessorMap" --type-add 'src:*.cs'Repository: SpecterOps/SharpHound
Length of output: 454
🏁 Script executed:
sed -n '375,390p' src/Runtime/ObjectProcessors.cs | cat -nRepository: SpecterOps/SharpHound
Length of output: 971
🏁 Script executed:
sed -n '77,95p' src/Runtime/ObjectProcessors.cs | cat -nRepository: SpecterOps/SharpHound
Length of output: 1253
GetOrAdd factory may run multiple times for the same key, leaking processors with dangling event subscriptions.
ConcurrentDictionary.GetOrAdd(key, factory) does not guarantee the factory executes only once per key under concurrent access. If two threads hit this for the same DomainSid simultaneously, both may create a RegistryProcessor and subscribe to ComputerStatusEvent, but only one is stored. The discarded instance retains its subscription — it is never unsubscribed in ClearEventHandlers (which only iterates the map) and may produce duplicate status events.
Wrap the value in Lazy<T> so the actual construction + subscription happens exactly once:
Proposed fix using Lazy<T>
Change the field declaration (line 44):
-private readonly ConcurrentDictionary<string, RegistryProcessor> _registryProcessorMap = new();
+private readonly ConcurrentDictionary<string, Lazy<RegistryProcessor>> _registryProcessorMap = new();Update the GetOrAdd call (lines 379–384):
-var processor = _registryProcessorMap.GetOrAdd(resolvedSearchResult.DomainSid, _ => {
- var newProcessor = new RegistryProcessor(null, new StrategyExecutor(), resolvedSearchResult.Domain);
- newProcessor.ComputerStatusEvent += HandleCompStatusEvent;
- return newProcessor;
-});
-ret.NTLMRegistryData = await processor.ReadRegistrySettings(resolvedSearchResult.DisplayName);
+var processor = _registryProcessorMap.GetOrAdd(resolvedSearchResult.DomainSid, _ =>
+ new Lazy<RegistryProcessor>(() => {
+ var newProcessor = new RegistryProcessor(null, new StrategyExecutor(), resolvedSearchResult.Domain);
+ newProcessor.ComputerStatusEvent += HandleCompStatusEvent;
+ return newProcessor;
+ })).Value;
+ret.NTLMRegistryData = await processor.ReadRegistrySettings(resolvedSearchResult.DisplayName);Update ClearEventHandlers (lines 89–91):
-foreach (var registryProcessor in _registryProcessorMap.Values) {
- registryProcessor.ComputerStatusEvent -= HandleCompStatusEvent;
+foreach (var lazy in _registryProcessorMap.Values) {
+ if (lazy.IsValueCreated) {
+ lazy.Value.ComputerStatusEvent -= HandleCompStatusEvent;
+ }
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/Runtime/ObjectProcessors.cs` around lines 379 - 383, The current use of
_registryProcessorMap.GetOrAdd with a factory can create multiple
RegistryProcessor instances that subscribe to ComputerStatusEvent and leak if
discarded; change the dictionary to store Lazy<RegistryProcessor> instead,
update the GetOrAdd call to add a new Lazy that constructs the RegistryProcessor
and subscribes to ComputerStatusEvent inside the Lazy factory (using
resolvedSearchResult.Domain and DomainSid), and update any code that reads
processors to use .Value (e.g., when accessing the processor) as well as modify
ClearEventHandlers to iterate the stored Lazy values and unsubscribe from each
Lazy.Value.ComputerStatusEvent (or otherwise access Value once) so that
construction and subscription happen exactly once per key.
Description
Add CompStatus Logging To Registry Processor
RunLog on Collection Failure
RegistryProcessor Unit Tests
Motivation and Context
BED-7374
How Has This Been Tested?
See SHCommon PR #272
Types of changes
Checklist:
Summary by CodeRabbit