The Hybrid Identity Protection Conference (HIP) Europe 2026 in Frankfurt took place last week and I presented my session “Inside Active Directory: Misconceptions, Myths and Bugs”
The session is now available online: Inside Active Directory: Misconceptions, Myths, and Bugs


I would like to start to summarize the first subject I covered in the session – the Security Descriptor Propagation Daemon (SDP) also known as SDPROP. It’s been a misconception for a long time that SDPROP would have been a process/task or in anyway related to AdminSDHolder – it’s NOT other than AdminSDHolder eventually would be one of many consumer of SDPROP e.g. making a change to an objects ntSecurityDescriptor attribute (Modify an objects securityDescriptor)
Here a write up on the subject even on microsoft.com that is completely wrong mixing up AdminSDHolder and SDPROP:
Appendix C: Protected Accounts and Groups in Active Directory | Microsoft Learn
There are 100+ articles, blog posts and presentations that have got this wrong over the years, but I want to highlight a write up that describes AdminSDHolder very well: AdminSDHolder: Misconceptions, Misconfigurations, and Myths – SpecterOps – all credits to Jim Sykora for this write up.
You can also see that I mention this already on this blog in the “How the Active Directory – Data Store Really Works (Inside NTDS.dit) – Part 3” post from 2012.
As my session this post will focus solidly on what SDPROP is responsible for and I will try to cover how it works.
SDPROP runs independently on each DC (Yes, even RODCs) and are responsible for the following:
- Propagates inheritable ACEs in an SD down the tree and merge the SD with parent SD
- More information can be found in the 3.1.1.6.3 Security Descriptor Propagator Update of [MS-ADTS]
- Fix up the Ancestry in the Active Directory database (DIT)
- ResetRDN (This is cause a index is changed used to enumerate children)
- ResetDN
- Lost parent in replication conflict
- Conflict between reference phantom and structural phantom
- Patches GUID-less objects if they have a SID
I did a demo in the session where I used SDPROP to patch up a GUID-less object, on a functional domain controller there should be no need to invoke SDPROP manually, but it can be trigged using a RootDSE Modify Operation – fixupInheritance.
Warning 1:
If you need to make a GUID-less object for testing purposes, you can make one in a dedicated test forest, DO NOT ATTEMPT to create a GUID-less object in a production forest.
Warning 2:
SDPROP can only patch GUID-less object if the object has a SID so this can only be done against a security principal – this is because the GUID is calculated based on the SID – Why is that? Because SDPROP runs independently on each domain controller and they must be able to set the very same GUID.
dn:
changetype: modify
add: schemaUpgradeInProgress
schemaUpgradeInProgress: 1
-
dn:CN=ULF,OU=GUIDLess,DC=dstest,DC=chrisse,DC=com
changetype: modify
delete: objectGUID
-If you want to enqueue a propagation for the entire tree, this can be done by setting ‘fixupInheritance’ to ‘1’, don’t do this in a production forest unless you know what you’re doing.
dn:
changetype: modify
add: fixupInheritance
fixupInheritance: 1
-There is also a possibility to Invoke the SDPROP to work ona specific object, however the object must be identified by DNT. For the concept about DNTs please see – “How the Active Directory – Data Store Really Works (Inside NTDS.dit) – Part 2“
To find an objects DNT without the need to dump the entire DIT, there is since Windows Server 2012 another RootDSE Modify Operation – ‘dumpReferences’ that can be used to translate between DN and DNT for objects. (This is due to the fact that all objects (not phantoms) are keeping a reference to them self by the DN attribute)
dn:
changetype: modify
add: dumpReferences
dumpReferences: CN=ULF,OU=GUIDLess,DC=dstest,DC=chrisse,DC=com
-On the targeted domain controller you will get a text file within the same location as your Active Directory database (NTDS.dit) file, for example C:\Windows\NTDS\ named ntds.ref.dmp – the content should look like.
Non-linked references to CN=ULF,OU=GUIDLess,DC=dstest,DC=chrisse,DC=com
DNT Attribute(s)
5559 distinguishedNameOnes we have obtained the DNT of the object using the self-reference from the DN attribute, we can proceed and trigger SDPROP up-on that specific object using it’s DNT.
dn:
changetype: modify
add: fixupInheritance
fixupInheritance: dnt:5559
-if the object was GUID less you should now see that it has a GUID assigned to it again, also the domain controller should have logged this:

Get-WinEvent -LogName 'Directory Service' -MaxEvents 10 | Where-Object {$_.id -eq '2084'} | ft -Property Message -WrapThis pretty much covers what I did show in the presentation, not let’s deep dive into SDPROP and look at how it works (something you can’t do in a 40 min session)
Implementation in the Active Directory database (NTDS.DIT) – SDPROP implements the following table: sdproptable
| Column Name | Description | Introduced |
|---|---|---|
| order_col | This is the primary key | Windows 2000 |
| begindnt_col | This is the DNT the propagation begins at | Windows 2000 |
| trimmable_col | Indicates if this propagation can be merged with another propagation | Windows 2000 |
| clientid_col | The thread that has enqueued the propagation, if enqueued by the SDPROP thread can the client id would be 0xFFFFFFFF, in all other cases it would be the thread state client id. | Windows 2000 |
| flags_col | This column holds a set of flags, or no flags at all | Windows Server 2003 |
| checkpoint_col | This column contains list of DNT’s (containers) and children to process, as well a list of deadends if the propagation encountered any issues. Note: the duration of the propagation must be long enough for a checkpoint to be saved. | Windows Server 2003 Note: The format changes in SP1 |
| unsafe_ancestors_col | if the propagation hasn’t finished and one or more objects have changed their parent- child relation, this hasn’t yet been reflected in the ancestors_col and therefor a list of “unsafe” ancestors have been saved, that would result in an eventual false-positive parent-child relation. Note: This column is multi-valued and contains each DNT as it’s own value, reading all iTagSequence values is require to get the full list of DNTs Note: The last DNT in the DNT’s list must match the DNT of the ‘begindnt_col’ | Windows Server 2008 |
Here is a screen of how the “sdproptable” looks like when dumped with ESEDump

So is there a way to read the data out of the “sdproptable” from the “outside” using LDAP or any other APIs? Yes, sort of.
You can use the RootDSE attribute “pendingPropagations” as documented here – 3.1.1.3.2.15 pendingPropagations

However this can only retrieve any pending propagations that was caused or enqueued by the same thread as the current LDAP connection, that would match the threads id in the “clientid_col”
The “dSCorePropagationData” none-replicated attribute and it’s relation to SDPROP
The “dSCorePropagationData” get’s updated by SDPROP with timestamps when SDPROP is working on the object and a set of flags associated with the stamps or the object/phantom.
Since Windows Server 2008 a propagation is always enqueued even if the SD hasn’t changed (is bitwise equal to the already existing SD) unless the 14th bit of the dSHeuristics is set aka fDontPropagateOnNoChangeUpdate – as documented here – 6.1.1.2.4.1.2 dSHeuristics -as this is FALSE by default on Windows Server 2008 and later a timestamp is written into “dSCorePropagationData” even if the SD didn’t change.
This attribute has the syntax String(Generalized-Time) – and is multi-valued, where the first value holds the flags and the other values holds a maximum of 4 timestamps, the following flags can be associated with each timestamp or object:
SDP_NEW_SD = 1
SDP_NEW_ANCESTORS = 2
SDP_TO_LEAVES = 4
SDP_ANCESTRY_INCONSISTENT_IN_SUBTREE = 0x08
SDP_ANCESTRY_BEING_UPDATED_IN_SUBTREE = 0x10
if there is a need to write a 5th timestamp into the “dSCorePropagationData” attribute the 2th value is overwritten together with it’s corresponding flags.
You can view this attribute using LDP, it decodes the timestamps and the flags, but do not pair the flags with the time stamps

Looking at the same object using ESEDump:

The flags:
SDP_ANCESTRY_INCONSISTENT_IN_SUBTREE | SDP_ANCESTRY_BEING_UPDATED_IN_SUBTREE signal to Active Directory query optimization code that the ancestors index isn’t safe to use for subtree searches as the SDPROP is working on brining it to a consistent state and that it might would generate false positives.
Security Descriptor (SD) – Single Instance Storage (SIS) in the Active Directory aatabase (NTDS.dit)
It’s time to go into the full possible parameters of the RootDSE modify operation ‘fixupInheritance’ – but before we can do that, we need to understand – single instance storage of security descriptors in the Active Directory database (NTDS.dit) and how it was implemented and introduced with Windows Server 2003.
Before the introduction of the “sd_table” every SD was stored on each object’s row within the “datatable” – directly in the “ATTp131353 / nTSecurityDescriptor” column.

Once the “sd_table” was introduced in Windows Server 2003, SDs was MD5-hased and placed in the “sd_table” instead of the “datatable” – leaving only a reference in the “datatable” pointing to the row storing the SD in the “sd_table”, if a SD to be inserted into the “sd_table” already existed as an existing – already stored SD, that row was referenced instead.

This is of course much more effective – saves space up to 40% according to How to upgrade Windows 2000 domain controllers to Windows Server 2003 – Windows Server | Microsoft Learn, and gives more room for other data to be stored on the row in the “datatable” representing the object or phantom.
So why did we have to touch this to understand more parameters of the RootDSE modify operation ‘fixupInheritance’?
In [MS-ADTS] 3.1.1.3.3.10 fixupInheritance – the following is documented:
In Windows Server 2003 operating system and later, setting the fixupInheritance attribute to the special values “forceupdate” and “downgrade” has effects outside the state model.
The “downgrade” have the effect that all SDs are moved off the “sd_table” and put back into the “datatable” onto each object or phantoms row, as it was in a Windows 2000 style DIT 🙂
So in a fairly modern Active Directory database (NTDS.dit) aka post-Windows 2000 Server – it will look like this for the “ATTp131353 / nTSecurityDescriptor” in the “datatable”

If we look up the SDID reference in the “sd_table” we will find the actual SD

Warning: Do not test or perform this operation is a production environment
Let’s try the RootDSE modify operation ‘fixupInheritance’ with the parameter set to “downgrade”
dn:
changetype: modify
add: fixupInheritance
fixupInheritance: downgrade
-Let’s have a look again at the same object in the “datatable”

Same SD but now stored directly in the “datatable” – I don’t expect you to compare the SDs but they do match 🙂
I don’t think I need to go into what the “forceupdate” parameter is doing.
Some old but good real-word scenarios related to SDPROP and the Active Directory database
I presented this at my first HiP Conf in 2017 in my session – “Inside the Active Directory database (NTDS.DIT)” – That is almost 10 years ago.
Case 1

The funny thing here is a added the -GetRecordSize into ESEDump to troubleshoot this very issue, it calls into the JetGetRecordSize function of the ESENT API.
Now in Windows Server 2025 this is available as an attribute natively – msDS-JetGetRecordSize3 that calls into a later version of JetGetRecordSize than me, JetRecordSize3 don’t seem to be publicly documented yet.

This is how it looks like in ESEDump

Case 2
The SDID reference between the “datatable” and the “sd_table” broke, the SDID stored for that object in the “datatable” pointed to a none-existent row in the “sd_table”

By the way, the database semantic checker would have patched up the GUID-less object as well, but that would have required to take the DC offline.














































