Isn’t the title of this blog post a bit strange? RID Masters and InfrastructureUpdate Objects? Shouldn’t it be Infrastructure Update Masters and InfrastructureUpdate Objects? – It could have been if this blog post was written in the year of 2000 – but it’s 2026 now and things change.
But let us go back to Windows 2000 and have a sort summary of what the IM does
A very detailed explanation (possibly the most detailed that exist) of what the IM or none-GC DCs (If Recycle-Bin is enabled) are responsible for is already available in if you start reading at “Let’s have a look at “Cross Naming Context (NC) references in NTDS.dit” in How the Active Directory–Data Store Really Works (Inside NTDS.dit)–Part 4 – Christoffer Andersson you will find those details.
Summary Windows 2000 – Windows Server 2008
The IM only have something to do in a multiple-domain forest, and only in domains that have at least one none-GC DC, if there is only one none-GC DC in a such forest – it has to hold the IM.
Summary Windows Server 2008 R2 – Windows Server vNext
Exactly the same as up until Windows Server 2008, after that still the same unless if the Recycle-Bin is enabled – If Recycle-Bin is enabled any none-GC DC will handle phantoms (Cross-DB/Domain reference integrity) – hence the IM as a FSMO has no work to do anymore and as each none-GC DC resolves the tasks them self, there is no need to create “carrier” objects in form of InfrastructureUpdates.
The RID Master
So in my session I mention that if the Recycle-Bin is enabled in the forest, the RID Master is the only FSMO that creates InfrastructureUpdate objects now days, but only certain conditions only – the most obvious role for the RID Master is to allocated and handle out RID pools to domain controllers within the domain – but let’s look at the documented description of the RID Master
So the the RID Master is responsible for cross-domain move (XDOMMOVE) aka interdomain moves – this means that a such move only can originate from a domains RID master, this is not a blog post about XDOMMOVE it self- trust me when I say that I’ve tried to write a blog article series about that very subject for several years, it’s one of the most complex working of Active Directory if you ask me – some day I might be able to finish it.
This is how an XDOMMOVE is performed in a multi-domain forest with both GCs and DCs (none-GCs)
1 – The user XDOM resides in D1
2- The user XDOM is moved from D1 (CN=XDOM,CN=Users,DC=D1) to D2 (CN=XDOM,CN=Users,DC=D2) – the move can only be initiated from the RID Master in D1 (D1-DC1)
2- RID Master creates an InfrastructureUpdate object with the attribute proxiedObjectName set to the objects new DN (CN=XDOM,CN=Users,DC=D2) and the binary portion to indicate it’s a moved object and 0 because it’s the first name the object is XDOMMOVE’ed
2 – The DC in the destination domain will create the object and in addition also set the attribute proxiedObjectName to DC=D1 to indicate the object was moved and last time it moved to instance the object it came from the domain D1
2+ – The infrastructureUpdate object replicates out to other DCs/GCs in D1, they will update the name of the object from CN=XDOM,CN=Users,DC=D1 to CN=XDOM,CN=Users,DC=D2 and phantomize the object (record in the database) to a phantom – the record in the database will keep it’s DNT
3 – The object CN=XDOM,CN=Users,DC=D2 now replicates out authoritatively in the forest belonging to D2, once this reaches GCs in D1, the phantomized object representing the moved object to D2 is now promoted back to an object and partial attributes are added – the row in the database will keep it’s DNT
This explain the usage of InfrastructureUpdate objects used by the RID FSMO Master for XDOMMOVE.
Moving the RID FSMO Master must also move the InfrastructureUpdate objects – but way?
So having the requirement of only being able to move objects off from the RID FSMO in the source domain during XDOMMOVE prevents several DCs to move the same object away to different destinations – cleaning that up would be very difficult once replicated out in the forest.
But if you move the RID FSMO from one DC to another DC (From D1-DC1 in Site to D1-DC2 in site2) as in the illustration move, this happens instant using the drsuapi RPC Interface and the GetNCChanges function when called with the extended operation to move the FSMO and the role is transferred instantly without any replication latency.
However we are now again in a dangerous state as the infrastructureUpdate objects that phantomize already XDOMMOVE’d objects takes the normal replication path and are there for subject to replication latency, the RID FSMO could potentially move, already moved objects again to other destinations (domains) – well thankfully this is engineered in such a way that if you move the RID FSMO – during the move all infrastructureUpdate objects having a ‘proxiedObjectName’ is moved within the same operation.
The condition of ‘proxiedObjectName’ is to not move InfrastructureUpdate objects eventually produced by the IM FSMO.
The 3rd undocumented producer of one InfrastructureUpdate object – the fix behind KB810089
So we have to go back in time to understand this one, or at least Pre-Windows 2000 Server SP4 or to be more specific KB810089
My understanding is that this was fixed up in several steps and places
Patched DC – first DC in new domain with a name that has previously existed in the enterprise (forest)
Patched ntdsutil.exe to fix up mangled nCName referances
Patched ntds.dll/ntdsai.dll – the core ds to prevent this from happen at all.
Fix (1.) is implemented in the install/boot code that is called by DCPROMO when you create a new domain within an existing forest, and here the ds devs made a smart move by creating and deleting an InfrastructureUpdate object in CN=LostAndFoundConfig,DC=X, named CN=CRUpdate,CN=LostAndFoundConfig,DC=X with the ‘dNReferenceUpdate’ attribute set to point to the newly created domain’s crossRef objects (via the IDL_DRSAddEntry API using the Directory Replication Service (DRS) Remote Protocol) value of nCName.
Since the InfrastructureUpdate is created/deleted in the same transaction and moved to the Deleted Objects container, it’s guaranteed to replicate in before any corssRef objects in the Partitions container and would reserve the name with a phantom that has name, guid and sid of the newly created domain at replicas in the enterprise (forest) or eventually improve an existing name to resolve a name conflict.
Here is a view of the DIT just pre-restart after promotion of the first DC in a new domain:
Here is the LDP.exe view post-restart after the promotion of the first DC in a new domain:
This is an illustration that might not be correct, but should give an idea:
It’s been very hard to get the correct picture of this, might some one who was part of designing KB810089 can reach out and inform us how it’s really working? How ever I’m sure this is the 3rd case that make use of InfrastructureUpdate objects at least.
This concludes this blog post and the is the final one in the series of blog posts covering the topics from my HiP 2026 Europe session “Inside Active Directory: Misconceptions, Myths and Bugs” – It’s been fun!
Note: When I did spend way to much time on this last 3:rd case I come across another bug in the DS, that isn’t accent – in fact it hasn’t been fixed yet, as I just reported it to Microsoft.
It’s been a while, sorry for the delay – reality and customer demands caught me with after the Hybrid Identity Protection Conference (HIP) Europe 2026 – but this is a follow up on DN references in the DIT and how they work, can logically corrupt the DIT and how a new RootDSE mod can fix those issues while the DS is online.
But let’s summary what it is and the different types, reference integrity is a concept on the Active Directory database layer, and how objects (represented) as rows reference each other by their primary key (DNT), on DCs that are none-GCs – references to objects in other domains are being made up by a phantom to satisfy reference integrity.
Simple references = None Linked attributes
Linked references = Easy to deal with from a DBLayer perspective (because they can be enumerated in the link_table)
Cross-NC references = Either simple or linked references between two objects in different instantiated NCs
Cross-DB references = Either simple or linked references between two objects in different NCs where one is un-instantiated.
Structural Phantoms are a different story (always GUID-less) and created to make up the full DN, in this case “Users” is a such – however if “Users” has been referenced directly earlier it might have a GUID, if “Users” is being referenced directly later (after being created as a structural phantom, it will continue to be GUID-less.)
The “CNT_col” contains the numbers of references that points to the row/object/phantom – it should always be 1 for live objects as the “obj-Dist-Name” attribute point’s to / references the object it self, it’s removed during logical deletion, the object is eventually demoted to a phantom if it’s up for garbage collection but there is still other rows/objects/phantoms in the DIT that points to it, physically removing that row/phantom would cause a logical corruption then DIT and break the reference integrity, once (or if ever) the “CNT_col” reaches “0” it would be physically removed (the row in the database table is removed)
As seen in the below example in “2.” the row representing “Elina Andersson” has been physically removed from the DIT, leaving the reference in “Lena Andersson’s” see-Also’s attribute dangling, this cause the object of “Lena Andersson” to become locally corrupted, trying to expand it in LDP.exe would generate an error -1601 (that is JetErrRecordNotFound) – aka DNT 5524 isn’t found in this case, but theoretically it can be any reference to another record in the DIT.
This post aims to demonstrate what happens when reference integrity fails and how to fix such issues, It has been significantly easier to do so with a new RootDSE mod called “CheckAndFixDNReference” that I did demo during my HIP talk, but let’s first have a look on how an object can become logically corrupted in the DIT due to reference integrity failing as well how that could be fixed before the introduction of the RootDSE mod “CheckAndFixDNReference”
Logically corrupt the DIT offline using ESE APIs How do you cause a corruption to happen? Well for my demo during the session I wrote a special tool to do that, I’m not going to share the exact code, but it looks something like this, to point the see-Also att to a bogus DNT (-2):
Code to o point see-Also to bogus DNT (-2)
// Special code to corrupt "See-Also" refEseHelper.JET_SETINFO jET_SETINFO =newEseHelper.JET_SETINFO();jET_SETINFO.itagSequence=1;jET_SETINFO.ibLongValue=0;jET_SETINFO.cbStruct=Marshal.SizeOf(jET_SETINFO);err =E.Check(EseHelper.JetBeginTransaction(sesid));err =E.Check(EseHelper.JetPrepareUpdate(sesid, tableid,2));int seealsoid =pAC.GetByDisplayName("seeAlso").ID;IntPtr pvBookmark =IntPtr.Zero;int pcbActual =0;// Write a bogus value that don't exist in the DBbyte[] pvDataBytes =BitConverter.GetBytes(-2);// Get the dbData of the tag converted to bytes. int cbData =pvDataBytes.Length;// Allocate our data handle.GCHandle pvDataHandle =GCHandle.Alloc(pvDataBytes,GCHandleType.Pinned);err =E.Check(EseHelper.JetSetColumn(sesid, tableid, seealsoid,pvDataHandle.AddrOfPinnedObject(), cbData,0,ref jET_SETINFO));err =E.Check(EseHelper.JetUpdate(sesid, tableid,out pvBookmark,1024*5,out pcbActual),true);err =E.Check(EseHelper.JetCommitTransaction(sesid,0));pvDataHandle.Free();
Logically corrupt the DIT online on debug/checked ntdsai.dll Okey, but is there any other way to do this without being able to write code on the ESE level and corrupt the DIT that way? Well yes sort of, but still nah – not really.
If you have a debug/checked version of ntdsai.dll – there is a lot of things you can do for troubleshooting and testing purpose, one of them happen to be to logically corrupt the DIT online. (Please not that this is NOT in anyway supported).
However there is another undocumented rootDSE mod that is only available in debug/checked versions of ntdsai.dll – named “genericControl” – this have several different categories of “fun” stuff, one of them being “CorruptDB” – that in it’s turn take 8 different ways of logically corrupt the DIT online (No ESE level database modifications required)
I’m going to describe one of them, because it’s the only one I know ntdsutil can reliability fix, “genericControl” with a value of “CorruptDB:corruptObjTblRef:<DN>” will logically corrupt the object of <DN> by writing (-2) into the object’s objectCategory attribute as shown in the illustration below.
As seen in the illustration above – one the rootDSE mod has been executed, we can’t expand the object anymore, we get the same error message and that (-1601 aka JetErrRecordNotFound) – cause we can’t follow the reference to our objectCategory anymore there is no (-2) DNT in the local DIT.
How can we get some diagnostics for this logically corrupted object?
Having a tool like ESEDump this is easy…
Is there any native tools to use? Yep – ntdsutil.exe and the semantic database analyser – but to get view a specific object using it, we need to know the DNT of the object – bah.
Okey so is there any native tools to do DN to DNT translation online?
Guess what? There is another rootDSE mod for this, but as the last one, only available under debug/checked version of ntdsai.dll
The rootDSE mod “getExtendedObjectData” takes a value of a DN.
Well that didn’t work did it? It’s exactly what it did, inside that NameErr some stuff are encoded.
This is as far as we get online in the absence of “CheckAndFixDNReference” – let’s head over to ntdsutil.exe and the semantic database analyser – this requires that we take the DS offline.
Okey we can get some basic stuff, let’s roll with a “go fixup” to see if ntdsutil finds some issues in the DIT and eventually fix them.
Having a look at the log:
dsdit.dmp
Removing Non-existent references:Object 6526 has reference (colid 1364) to non-existent DNT -2. Removed
Let’s bring the DS online again and look if we can access the object “GOOSE” again.
Yes, we can now successfully access the object “GOOSE” again, however something is missing – let’s leave it as that, but comment if you can figure what’s missing.
That was one way of dealing with the problem before
It’s 2026 so let’s take another approach with – the “dumpReferences”, “dumpDatabaseExtended” and “CheckAndFixDNReference” rootDSE mod
Well there is a lot of things that don’t get any easier, and that is to determine where on a object we have a dangling DN reference – the best we can do is to get all attributes with such syntax over the given object, or we could reduce that a bit to only the attributes possible for the objects given class (How-ever the logical corruption could still be on a attribute that was once valid for the given class, but is no longer)
Let’s have a look again on a logically corrupted object, we can’t expand the object anymore, we get the same error message again (-1601 aka JetErrRecordNotFound) – most likely some reference issue.
Now let’s try another approach to get the DNT of this object, that won’t require a debug/checked version of ntdsai.dll.
dumpReferences
dumpReferences is a rootDSE mod available since Windows Server 2012 that will dump all none-linked attributes and objects containing a references pointing to a given object’s DN but written as a text file to the DCs disk with the DNT instead of the DN. Since all objects have the obj-Dist-Name (distinguishedName) pointing towards them self, we gan convert from DN to DNT
So let’s get the DNT of “CN=GOOSE,OU=Corrupt,DC=ese,DC=nttest,DC=chrisse,DC=com”
Opening the “ntds.ref.dmp” file manually will look something like, depending on numbers of object pointing towards the object you dumped the DN for:
But in our case for “CN=GOOSE,OU=Corrupt,DC=ese,DC=nttest,DC=chrisse,DC=com” – it happened to look like:
We do know now that “CN=GOOSE,OU=Corrupt,DC=ese,DC=nttest,DC=chrisse,DC=com” is equal to the DNT “5228” without having a debug/checked version of ntdsai.dll. Knowing the DNT is required for the next step
dumpDatabaseExtended
So now when we got the DNT of the troublesome object that seems to have reference integrity issues caused by a logical corruption as the object “CN=GOOSE,OU=Corrupt,DC=ese,DC=nttest,DC=chrisse,DC=com” can’t be read we’re going on a chase for that DN syntaxed attribute that have a dangling DNT reference to a none-existent object/phantom/row in our DIT.
This can be done in a few different ways using the rootDSE dumpDatabaseExtended mod that is a bit poorly documented at 3.1.1.3.3.41 dumpDatabaseExtended I did decode/reverse it so let’s document it a bit more complete here – the format documented is
LDIF sample
dn: changetype: modify add: dumpDatabaseExtended dumpDatabaseExtended: 01:01:*:description sn -
Mentioning “The effects of dumpDatabaseExtended are outside of the state model. An update of dumpDatabaseExtended causes part of the contents of the Active Directory DCs database to be written to a text file on the DCs disk.
The format is like this:
XX:YY:OO: XX = 00 = both obj och phantoms (all records) XX = 01 = IsDeleted XX = 02 = only phantoms YY = The index to be used, see YY list OO = Only “*” supported – but supporting a DNT or DN here would have been very nice
YY as following: 00 – “DNT_index” – “Index over DNT_col” 01 – “PDNT_index” – “Index over PDNT_col and ATTm589825 (RDN)” 02 – “Ancestors_index” – “Index over Ancestors_col” 03 – “deltime_not_recycled_index” – Index over “time_col” 04 – “DRA_USN_CRITICAL_index” – “Index over NCDNT_col, ATTq131192 (uSNChanged), ATTi590692 (isCriticalSystemObject) and DNT_col” 05 – “DRA_USN_index” – “Index over NCDNT_col and ATTq131192 (uSNChanged)” 06 – “exprocesslinks_index” – “Index over extended extendedprocesslinks_col used by delayed tasks on the object or links that the object are part of” 07 – “nc_guid_Index” – “Index over NCDNT_col and ATTk589826 (objectGUID)” 08 – “PhantomIndex22” – “Index over time_col” 09 – “recycletime_index” – “index over recycle_time_col” 10 – “INDEX_00000000” – “Index over ATTc0 (objectClass)” 11 – “INDEX_00090062” – “Index over ATTj589922 (primaryGroupID)” 12 – “INDEX_000904E1” – “Index over ATTh591073 (proxiedObjectName)” 13 – “INDEX_000200D2” – “Index over ATTm131282 (proxyAddresses)” 14 – “INDEX_00090001” – “Index over ATTm589825 (RDN)” 15 – “INDEX_00090092” – “Index over ATTr589970 (objectSID)”
Bonus if (08) – “PhantomIndex22” index and (03) – “deltime_not_recycled_index” looks the same, they are not – in their ESE index definition they have something called a conditional column so in addition:
PhantomIndex22 has two conditional columns on objectGUID being non-null and obj-Dist-Name being null – this filters out structural phantoms and ensure no objects are included, hence the null requirement for the obj-Dist-Name.
deltime_not_recycled_index has one conditional column on recycle_time_col being null.
Get all DN-syntaxed attributes for the user objectClass
$SchemaPath = (Get-ADRootDSE).schemaNamingContext# 1. Get all attributes that have a DN-related syntax (2.5.5.1, 2.5.5.7, 2.5.5.14)$DNSyntaxOIDs =@('2.5.5.1','2.5.5.7','2.5.5.14')$DNAttributes =Get-ADObject-SearchBase $SchemaPath -LDAPFilter "(&(objectClass=attributeSchema)(!linkid=*)(|(attributeSyntax=2.5.5.1)(attributeSyntax=2.5.5.7)(attributeSyntax=2.5.5.14)))"-Properties ldapDisplayName, attributeSyntax$DNAttrNames = $DNAttributes.ldapDisplayName# 2. Resolve User class inheritance tree and auxiliary classes$CurrentClassName ="user"$ValidClassNames = [System.Collections.ArrayList]@()while ($CurrentClassName) { $ClassObj =Get-ADObject-SearchBase $SchemaPath -Filter "lDAPDisplayName -eq '$CurrentClassName'"-Properties subClassOf, auxiliaryClass, systemAuxiliaryClass, mayContain, mustContain, systemMayContain, systemMustContainif (-not $ClassObj) { break } [void]$ValidClassNames.Add($ClassObj.lDAPDisplayName)# Collect container attributes for this level of the class hierarchyforeach ($prop in@('mayContain','mustContain','systemMayContain','systemMustContain')) {if ($ClassObj.$prop) { foreach ($val in $ClassObj.$prop) { [void]$ValidClassNames.Add($val) } } }# Check auxiliary classes if presentforeach ($aux in@('auxiliaryClass','systemAuxiliaryClass')) {if ($ClassObj.$aux) { foreach ($val in $ClassObj.$aux) { [void]$ValidClassNames.Add($val) } } }if ($ClassObj.subClassOf -eq $CurrentClassName) { $CurrentClassName =$null } else { $CurrentClassName = $ClassObj.subClassOf }}# 3. Intersect valid attributes for the user class and STORE in a variable$UserDNAttributes = $DNAttributes |Where-Object { $ValidClassNames -contains$_.ldapDisplayName } |Select-Object@{N="AttributeName";E={$_.ldapDisplayName}},@{N="SyntaxOID";E={$_.attributeSyntax}} |Sort-Object AttributeName# 4. (Optional) Display the contents$UserDNAttributes
Or you can choose to get all DN-syntaxed attributes in the schema
Get all DN-syntaxed attributes in the schema
# 1. Get all variations of DN syntax (2.5.5.1, 2.5.5.7, 2.5.5.14)$SchemaPath = (Get-ADRootDSE).schemaNamingContext$DNFilters ="(attributeSyntax=2.5.5.1)(attributeSyntax=2.5.5.7)(attributeSyntax=2.5.5.14)"$UserDNAttributes =Get-ADObject-SearchBase $SchemaPath `-LDAPFilter "(&(objectClass=attributeSchema)(!linkID=*)(|$DNFilters))"`-Properties ldapDisplayName, attributeSyntax |Select-Object@{N="AttributeName";E={$_.ldapDisplayName}},@{N="SyntaxOID";E={$_.attributeSyntax}} |Sort-Object AttributeName# 2. (Optional) Display the contents$UserDNAttributes
So now try to use the “dumpDatabaseExtended” to get all the DN-syntaxed attributes in the DIT and try to find DNT 5228 (“CN=GOOSE,OU=Corrupt,DC=ese,DC=nttest,DC=chrisse,DC=com”)
Call the dumpDatabaseExtended rootDSE mod
# 1. Get all variations of DN syntax (2.5.5.1, 2.5.5.7, 2.5.5.14)$SchemaPath = (Get-ADRootDSE).schemaNamingContext$DNFilters ="(attributeSyntax=2.5.5.1)(attributeSyntax=2.5.5.7)(attributeSyntax=2.5.5.14)"$UserDNAttributes =Get-ADObject-SearchBase $SchemaPath `-LDAPFilter "(&(objectClass=attributeSchema)(!linkID=*)(!ldapDisplayName=*WellKnownObjects)(|$DNFilters))"`-Properties ldapDisplayName, attributeSyntax |Select-Object@{N="AttributeName";E={$_.ldapDisplayName}},@{N="SyntaxOID";E={$_.attributeSyntax}} |Sort-Object AttributeName# 2. Call the 'dumpDatabaseExtended' rootDSE mod$RootDSE = [ADSI]"LDAP://RootDSE"$RootDSE.Put("dumpDatabaseExtended", [String]::Format("00:15:*:{0}", [String]::Join(" ",$UserDNAttributes.AttributeName)))$RootDSE.SetInfo()
Now let’s trim the output to find DNT 5228 (“CN=GOOSE,OU=Corrupt,DC=ese,DC=nttest,DC=chrisse,DC=com”)
So we do now have a a list of all DN-syntaxed none-linked attributes for DNT 5228 (“CN=GOOSE,OU=Corrupt,DC=ese,DC=nttest,DC=chrisse,DC=com”) and we can start to investigate if we think we have any of them pointing towards a none-existent DNT in the DIT.
Let’s have a look here:
DNT 9696 looks suspicious – so why is that?
Because the highest DNT allocated in our DIT is 5307, we can get this online via the operational attribute approximateHighestInternalObjectID, but also just scroll to the end of the ntds.dmp file we dumped with the rootDSE mod ‘dumpDatabaseExtended’
But let’s search the dit.dmp for the DNT 9696 so be sure
CheckAndFixDNReference is a rootDSE mod available for Windows Server 2019, Windows Server 2022 and Windows Server 2025 depending on the patch level. It’s capable of fixing up reference integrity issues such as dangling DN references online – but is limited to syntaxes (2.5.5.1, 2.5.5.7, 2.5.5.14) on none linked attributes (cause linked-attributes rarely suffers from reference integrity issues).
CheckAndFixDNReference attribute is supported in Windows Server 2019 operating system with [MSKB-5078752], Windows Server 2022 operating system with [MSKB-5078766], Windows Server 2022, 23H2 operating system with [MSKB-5078734], Windows Server 2025 operating system with [MSKB-5068966] and later.
So now we have everything to use ‘CheckAndFixDNReference’ to fix up the CN=GOOSE,OU=Corrupt,DC=ese,DC=nttest,DC=chrisse,DC=com object that has a dangling DN reference in the ‘seeAlso’ attribute as we have discovered above.
However the ” support two operation modes
0 – Check verify that the dn syntaxed none-linked attribute reference cause a logical database corruption, if there is a reference to a none-existing row (object or phantom) it will be reported by an event logged in the Directory Services log.
1- Fix the logical database corruption by removing the value of the dn syntaxed none-linked attribute, the invalid reference from the database.
# Define the query parameters in a hashtable for optimized filtering$FilterHashtable =@{ LogName ='Directory Service' Id =3091}try {# Retrieve the single most recent event matching the ID $LatestEvent =Get-WinEvent-FilterHashtable $FilterHashtable -MaxEvents 1-ErrorAction Stop $LatestEvent.Message}catch [System.Exception] {Write-Warning"No event with ID 3091 was found in the Directory Service log, or the log could not be accessed."}
We can now see that this is indeed a logical corruption where the ‘seeAlso’ attribute points to a DNT that don’t exist in the database ‘9696’
Now let’s run ‘CheckAndFixDNReference’ in fix mode to have the invalid reference that cause the logical corruption removed.
Now let’s get the 3091 event again from the Directory Services log:
We can see that the ‘Operation Mode’ this time is ‘Fix’ – and we can now also verify that our object is accessible and no longer causing a logical corruption in the database (DIT)
Thank you for reading and if you did follow the entire post to the end, it turned out to be long and might with to much details.
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”
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)
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.
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.
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.
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)
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.
ntds.ref.dmp
Non-linked references to CN=ULF,OU=GUIDLess,DC=dstest,DC=chrisse,DC=comDNT Attribute(s)5559 distinguishedName
Ones 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.
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 GUID fix event
Get-WinEvent-LogName 'Directory Service'-MaxEvents 10|Where-Object {$_.id-eq'2084'} | ft -Property Message -Wrap
This 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.
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.
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”
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.
I tried to dump a NTDS.dit from a RODC with ESEDump – something I haven’t done in years and just stumbled up on the fact that i got NDNC’s (None-Domain Naming Contexts) appearing twice in ‘msDS-HasInstantiatedNCs’ – ESEDump did work as expected but I started to question my self if my code walking the range in the “link_table” worked correctly.
I then thought that this must have something to do with the fact that this RODC is promoted from IFM. Let’s have a look on that attribute with repadmin.exe
Yep, I but those are my two ‘duplicated’ NDNCs – but why? I wrote two articles in the past about how IFM is working and did almost cover this but missed it in the first part as it solidly was focusing on Windows Server 2003 – it says:
Sourcing NDNCs with Windows Server 2003 is only supported by Windows Server 2003 SP1 or later under the following conditions:
Both the DC your souring the IFM from must be running at least Windows Server 2003 SP1 or later and as well the machine intending to become a DC using the source IFM.
The forest functional level (FFL) has to be: Windows Server 2003 (Pre-Windows Server 2003 FFL adding replicas to NCs has to be done on the Domain Naming Master – FSMO) Note: The promotion completes with the sourced IFM even if the forest functional level (FFL) is less than Windows Server 2003 but NDNCs aren’t sourced from the IFM and the following will happen:
The DomainDNSZones and ForestDNSZones are begin replicated in again over the wire using normal replication, as the promoted DC (Sourced from IFM) hosts the DNS Service
I forgot to mention that it’s not supported to keep any NDNCs in the DIT for the Red-Only Domain Controller IFM case – those get wacked and replicated back in again.
The solution here if I really only want to get PRESENT links would be to change incides over the “link_table” to the “present” ones depending on fwd o back links, the Recycle-Bin was not enabled in this environment.
All DIT’s on Windows Server 2025 isn’t 32k page size (Yes I know – I did say that in my last blog post), there is two exceptions, 1: the IPU case aka In-place-upgrade of a pre-Windows Server 2025 DC, in that case the DB will remain as-is in terms of page size.
You can verify the actual page size using a new attribute msDS-JetDBPageSize to determine the page size of a Windows Server 2025 DSA.
This clearly show us that the page size of the one and only DSA running Windows Server 2025 is 8k – this is because the DIT comes from a an IPU’ed aka in-place-upgraded Windows Server 2022 DC.
Let’s have a look with esentutil /m C:\Windows\NTDS\NTDS.dit
For sure the DIT is 8k page size, we can also validate by it’s DB Signature that it was created back in 2017, we can compare with the Distribution DIT that is 32k page size and created in 2023 – more on the Distribution DIT can be found here.
So how do we get to 32k page size on this DC? The answer is you can’t unless you demote and re-promote it. You can get to Windows Server 2025 DFL and FFL – but you can’t enable the “Database 32k pages feature”
There is is a 2:nd way to get up additional DCs running Windows Server 2025 with 8k page size DIT’s.
If you produce IFM media of a Windows Server 2025 DC with 8k page size – the Windows Server 2025 DCs you promote using that IFM media also going to have 8k page size of their DITs.
IPU-DC-02 is promoted using a IFM produced off IPU-DC-01 that once was IPU:ed (In-place-upgraded) from Windows Server 2022.
Extensible Storage Engine – ESENT and the Engine Format Version
Some where after Windows 19H1 version 1903 – released in May 2019 and within Windows 20H1 version 2004 – ESENT seem to have got support for something called “EngineFormatVersion” that enables specific features depending on the ESE Engine version – by default if a ESE DB is attached on a more recent engine than the ESE DB was initially created on, it and it’s logs are upgraded to the current ESE Engine version, that might break backwards compatibility to be able to attach the ESE DB by a older version of the ESE Engine (older version of the operating system with an ESE Engine only supporting certain EngineFormatVersions) – this is by default aka:
ESENT.h
#defineJET_efvUseEngineDefault (0x40000001) // Instructs the engine to use the maximal default supported Engine Format Version. (default)
How ever the Active Directory DSA (ntdsai.dll) have decided to support databases (DIT’s) that don’t even support the efv (Engine Format Version) by setting a hard version of:
ESENT.h
#defineJET_efvWindows19H1Rtm8920 // Last pre-efv version, shipped in Windows 10 until 19H1 release.
This sort of came up and got awareness to the outside world by Michael Grafnetter when someone used his incredible tool dsinternals to attach to a DIT and the default – JET_efvUseEngineDefault was used, it upgraded the DIT and when the Active Directory DSA (ntsdsai.dll) would try to attach to it again it would have a efv (Engine Format Version) that was way head of it’s hardcoded JET_efvWindows19H1Rtm
Ops the DIT can no longer be used by the Active Directory DSA (ntsdsai.dll).
ESENT error -623 is JET_errEngineFormatVersionSpecifiedTooLowForDatabaseVersion
This was resolved by setting the JetSetSystemParameter JET_paramEngineFormatVersion to JET_efvUsePersistedFormat before attaching the database – Michael figured out that this was how the esentutil would do it. DsIntetrnals got updated and I updated my ESEDump tool pretty much the same way:
ESEDump source
if (DbInfoMisc.dwBuildNumber>=20348) err =E.Check(EseHelper.JetSetSystemParameter(ref instance,EseHelper.JET_sesidNil,newIntPtr(EseHelper.JET_paramEngineFormatVersion),newIntPtr(EseHelper.JET_efvUsePersistedFormat),null));
What happened to NTDSUTIL.exe and 8k size DITs?
For some reason if you’re using ntdsutil.exe against a 8k size DIT on Windows Server 2025 and it attaches your DIT – It’s going to destroy your dit and your DC will never boot again – guess what? It upgrades your DB beyond efv (Engine Fromat Version) 8920 (JET_efvWindows19H1Rtm) – This is NOT good and is another bug in Windows Server 2025.
If you’re on Windows Server 2025 with 8k page size DITs – stay away from ntdsutil.exe and especially the file context, it will destroy your DIT. Or to be honest stay away from 8k page size DITs on Windows Server 2025 to being with.
Update 2025-11-07: Microsoft responded quickly and seriously to this issue and a fix is already on it’s way, also note that the DIT is not corrupted in anyway so that data is NOT lost, it’s just upgraded to a later version and the DSA is refusing to attach the DIT as Active Directory don’t support that ESE evf (Engine Fromat Version)
So what is going on here – a bad exchange schema upgrade that would cause replication to fail? At first you can think that would be the case – it’s NOT according to me, but I would hold on that the exchange schema upgrade do something bad [1] – let’s get back to that later.
So when can this disaster strike – if you’re running Exchange adprep to extend your Active Directory schema for the first time, aka the forest had no previous versions of Exchange schema extensions – nope nothing happens regardless of the versions of your DCs, Including the Schema FSMO – running Windows Server 2025.
If you run adprep and ALREADY have extended your schema for Exchange previously and the Schema FSMO would be running Windows Server 2025 – then you would run into issues – But what really happens?
Attributes with the syntax 2.5.5.2 aka String(Object-Identifier) suddenly takes duplicates – here it’s time to tell what exchange is doing bad [1] – Exchange adprep never check if a value is already present in those attributes, instead on each update (CU/Release) it’s trying to add the same value again, relaying that AD throw’s them out with ATT_OR_VALUE_EXISTS – below we can see how that works when the Schema – FSMO is held by a Windows Server 2008 R2
But this will be allowed on Windows Server 2025?
That is not good – and it will break Active Directory replication on the receiving end if it’s a down-level DC (e.g. not Windows Server 2025 [2]) the update will only apply one of the duplicate e.g. the duplicate detection will work, so if we’re looking up our example class here ‘Address-Book-Container’ between the schema FSMO and another DC.
Why will it not break replication to other Windows Server 2025 DCs? Well they suffer from the same duplication bug – or do they really? [2]
This is how it would look like on a down-level DC (e.g. not Windows Server 2025)
And of course it’s a schema mismatch when the definition of a class is different between two replocas, one with two values for the attribute auxiliaryClass (2) country;country while the other only has auxiliaryClass (1) country;
Well let’s go a step beyond this and leave Exchange for a while, let’s add our own attr (chDsObjid) with the syntax 2.5.5.2 aka String(Object-Identifier)
Windows Server 2025 DC
Windows Server 2022 DC
So the same behavior – however this will not cause a schema mismatch and break replication, only bring inconsistency.
So what is wrong here, something most be wrong with Active Directory’s duplication logic on Windows Server 2025 [2] or?
Let’s again look at how it should work – we should be thrown out if where trying to add a value that already exists on a attribute with syntax 2.5.5.2 aka String(Object-Identifier)
We are and it works as expected on down-level DC (e.g. not Windows Server 2025) – We get a DSID – that can give us a pointer to where this is blocked in the ds source. I happen to know that AD has it’s own detection for duplicates values for certain syntaxes, but not for 2.5.5.2 aka String(Object-Identifier) – here Active Directory solidly relays on Extensible Storage Engine – ESENT / Jet for value duplication detection.
JetSetColumn is called with grbit JET_bitSetUniqueMultiValues = 0x00000080 – this seems to fail on Windows Server 2025? [2]
I felt I had to try out this my self on the Extensible Storage Engine – ESE level so what the heck how hard can it be to modify ESEDump to do writes to the DIT:
So let’s try this and see if we’re thrown out by ESE – if we first try with a Windows Server 2022 DIT (8k)
Yes that works as expected, let’s try the very same code now on a Windows Server 2025 DIT (32k)
Ops – Something must be wrong within Extensible Storage Engine -ESE (ESENT.dll) – we’re getting through here even if we’re calling JetSetColumn with grbit JET_bitSetUniqueMultiValues and a value that is already present in the column.
But let’s try a DIT from Windows Server 2022 (8k) on Windows Server 2025 🙂 We’re thrown out with ESENT error: A duplicate value was detected on a unique multi-valued column.
But wait wasn’t Windows Server 2025 broken or had some defect in ESENT.dll?
My understanding is that this has to do with the page size of the NTDS.dit database and that JET_bitSetUniqueMultiValues don’t work correctly on 32k pages DBs and has never done, no matter of the underlaying operating system, it just happen to be that on Windows Server 2025 all NTDS.dit databases are 32k pages.
But wait shouldn’t the database be 32k pages first when the “Database 32k pages optional feature” has been enabled? No, again all NTDS.dit’s on Windows Server 2025 is 32k page by default, the “Database 32k pages optional feature” only let go of restrictions enforced to be able to co-exist with downl-level replicas (e.g. none Windows Server 2025 DCs)
[2] The issue has nothing to do with Active Directory – it seems to be a bug in Extensible Storage Engine – ESENT (ESENT.dll)
Summary
It took me a day to figure out and test this, including writing a version of ESEDump that could prove this. Exchange should look for the values they are trying to add and not relay on Active Directory throwing an error that the value already exists.
How can you find effected attributes, run the following AD query against your Schema NC:
It is possible to fix this if you run into it – without during a forest recovery. Please contact Microsoft Support and they will help you to get back in a supported manner, I know how to get out of this as well – but I’m not Microsoft nor do I work for Microsoft, I’m just a Active Directory geek that like to figure out how things work, or not work.
This blog post will describe and go into details about the may not so known Active Directory Database Epoch / Copy Protection.
This concept that was introduced with Active Directory Application Mode) ADAM initial release (Now follows some ADAM history), but never made it to Active Directory Domain Services (AD DS) until Longhorn/WS2008 for some good reasons. One of the changes with Windows Server 2008 was that AD got exposed as a windows service allowing admins to stop, restart and start the service on DCs, this behavior already existed since day one in Active Directory Application Mode (ADAM ) first introduced as a standalone package to the web in November 2003 and was also targeting Windows XP, in Windows Server 2003 R2, Active Directory Application Mode (ADAM) Service Pack 1 (SP1) is included as a windows component (On CD2) but still ship as a download for other operating systems, Active Directory Application Mode (ADAM) Service Pack 2 (SP2) is the latest version to ship except some QFEs and Security Updates before the source code of Active Directory Application Mode (ADAM) merges into the Directory Service (DS) source depot, integrates into windows builds and get’s available again with Windows Server 2008/Windows 7 rebranded as Active Directory Lightweight Directory Service (AD LDS) as an installable role with the operating system – no more downloads are available.
The potential issues and damages that this feature is trying to protect from, given the above.
Pre-Windows Server 2008 and ADAM it wasn’t that easy to manually restore or replace the database (DIT) using none supported restore methods for let’s say average sysadmins – you needed to boot into DSRM – Directory Services Restore Mode cause the DB was locked by LSASS/ESE and the database by default was located under C:\Windows folder.
Now with these requirements gone as peer Windows Server 2008 and ADAM/AD LDS – Microsoft wanted to prevent some scenarios:
Potentially foreseen scenarios:
• Stop service, copy off database, restart service, make changes that replicate, stop service, copy old database back in.
• Stop service, copy off database from instance1, stop second service, copy database over data files for instance2.
Both these scenarios breaks the Active Directory replication model, because two different/distinct changes could get the same <OriginatingInvocationID>:<OriginatingUSN> pair (lets call this the ChangeID). If two changes have the same ChangeID, one of those two changes would fail to replicate, because the DSA will claim to have “seen” the change with the same ChangeID, from the previous instance of the database, and fail to replicate the new change with this ChangeID. Also, other partners of this instance, will assume they’ve seen any new changes made that match previous changes this DSA has replicated out.
The implementation – a database epoch stored in both in the database (DIT) and the registry, during initialization of the DSA and the DB a random value is written in case of a none-existent epoch or the current epoch +1 is written both to the database (DIT) – more specifically to the “epoch_col” column in the hiddentable and the “HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters\ DSA Database Epoch” Registry DWORD.
Rules are as following
If both the registry and the database have NULL – it’s considered a match. The epoch is initially set with rand() in both the database and the registry
if the value stored in the database is > than the value stored in the registry – it’s considered a match.
If the value stored in the database and the registry match – it’s considered a match
If either 2 or 3 the epoch is advanced by 1 in both the database and the registry, if any of the updates fail the ESE update to the DIT is rolledback.
If the “HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters\ Disable DSA Database Epoch Check” Registry DWORD is set to 1 it’s considered a match.
If the values do not match as per above 1-3, a “restore” is forced to get a new invocationID for the DSA/DRA and the following event is logged:
Table 1: Epoch mismatch and restore initiated.
Event ID
Source
Category
Description
2524
ActiveDirectory_DomainService
Backup
The Directory Server detected that the database has been replaced. This is an unsafe and unsupported operation. User Action: None. Active Directory Domain Services was able to recover the database in this instance, but this is not guaranteed in all circumstances. Replacing the database is strongly discouraged. The user is strongly encouraged to use the backup and restore facility to rollback the database.
The “restore” is forced by writing/setting the “state_col” in the “hiddentable” to “4” aka “BackedupDIT” as well “uns_col” to next USN-1 and “backupexpiration_col” to the next day.
If there is any other failure than retrieving the epoch from the database than that the column is null/nonexistent or that the registry value is nonexistent – the DSA is going to fail init and stop hard with the error message:
Table 2: Epoch mismatch fatal
Event ID
Source
Category
Description
2542
ActiveDirectory_DomainService
Backup
The Directory Server detected that the database has been replaced. This is an unsafe and unsupported operation. The service will stop until the problem is corrected. User Action: Restore the previous copy of the database that was in use on this machine. In the future, the user is strongly encouraged to use the backup and restore facility to rollback the database. This error can be suppressed and the database repaired by removing the following registry key. Additional Data Registry key: System\CurrentControlSet\Services\NTDS\Parameters Registry value: DSA Database Epoch
Note that this feature could have been implemented differently, technically there is no need to change/advance the epoch each time during init, not even during an originate write to the database (DIT) – however it must only really change if a new originate write is replicated off the local DSA.
I wrote this blog post because I got a question – “So what happens when distribution DIT is mounted” – If you don’t know what the distribution DIT is you can read about it here: https://blog.chrisse.se/?p=1005
The answer to the question can be figured out by reading this post (Rule 1 above), The answer explained is that the “epoch_col” is NULL in the Distribution DIT and once the DSA Initialize on the Distribution DIT for the first time the registry value don’t exist and peer above that is considered a match, a random value is written as the initial epoch to both the database (DIT) and the registry on the DSA.
Bonus: the “state_col” of a distribution DIT should be “1”.
It will NOT remove FAS (Filtered Attribute Set) from the link_table? Can you even have that? Sure in the “link_data” column for the string/data portion of a linked attribute with syntax DN-String or DN-binary: https://learn.microsoft.com/en-us/windows/win32/adschema/syntaxes
Dumping the “link_table” from a Windows 2000 Server DC just because there are only a few initial columns in the table initially at Windows 2000 DCs and I have all kinds of DIT’s around for testing when I write tools.
So now we’re coming to the scrubbing part – Let’s say you used the VSS API and the NTDS Writer with the “RODC_REMOVE_SECRETS;” _AND_ cleaned up any potential linked FAS attribute that stored it’s data in the ‘link_data” column by your own.
Would the NTDS.DIT be secure? Nope – you need to scrub your DIT so it becomes secure. You just call into esent.dll?JetDBUtilitiesW and ask for a scrub operation (opDBUTILEDBScrub) to take place, if I say securing the DIT instead of scrubbing, sounds more familiar to you?
So why is this done? Well the “NTDS Writer” with the “RODC_REMOVE_SECRETS;” just call regular jet/esent APIs to delete the columns/attributes that contained hidden, secret or FAS data – and that is what you’re doing in the “link_table” as well but it’s more complicated as we can’t drop the entire column, instead the value for a specific row representing the linked attribute need to reset the data in the “link_data” column using for example JetSetColumn | JetSetColumnDefaultValue
Regardless of the above, there is no guarantee that all metadata and left over data isn’t still present somewhere in the database – hence the need to secure / scrub the DIT.
By the way you don’t need to write up your own code and call into esent.dll?JetDBUtilitiesW you can just use the undocumented option “Z” of esentutl.exe that has been there since Windows Server 2008:
How deletion/removal of data really works in Active Directory This is the fourth post in a series of articles that will describe what’s really inside NTDS.dit and how Active Directory works on the database layer, the past three articles has been about:
Explains the tables within NTDS.dit in detail as far as what they are used for, in which release of Active Directory (Windows Server) they were introduced in, as well any major changes being added in later versions: How the Active Directory – Data Store Really Works (Inside NTDS.dit) – Part 1
Explains how the object tree hierarchy is maintained at the database layer and the concept of DNTs and PDNTs and how they make up the relation between parent and descendent objects: How the Active Directory – Data Store Really Works (Inside NTDS.dit) – Part 2
Explains the “Ancestors_col” that supports subtree searches and the security propagation (SDProp) – Part 3
This blog article will not cover the following scenarios:
Deletions of DSAs (Domain Controllers) cause it involves SAM and NETLOGON
The Foreign-Security-Principal Cleanup task – as it cleans and removes duplicate SIDs that only can end up in your directory in very rare scenarios and involves SAM – to – AD Upgrades.
This blog article wouldn’t have been possible without:
The ESEDump utility that I’ve developed together with my very good friend Stanimir Stoyanov – His contribution to the code has been invaluable.
Countless of sleepless nights and the support from my team at Enfo Zipper.
What you need as a prerequisites before reading this blog article:
Basic understanding of Active Directory at the database layer, that can be found at this blog:
Basic understanding of deletions in Active Directory
When an object is deleted in Active Directory (this is referred to as a logical deletion), the object is not immediately removed from Active Directory, and instead the object is flagged/marked for deletion by defining two or three attributes on the object depending on if the Active Directory feature “Recycle-Bin” is enabled and if there is Windows Server 2008 R2 or later DSAs that host a writeable instance of the object – see ‘Table 1.’
There are a few exceptions where deletions are trying to be physically deleted immediately (the row representing the object is being deleted from the database) without ever transit to the intermediate state of being a tombstone, deleted object or recycled object:
Dynamic-Objects, object that have the auxiliary class dynamic-Object present – those are physically removed once their time-to-live has passed; eventually they remain as phantom’s in the database until all references pointing towards them have been cleared.
Placeholder object’s that are in the distribution NTDS.dit – Deleted by DCPROMO.
A read-only naming context (NC) is being removed from a replica (e.g. a Global Catalog (GC) demotion)
Removal of lingering objects – using the operational attribute ‘removeLingeringObject’: http://msdn.microsoft.com/en-us/library/cc223303(v=prot.10).
Removal of an object as a result of being cross-domain moved.
Table 1: Logical deletion sets the following attributes
Attributes
Recycle Bin State
Minimum DSAs Versions Present
IsDeleted lastKnownParent
Off
Windows 2000 Server (All versions) Windows Server 2008 (All versions) Windows Server 2008 R2 – Read Only DC** Windows Server 2012 – Read Only DC**
** Originated deletions on a RODC such as “Connection Objects” that is writable 0x4 has “isRecycled” as well.
IsDeleted IsRecycled lastKnownParent
Off
Windows Server 2008 R2 (All versions) – Writable DC Windows Server 2012 (All versions) – Writable DC
IsDeleted lastKnownParent msDS-LastKnownRDN
On
Windows Server 2008 R2 (All versions)
Windows Server 2012 (All versions)
Once objects are being logically deleted and transformed into tombstones, they lose most of their attributes – unless the Recycle Bin is enabled (and is as a result of a logical deletion being transformed into a “deleted object” rather than a tombstone).
The following attributes are always preserved (during a logical delete, regardless the state of the Recycle Bin) until the object (row in the database) is either
Physically deleted – (The object isn’t referenced by any other object within the database) the row representing the object can now safely be deleted.
The object (row) remains represented as a phantom to satisfy reference integrity (e.g. there is other objects (rows) in the database that reference the object and prevent the row from being physically deleted). Please see Phantoms and reference integrity below.
Additionally the attribute that serves as the RDN of the object being logically deleted is also always preserved. (E.g. CN/O/OU/DC)
Attributes that in the schema are defined to be preserved on deletion by having their searchFlags attribute containing bit 8 (0x00000008 = fPRESERVEONDELETE) are also preserved during a logical delete, unless they are linked attributes or a constructed attribute that drives it’s data from an attribute that is also not preserved.
Linked attributes are removed (Both forward links and backlinks) on logical deletions unless if the Recycle-Bin is enabled, then the links are deactivated instead.
To view deactivated links if the Recycle-Bin is enabled above the database layer, the following control can be used:
Table 2: LDAP controls to make deactivated links visible to LDAP
Control Name
Visible when control present
LDAP_SERVER_SHOW_DEACTIVATED_LINK_OID
Link values referring to deleted-objects
Note: See ‘The removal of linked attribute values’ – below for the behavior of removing a link value rather than a link value being removed as a part of a logical deletion.
The following attributes will always be removed (during a logical delete) regardless of searchFlags (0x00000008 = Preserve on delete) and regardless of the Recycle Bin state.
During re-animation or undelete (if not supplied by the end-user) the objectCategory are driven back and set to the objectCategory of the objects most specific ObjectClass.
If the object is a user, this is computed back to its original state by using the userAccountControls attribute.
If the object is a group, this is computed back to its original state by using the groupType attribute.
Logical Deletion introduces the following internal columns to the database (NTDS.dit)
The following ‘internal’ columns (those are internal to the DSA only and are not real attributes) in addition with the attributes set in Table 1: ‘Logical deletion sets the following attributes’ represents the support on the database level for logical deletions of objects.
Table 3: datatable – Simplified for logical deletion
The ‘time_col’ is the column that holds the time of when an object was converted into a tombstone or deleted object.
‘time_col’ is not accessible through any external interfaces such as (LDAP, ADSI) – However the metadata for the attribute ‘isDeleted’ should have a ‘Org.Time/Date’ that matches the value stored in the ‘time_col’ column.
The ‘recycle_time_col’ is the column that holds the time of when an object was logically deleted or if the Recycle-Bin is enabled converted into a recycled object.
‘recyle_time_col’ is not accessible through any external interfaces such as (LDAP, ADSI) – However the metadata for the attribute ‘isRecycled’ should have a ‘Org.Time/Date’ that matches the value stored in the ‘recyle_time_col’ column.
Note: Only present in Windows Server 2008 R2 databases and later
Sample NTDS.dit representing the state of a logically deleted object:
Once the deleted objects life time (DOL) has passed since the object was logically deleted, ‘recycle_time_col’ and ‘isRecycled’ will be set. See ‘Deleted objects lifetime and deleted objects’ below for more information.
Originating logical deletion constraints
The object has to reside in a writable naming context (NC) on the DSA where the logical deletion originates.
If the object being deleted has the following characteristics within the Security Descriptor:
More exactly when the Sbz1 field has the value of 0x1 and the Control (RM control) field has the value (SECURITY_PRIVATE_OBJECT bit) of 0x1 and when deleting an object that resides within the schema naming context (NC) or the configuration naming context (NC) the following conditions have to be meet:
The Domain Controller (DC) (where the originated delete is taking place) must be a member of the root domain in the forest, or
The Domain Controller (DC) (where the originated delete is taking place) must be a member of the same domain where the current object owner belongs.
If the FLAG_DISALLOW_DELETE bit is set in the ‘systemFlags’ attribute of the object being deleted, the delete is being rejected by the DSA.
If the object being deleted is a tombstone e.g. has the ‘isDeleted’ attribute set to True and the Recycle Bin isn’t enabled, the delete is being rejected by the DSA.
If the object being deleted is recycled e.g. has the ‘isRecycled’ attribute se to True and the Recycle Bin is enabled, the delete is being rejected by the DSA.
If the object being deleted has descendants/child objects, the delete operation is being rejected by the DSA unless the requester has passed the LDAP_SERVER_TREE_DELETE_OID control. The following constraints applies to Tree-Deletes as well:
The requester must have the RIGHT_DS_DELETE_TREE on the object being deleted. Note that no additional permissions are required on the descendants of the object.
The tree-delete operation cannot be applied to a naming context (NC) root.
Objects with the ‘isCriticalSystemObject’ attribute equal to true and which is not Security Account Manager (SAM) specific objects cannot be deleted by the tree-delete operation. This constraint is checked object-by-object, and deletion stops at the first deletion attempt that violates the constraint. If deletion stops, the resultant tree might not be the same as the original tree because some objects might have been deleted prior to the failure.
If the object being deleted resides within the schema naming context (NC) and the “Schema Devil” hasn’t been invoked, the delete is being rejected by the DSA.
If the object being deleted is the DC’s nTDSDSA object (representing the DSA where the delete operation is taking place) or any of its ancestors, the delete is being rejected by the DSA.
If the object being deleted is a crossRef object corresponding to the DC’s (The DSA where the delete operation is taking place) configuration, schema, or default domain naming contexts (NCs), the delete is being rejected by the DSA.
If the object being deleted is protected, the delete is being rejected by the DSA.
If the delete operation would require delayed link processing or link clean up (please see the section below: The link cleaner and the delayed link processing mechanism) and such processing is already taking place on the object that is about the be deleted, the DSA may or may not proceed with the requested delete operation depending on the following:
The DSA is running Windows Server 2008 R2 or later and are subject to “Delayed Link Processing” and the object being deleted is already being processed by the “Delayed Link Processing” mechanism in a such way that the current processing can’t be merged with the requested operation (To remove forward or backward links associated with the object being deleted) – that is if:
The ongoing link processing is performing any of the following operations:
Processing Physical Link Deletions (Can be merged with the delete operation if the Recycle-Bin is absent, otherwise the delete is being rejected by the DSA)
Processing Logical Link Deletions (Can be merged with the delete operation if the Recycle-Bin is present, otherwise the delete is being rejected by the DSA)
Processing Logical Link Un-Deletions (Can’t be merged with the delete operation, the delete is being rejected by the DSA)
Processing/Touching MetaData for groupType change (Can’t be merged with the delete operation, otherwise the delete is being rejected by the DSA)
If the current operation implicit force processing on a specific attribute, a merge can only take place if the requested operation is about to perform processing on the same attribute, otherwise the delete is being rejected by the DSA.
If the current operation implicit force processing bound by a specific update sequence (USN), a merge can only take place if the requested operation is about to perform processing bound to the same USN, otherwise the delete is being rejected by the DSA.
The DSA is running Windows Server 2003 or Windows Server 2008 and are therefore subject to “Link Cleaner” – However the Link Cleaner has the ability for any object being processed to determine the required work to perform by the objects current state (instead of from a work list), that is if the delete operation requires processing (To remove forward or backward links associated with the object being deleted) – and the object already is under link cleanup, the current link cleanup task will pick up the requirement just made by the delete operation.
The Deleted Objects (DO) containers
When an object is logically deleted, they are eventually moved into something referred to as the “Deleted Objects Container” (DO container) – Each and every naming context (NC) in Active Directory has by default a deleted objects container except the Schema naming context (NC).
The deleted objects container(s) are not visible to ADSI or LDAP by default- However they can be made visible to LDAP by specifying a server control. (The deleted objects container(s) are marked as deleted themselves by having the “isDeleted” attribute set to: True) and are therefore by default invisible as any other deleted object.
The following controls make the deleted objects container(s), the deleted objects and eventually recycled objects visible.
Table 4: LDAP controls to make tombstones, deleted objects and recycled objects visible to LDAP
Control Name
Visible when control present
LDAP_SERVER_SHOW_DELETED_OID
Tombstones and Deleted Objects
LDAP_SERVER_SHOW_RECYCLED_OID
Tombstones, Deleted Objects and Recycled Objects
The deleted objects container(s) are referenced as well-known objects on each naming context (NC) head they belong to by the “wellKnownObjects” attribute, the reference is stored with the syntax binary distinguished name (Binary-DN) where the DN portion must be the DN of the deleted objects container ex: “CN=Deleted Objects,CN=Configuration,DC=X” and the binary portion must contain a well-known GUID (identifying that the DN represent a deleted objects container) “18E2EA80684F11D2B9AA00C04F79F805” if no such entry is present in the “wellKnownObjects” attribute on the naming context (NC) head the NC is considered to not having a deleted objects container.
Logically deleted objects are moved into the “Deleted Objects” container except in the following cases:
There is no “Deleted Objects” container in the NC the object resides in
The object has the SystemFlags bit: FLAG_DISALLOW_MOVE_ON_DELETE
The object is a naming context (NC) head.
The object is being removed as a part of the removal of an entire read-only naming context (NC)
If the object can’t for some reason be moved into a deleted objects container, the object is deleted anyway (The attributes in Table 1: ‘Logical deletion sets the following attributes’ are still set) but without being moved, additionally objects that are immediately physically deleted (e.g. objects that never transit to the phases of tombstones, deleted objects or recycled objects, such as dynamic objects for example) are never moved into a deleted objects container.
The Deleted Objects (DO) containers implements the following semantics to the database (NTDS.dit)
Note: ‘time_col’ is set to “9999-12-29 23:59:59” – pretty far from now right? As mentioned above the ‘Deleted Objects’ container(s) them self are having ‘isDeleted’ true making them invisible to LDAP except when the LDAP_SERVER_SHOW_DELETED_OID control is passed – this comes with another problem. What prevents the ‘Deleted Objects’ container(s) from being garbage collected? Well they are on the index and appear as a deleted object, it’s just that it won’t happen until “9999-12-29 23:59:59”
I did a test and actually changed this to something (not that far) away in time:
Name mangling for deletion
When an object is logically deleted, they are eventually relative distinguished name (RDN) mangled for deletion – that is to enforce an unique relative distinguished name (RDN) when and if the object is being moved into the deleted objects container (Please see the: ‘The Deleted Objects (DO) containers’ above for constraints when the object moves)
An relative distinguished name (RDN) is mangled for deletion as follows: <RDN>0x0ADEL:<GUID> Note: The <RDN> might be truncated to ensure the whole new RDN isn’t larger than 255 characters. Sample: “CN=Christoffer Andersson\0ADEL:1e5f5da7-af10-4d69-9c06-491c79659116”
If the Recycle-Bin is enabled, the original relative distinguished name (RDN) is stored in the msDs-LastKnownRDN before it’s mangled for deletion.
Logically deleted objects are relative distinguished name (RDN) mangled except the following cases:
The object is a naming context (NC) head.
The object is being removed as a part of the removal of an entire read-only naming context (NC).
Objects that are immediately physically deleted (e.g. objects that never transit to the phases of tombstones, deleted objects or recycled objects, such as dynamic objects for example) – don’t have their names mangled for deletions either.
The removal of linked attribute values
Prior to Windows Server 2003 and Linked Value Replication (LVR) – Linked values where removed instantly from the ‘link_table’ on each DSA.
Linked value replication (LVR) allows individual values of a multivalued attribute to be replicated separately. In Windows 2000 Server, when a change was made to a member of a group (one example of a multivalued attribute with linked values) the entire group had to be replicated including all linked values. With linked value replication (LVR), only the group member that has changed is replicated, and not the entire group. Linked valued replication (LVR) was introduced in Windows Server 2003 and requires all DSAs in the forest to run at least Windows Server 2003 and the forest functional level (FFL) to be Windows Server 2003 or later, other than those requirements linked value replication requires replication metadata peer value in addition to replication metadata peer attribute to track changed values.
Starting with linked value replication (LVR) the state of a deleted (ABSENT) value has to be replicated and received by all DSAs within the tombstone lifetime (TSL) just like logically deleted objects – after that a value has been deleted (marked as ABSENT) it will be garbage collected and physically removed once a tombstone lifetime (TSL) has elapsed since the date if the delete – Please see ‘Tombstones and tombstone lifetime’ and ‘The Garbage collector process’ below for more information.
The following events are logged if an ABSENT linked value is garbage collected (physically deleted from the database):
Table 5: ABSENT Linked Value being Garbage Collected
Event ID
Source
Category
Description
1697
ActiveDirectory_DomainService
Garbage Collection
Internal event: Active Directory Domain Services removed the following expired, deleted attribute value from the following object.
The ‘link_deltime’ column is the column that holds the time of when a linked value was made ‘ASBENT’ and marked for deletion.
‘link_deltime’ is not accessible through any external interfaces such as (LDAP, ADSI) – However the value property metadata for the value should have a ‘Last Org.Time/Date’ that matches the value stored in the ‘link_deltime’ column.
Note: Only present in Windows Server 2003 databases and later
The ‘link_ncdnt’ column was added to support linked value replication (LVR) – as linked attribute values are being replicated, there needs to be a way to determine the naming context (NC) they belong to (replication is always performed peer naming context)
Note: Only present in Windows Server 2003 databases and later
Let’s have a look at the ‘link_table’ in NTDS.dit with link values representing the LEGACY, PRESENT and ABSENT states:
The link cleaner and the delayed link processing mechanism
The link cleaner (Present in Windows Server 2003 and later) and the delayed link processing (Present in Windows Server 2008 R2 and later) are mechanisms that continue operations regarding links on objects that needs and have been marked for “cleaning”, objects needs “cleaning” when more than a maximum numbers of links have to change state (only the maximum limit of links are going to change to its desired new state in the original transaction) the remaining links are going to be processed (in batches of the maximum limit in their own transactions) by either the link cleaner or the delayed link processing mechanism that runs as an scheduled task.
The reason for that only a maximum (limit) of links can change state in a single transaction is to avoid ESE version store limits (e.g. the numbers of changes that can be held in the ‘copy buffer’ and commit in a single transaction to the NTDS.dit database) – therefor the remaining links over the maximum limit has to be processed in separate transactions, hence the link cleaner and the delayed link processing mechanism.
The link cleaner was introduced in Windows Server 2003 – the issue was introduced/become more critical with the capability of Linked Value Replication (LVR) that allows a linked attribute to store more than 5000 linked values.
Table 7: Background processing of linked values
Mechanism
DSAs Version
Maximum links in one transaction
Initialized
Re-Scheduled
Triggers
Maximum Run Time
Link Cleaner
Windows Server 2003 Windows Server 2003 R2 Windows Server 2008
1000
30 minutes after boot
Every 12 hours
60 seconds after that an object has been marked for cleaning
The task runs for maximum of 5 minutes until it reschedule itself:
If there is more work to do: immediately rescheduled.
If trigged by cleaning don’t reschedule
If the re occurring task, reschedule within 12 hours
Note: The Link Cleaner takes the above action as well if it has processed more than: 1000000 links
Delayed Link Processing
Windows Server 2008 R2 Windows Server 2012
10000
30 minutes after boot
Every 12 hours
When an object has been marked for cleaning and the original transaction has been committed
The task runs for maximum of 5 minutes until it reschedule itself:
If there is more work to do: immediately rescheduled.
If trigged by cleaning don’t reschedule
If the re occurring task, reschedule within 12 hours (e.g. not trigged by ‘doLinkCleanUp’)
The following events will eventually trigger the link cleaner or the delayed link processing mechanism:
Table 8: Events that trigger background processing of linked values
Mechanism
Event
Recycle Bin State
Action
Link Cleaner Delayed Link Processing
More than 1000/10000 links removed when an object is logically deleted
Off
Links are physically removed
Link Cleaner Delayed Link Processing
More than 1000/10000 links removed when an object is physically deleted
(Note: The object remains represented as a none-object also known as a phantom and the row in the database remains until all links has been deleted)
N/A
Links are physically removed
Link Cleaner Delayed Link Processing
More than 1000/10000 links are touched (making the replicator replicate them out – as when a none-universal group changes into an universal group)
N/A
Links replicate out to the GCs
Link Cleaner Delayed Link Processing
More than 1000/10000 links are removed as a results of an universal group being changed to a none-universal group
N/A
Links disappear from the GCs
Delayed Link Processing
More than 10000 links are removed when an object is logically deleted
On
Links are deactivated
Delayed Link Processing
An object with more than 10000 links transforms from being logically deleted into being recycled.
On
Links are physically removed
Delayed Link Processing
An object with more than 10000 links are being undeleted
On
Links are activated
In Windows Server 2008 R2 and later, the amount of links that are processed in a single transaction can be configured by a registry parameter: “Links process batch size”.
‘Links process batch size’ DWORD registry values can be configured within the following key: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\<INSTANCE>\Parameters. Note: a value of 0 can’t be used – and I strongly don’t recommend anyone to change the default behavior.
The Delayed Link Processing or the Link Cleaner process can be initiated manually by triggering the operational attribute: “doLinkCleanUp”: http://msdn.microsoft.com/en-us/library/cc223304(v=prot.10) on Windows Server 2003 DSAs or later.
The link cleaner and the delayed link processing mechanism implements the following at the database level (NTDS.dit)
Table 9: datatable – Simplified for the link cleaner and the delayed link processing mechanism
Used by the delayed link processing mechanism to identify rows that needs work performed.
Note: Only present in Windows Server 2008 R2 databases and later
The following Performance Monitors are available to track Delayed Link Processing:
Table 11: Delayed Link Processing Performance Counters
Category
Name
Operating System
Description
NTDS
Link Values Cleaned/sec
Windows Server 2008 or later
The rate at which link values that need to be cleaned are cleaned.
Tombstones and tombstone lifetime The object remains in this state (referred to as tombstones) for the period of tombstone lifetime (TSL) that is by default either 60 or 180 days depending on the operating system and service pack level of the first domain controller introduced in the Active Directory forest:
Table 12: tombstoneLifetime (TSL) defaults
Days
Operating System
60
Windows 2000 Server (All versions) Windows Server 2003 RTM Windows Server 2003 R2
180
Windows Server 2003 Service Pack 1 or later Windows Server 2008 Windows Server 2008 R2 Windows Server 2012
The defaults are more specifically defined in the [Directory Service] section of the schema.ini file that is used during the DCPROMO process; table 2 gives you an extract of the schema.ini file:
Table 13: Schema.ini (TSL) defaults
Schema.ini
;——————————————————–
; Windows NT subtree under the Services subtree
;——————————————————–
[Windows NT]
objectClass=Container
ObjectCategory=Container
ShowInAdvancedViewOnly=True
CHILD=Directory Service
[Directory Service]
objectClass=nTDSService
ObjectCategory=NTDS-Service
ShowInAdvancedViewOnly=True
; Explict TSL default set in W2K3 SP1 to increase shelf-life of backups and allow longer
; disconnection times.
tombstoneLifetime=180
The tombstone lifetime (TSL) is defined in days by the “tombstoneLifetime” attribute on the “CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration,DC=X” object. However if the attribute doesn’t contain a value e.g. is NULL – tombstone lifetime (TSL) is always 60 days regardless of ‘Table 1.’
If a value of less than 2 days is specified, the tombstone lifetime (TSL) defaults to 60 days, except for Windows Server 2008 R2 and Windows Server 2012, where the tombstone lifetime defaults to 2 days, finally the tombstone lifetime (TSL) can as mentioned in ‘Table 2.’ above be defined in “schema.ini” before the first domain controller is promoted in the forest.
The tombstones remain for as long as the tombstone lifetime (TSL) and are then deleted from the database (this is referred to as physical deletion and happens locally on each DSA by the Garbage Collector), eventually the object (row) remains represented as a phantom that exists in the database to satisfy reference integrity (e.g. other rows in the database that references the row being deleted).
Deleted objects lifetime and deleted objects
The deleted objects lifetime also known as the DOL and was introduced with Windows Server 2008 R2 to support the Recycle Bin feature where it becomes possible to un-delete an already deleted object to its original state, the deleted objects lifetime (DOL) determines the timeframe in days (by default the same as tombstone lifetime), for how long a deleted object is left in a deleted state so that it can be fully restored (un-deleted). Once the object has passed the DOL it’s transformed into a recycled object that reminds of a tombstone and is processed very likely a tombstone. The deleted objects lifetime is defined in days by the “msDS-DeletedObjectLifetime” attribute on the “CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration,DC=X” object. However if the attribute doesn’t contain a value e.g. is NULL – the deleted objects lifetime is equal to the tombstone lifetime mentioned above. However the deleted objects lifetime can never be shorter than 2 days.
The deleted objects remain for as long as the deleted object lifetime (DOL) and are then translated into a recycled object, where most information on the object is stripped away.
Dynamic Objects
Dynamic objects were introduced with Windows Server 2003 (by introducing the ability to dynamically linking auxiliary classes to instantiated objects). More specifically the “dynamicObject” auxiliary class that can be added to an object during creation/instantiation of an object and/or added as an objectClass later on to an already existing/instantiated object.
Dynamic objects are objects that are automatically deleted after a period of time. When they are deleted (automatically or manually), they do not transform into any other state, such as a tombstone, deleted-object, or recycled-object (However they may transform into a none-object also known as a phantom to satisfy reference integrity). They are distinguished from regular objects by the presence of the dynamicObject auxiliary class among their objectClass values. The intended time of deletion is specified (in seconds) by the Entry-TTL attribute.
The following requirement applies to Dynamic Objects:
Most be hosted by a Windows Server 2003 DSA or later
Can only be instantiated in a domain naming context (Domain NC) if the forest functional level (FFL) is Windows Server 2003 or later Note: This is cause a down-level DSA (earlier than Windows Server 2003) wouldn’t know what dynamic objects are, and wouldn’t delete them). One can first think that a requirement of domain functional level (DFL) would have been more suitable. However the object still has the possibility to be spread to down-level DSAs in other domains thought Global Catalogs (GCs).
Can always be instantiated in none-domain naming contexts (NDNCs) – because NDNCs can’t by design ever be fully hosted by a down-level DSA (earlier than Windows Server 2003) nor are they Global Catalog (GC) replicated. (I’ve done several attempts to trick an NDNC into being GC replicated without success)
Can never be instantiated in/added to an object at:
The Schema naming context (NC)
The Configuration naming context (NC)
An object that has the FLAG_DISALLOW_DELETE bit present in their systemFlags attribute:
All if any descendant objects, they must be dynamic objects as well.
Must be physically deleted by the DSA when all of the following conditions are meet:
The current time is greater than or equal to its msDS-Entry-Time-To-Die.
There are no descendant objects.
In theory there should never be, any descendant objects will have their msDS-Entry-Time-To-Die adjusted by the garbage collector so that they are removed before its parent.
There is no simple (none-linked) references to the object (there is no other object referencing the dynamic object)
If there are simple (none-linked) references to the object, the object (row) remains represented as a none-object also known as a phantom.
The object is not a critical system object (e.g. NOT having the Is-Critical-System-Object or is-Critical-Object set to: True)
Entry-TTL requirements: The Entry-TTL attribute specifies (in seconds) the intended time-to-live (TTL) for the dynamic (once the time has elapsed the object is being processed for physical deletion by the DSA) and have the following requirements:
The minimum Entry-TTL that can be configured/set for any dynamic object is the configurable value ‘DynamicObjectMinTTL’ value that’s by default are 900 seconds (15 minutes). However the ‘DynamicObjectMinTTL’ default value is configurable and can be changed by modifying the ‘DynamicObjectMinTTL=<seconds>’ value of the “msDS-Other-Settings” attribute on the “CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration,DC=X” object (The minimum value is 1 and the maximum value is 31557600 (one year).If the ‘DynamicObjectMinTTL’ value isn’t present in the “msDS-Other-Settings” attribute, the default is 900 seconds (15 minutes).
The maximum Entry-TTL that can be configured/set for any dynamic object are by default defined by the upper-Range attribute of the Entry-TTL schema object. The default maximum value is 31557600 seconds (one year). Note: The upper-Range attribute can’t be modified on a constructed attribute, it is possible to change this default value but it’s not recommended and requires a lot of knowledge.
The Entry-TTL can be set during the instantiation/creation of a dynamic object as long as the value is between (1.) and (2.) above. However if there is no Entry-TTL attribute specified during the creation of a dynamic object, The DSA will set an default configurable value ‘DynamicObjectDefaultTTL’ value that’s by default are 86400 (one day).However the ‘DynamicObjectDefaultTTL’ default value is configurable and can be changed by modifying the ‘DynamicObjectDefaultTTL =<seconds>’ value of the “msDS-Other-Settings” attribute on the “CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration,DC=X” object (The minimum value is 1 and the maximum value is 31557600 (one year).
How Dynamic Objects are maintained by the DSA
Dynamic objects are maintained by the DSA (evaluated for deletion once their TTL is expiring) by two different tasks:
By its own scheduled task that is initiated and starts 15 minutes after startup – the task then process dynamic objects in the following manner:
Process dynamic objects that match: current time => msDS-Time-To-Die value until all dynamic objects in the range has been processed or > 5000 objects has been processed (ESE version store limit) or abort if a shutdown has been initiated.
If the object found wasn’t able to be deleted because any of the following reasons skip to next candidate:
The object is marked critical. (e.g. the isCriticalSystemObject is set to: TRUE)
The object has backlinks pointing towards it and those can’t be removed.
The object still have descendant / child objects.
The object has simple references pointing towards it and was there for converted into a none-object also known as a phantom instead.
If (.a) complete the task with the following status:
There was more than 5000 object to process (more object’s than can be modified in a single ESE DB transaction to the NTDS.dit with respect to the ESE version store): Action: The task is re-scheduled immediately.
If the last object that was evaluated in (a.) and hadn’t reached its expiring time yet, and was about to expire in less than the default configurable ‘Delete expired entryTTL (secs)’ value that’s by default are 900 (15 minutes) then the task is re-scheduled within the default configurable ‘Delete next expired entryTTL (secs)’ value that’s by default are 30 (30 seconds) + the seconds until that object is expiring.
The ‘Delete next expired entryTTL (secs)’ and the ‘Delete next expired entryTTL (secs)’ DWORD registry values can be configured within the following key:
Note: if those registry values aren’t present the defaults mentioned above will take place, the value can’t be 0 (zero) and the updated values aren’t affected until next reboot.
If (i.) or (ii.) didn’t occur then the task would be re-scheduled to run within the default configurable ‘Delete expired entryTTL (secs)’ value that’s by default are 900 (15 minutes) again.
By the garbage collector (Please see the next section: The Garbage collector process)
The Garbage collector process
The Garbage collector process runs by default every 12 hours and 15 minutes after startup independently on each DSA, the garbage collection interval is defined in hours by the: “garbageCollPeriod” attribute on the “CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration,DC=X” object. (Minimum is one hour and maximum is one week) However if the attribute doesn’t contain a value e.g. is NULL – Then the garbage collection period is always the default 12 hours.
The Garbage Collector is responsible for the following subtasks:
Manage logically deleted objects and phantoms:
Phantoms that is older than the TSL/DOL:
Physically delete: reference countless phantoms that doesn’t appear on the ‘recycle_time_index’ (e.g. has a ‘time_col’ value that has passed TSL/DOL.)
Modify: phantoms with (that has) reference counts and set their ‘time_col’ to the current date, which have expired and doesn’t appear on the ‘recycle_time_index’ (e.g. had a ‘time_col’ value that had passed TSL/DOL.) – So that they won’t be looked at again for another TSL/DOL.
Deleted Objects that is older than the TLS/DOL:
Physically delete: All Deleted Objects if the schema doesn’t have the ‘IsRecycled’ attribute *Only happens on a Windows Server 2008 R2 DSA or later Note: Can happen in AD LDS
Physically delete: All Deleted Objects in none-writable naming contexts (NCs) if the recycle bin is in the off state
Recycle: Deleted Objects in writable naming contexts (NCs)*Only happens on a Windows Server 2008 R2 DSA or later and schema has ‘isRecycled’
Physically delete: All boot-strap objects from the distribution database (NTDS.dit)
Recycled-Objects that is older than the TSL:
Physically delete: All Recycled-Objects regardless of naming context (NC) or state of the recycle bin.
Recycled-Phantoms that is older than the TSL:
Physically delete: All Phantoms that are reference countless and appear on the ‘recycle_time_index’ (e.g. has a ‘recycle_time_col’ value that has passed TSL).
Modify: phantoms with (that has) reference counts and set their ‘recycle_time_col’ to the current date, which have expired and appears on the ‘recycle_time_index’ (e.g. had a ‘recycle_time_col’ value that had passed TSL.) – So that they won’t be looked at again for another TSL.
Deleted Linked-Values that is older than the TSL:
Physically delete: All linked-value replication (LVR) values that are ABSENT and older than the TSL.
Dynamic objects that is older than their ‘msDS-Time-To-Die’:
Physically delete: All dynamic objects that has expired (older than their time to live). Note: (For detailed processing, please see the section above: How Dynamic Objects are maintained by the DSA)
Note: Pre-Windows Server 2008 R2 DSAs (a.) and (b.) above applies to TSL rather than DOL.
Note: Pre-Windows Server 2008 R2 DSAs doesn’t process step (c.) and (d.) above.
Note: Pre-Windows Server 2003 DSAs doesn’t process step (e.) and (f.) above.
The following is logged once the garbage collection processes successfully removes a tombstone or recycled object (the object is most likely really demoted to a phantom):
Table 15: Garbage Collection Removal Event
Event ID
Source
Category
Description
1132
ActiveDirectory_DomainService
Garbage Collection
Internal event: The Directory Service removed the expired, deleted object <Object> from the database.
2918
ActiveDirectory_DomainService
Garbage Collection
Internal event: The Directory Service recycled the expired, deleted object <Object> from the database.
Perform online defragmentation of the Active Directory database NTDS.dit – if and only if the following DSA Heuristics flag has not been set: DecoupleDefragFromGarbageCollection (0000000001) and the DSA didn’t processed more than 5000 objects or values according to the table below: http://support.microsoft.com/kb/871003
The following event is logged once the online defragmentation has completed:
Table 16: Online Defragmentation Completed Event
Event ID
Source
Category
Description
1646
ActiveDirectory_DomainService
Garbage Collection
Internal event: The Active Directory Domain Services database has the following amount of free hard disk space remaining.
The Garbage Collection process is rescheduled more aggressively than the garbage collection period if there was more than 5000 objects/phantoms and/or values examined:
The task is rescheduled to 1/2 of the garbage collection period.
There was more than 5000 expired tombstones in the pass
Windows 2000 Server (All versions)
The task is rescheduled immediately.
There was more than 5000 expired tombstones, expired absent link values and expired dynamic objects (combined) in the pass
Windows Server 2003 (All versions)
Windows Server 2008 (All versions)
The task is rescheduled immediately.
There was more than 5000 expired deleted objects/phantoms, expired recycled objects/phantoms, expired absent link values and expired dynamic objects (combined) in the pass
Windows Server 2008 R2
Windows Server 2012
The following indices are implemented at the database layer to support the garbage collection tasked described above:
Table 18: datatable – Indices required to support the Garbage Collection Process
Used by the garbage collector to find expired deleted objects and phantoms that doesn’t have a ‘recycle_time_col’ value and either evaluates them for physical deletion or recycles them.
Note: Only present in Windows Server 2008 R2 and later database.
Used by the garbage collector to find expired ABSENT deleted link values and physically delete them.
Note: Only present in Windows Server 2003 databases or later.
The following Performance Monitors are available to track Garbage Collection activity:
Table 19: Garbage Collection Performance Counters
Category
Name
Operating System
Description
NTDS
Tombstones Visited/sec
Windows Server 2008 or later
The rate at which tombstoned objects are visited to be considered for garbage collected.
NTDS
Tombstones Garbage Collected/sec
Windows Server 2008 or later
The rate at which expired tombstoned objects are garbage collected.
The following event will be logged once the Garbage Collector task has completed:
Table 20: Garbage Collection Completed Event
Event ID
Source
Category
Description
1006
ActiveDirectory_DomainService
Garbage Collection
Internal event: Finished removing deleted objects that have expired (garbage collection). Number of expired deleted objects that have been removed: <Number of removed objects>.
Initialization of the Recycle-Bin in the off mode – Introduction of a Windows Server 2008 R2 DSA or later
So turning the Recycle-Bin off is impossible right? We’ll all writable Windows Server 2008 R2 and later DSAs actually will do so on every startup, but not in the way you’re thinking. However they are sort of preparing for someone to eventually turning the feature on in the future (this task can simply be referred to as a cleanup).
This is accomplished by scanning the ‘deltime_not_recycled_index’ for tombstones (excluding phantoms) (e.g. logically deleted objects with ‘isDeleted’ set to ‘1 (True)’) in all writable naming contexts (NCs) present on each writable Windows Server 2008 R2 DSA or later (regardless of functional level) and recycles them, e.g. setting ‘isRecycled’ to ‘1 (True)’ and their ‘recycle_time_col’ so that they instead appear on the ‘recycletime_index‘ and disappears from the ‘deltime_not_recycled_index’ there is however two conditions:
The ‘isRecycled’ attribute must be present in the schema
The Recycle-Bin must be off (However it must have been off at some time, the exception here is DSA being built from a Install From Media (IFM) database (NTDS.DIT))
This is required because the garbage collector in Windows Server 2008 R2 or later works with the ‘recycletime_index’ for physically deleting tombstones and recycled objects in writable naming contexts, this is an optimization so that once the Recycle-Bin is enabled; there is no need to simultaneously on all DSAs trying to convert all current tombstones into recycled objects.
The initialization process will process tombstones in batches of 5000 until it re-schedule itself again (as the regular garbage collection process, please see ‘The Garbage collector process’ above) until it has finished processing all tombstones and log the following event for every time it reschedules:
Table 21: Windows Server 2008 R2 DSA still processing initialization of the Recycle-Bin in the off-mode
Event ID
Source
Category
Description
2138
ActiveDirectory_DomainService
Garbage Collection
Internal processing of the Active Directory Domain Services database has not yet completely updated the AD_TERM’s tracking of the state of deleted objects. Until this processing completes successfully, objects may not be undeleted. Additionally, the Recycle Bin feature may not be enabled. This processing is continuing.
Once it has completed processing all tombstones the following event will be logged:
Table 21: Windows Server 2008 R2 DSA has completed processing initialization of the Recycle-Bin in the off-mode
Event ID
Source
Category
Description
2139
ActiveDirectory_DomainService
Garbage Collection
Internal processing of the Active Directory Domain Services database has completed the update of the Active Directory Domain Services’s tracking of the state of deleted objects.
This also comes with one disadvantage initially – Temporary Lingering Objects:
Recycle an object (in this case a tombstone) e.g. setting ‘isRecycled’ to ‘1 (True)’ is a replicating event – causing the change to replicate out to other DSAs that host/or has hosted the object in the past (as it is a tombstone) it could already have been physically deleted on some DSAs depending on if/when those DSAs where restarted and when the latest scheduled garbage collection process took place. In the case where the object has already been physically deleted, and an update to the object is being replicated to a now nonexistent object on this DSA (The DRA – Replicator on this DSA is going to treat this as an ‘Add’ for a new object until it realize it’s missing required attributes for an ‘add’ of a new object) – the DSA is going to indicate lingering objects by logging the following event:
Table 21: Lingering Object caused by introduction of a writable Windows Server 2008 R2 or later DSA
Event ID
Source
Category
Description
1388
ActiveDirectory_DomainService
Replication
Active Directory Domain Services Replication encountered the existence of objects in the following partition that have been deleted from the local domain controllers (DCs) Active Directory Domain Services database. Not all direct or transitive replication partners replicated in the deletion before the tombstone lifetime number of days passed. Objects that have been deleted and garbage collected from an Active Directory Domain Services partition but still exist in the writable partitions of other DCs in the same domain, or read-only partitions of global catalog servers in other domains in the forest are known as “lingering objects”.
4bbfc010-4bec-4c3f-b3e8-1dcab9d3238d This event is being logged because the source DC contains a lingering object which does not exist on the local DCs Active Directory Domain Services database. This replication attempt has been blocked.
The situation is going to resolve itself within a garbage collection interval that is default 12 hours (e.g. all DSAs will have garbage collected the object/Physically deleted the object from the database) – however the issue can be remediated by doing one of the following:
Increase the garbage collection interval prior to the introduction of the first writable Windows Server 2008 R2 DSA or later.
Once this has been performed on a Windows Server 2008 R2 DSA or later for the first time in all writable naming contexts in your forest, it’s unlikely that the initialization of Recycle-Bin in the off mode are going to find any tombstones to convert into recycled objects in the future (although this is checked on every startup), because from now on a logical deletion (while the Recycle-Bin is still off) will have both the ‘isRecycled’ and the ‘isDeleted’ attribute set by the writable Windows Server 2008 R2 and later DSAs.
Phantoms and reference integrity
So what is reference integrity in Active Directory? References in Active Directory are one object pointing to another object (Or one row that references another row in the database) by an attribute that has one of the following syntaxes:
Object(DS-DN)
Object(DN-String)
Object(DN-Binary)
Object(Access-Point)
Object(OR-Name)
The requirement that comes with references is that an object (row in the database) that has other objects (rows in the database) pointing towards it (referencing it) – Can’t be physically removed unless all references are removed first.
Let’s create two objects, where one of the objects references the other object by the attribute ‘seeAlso’:
dn: cn=Christoffer Andersson,cn=ESEDEV,DC=ADAM,DC=chrisse,DC=com changetype: add objectClass: user
dn: cn=Jimmy Andersson,cn=ESEDEV,DC=ADAM,DC=chrisse,DC=com changetype: add objectClass: user seeAlso: cn=Christoffer Andersson,cn=ESEDEV,DC=ADAM,DC=chrisse,DC=com
Let’s now logically delete the first created object “CN=Christoffer Andersson” (that the other object “CN=Jimmy Andersson” references) and wait until the object is up for physical deletion (e.g. the tombstone life time has passed since the object was logically deleted) – “CN=Christoffer Andersson” is now about to be physically deleted from the database, but it’s NOT (To satisfy the requirement mentioned above and not leaving the object “CN=Jimmy Andersson” pointing to a none-existing object in its ‘seeAlso’ attribute) instead “CN=Christoffer Andersson” remains represented as a none-object also known as phantom to satisfy the reference integrity and will remain in the database for as long as the reference exists.
Linked vs. none-linked references:
Simple references – This is none linked attributes as the example described above with the ‘seeAlso’ attribute – those are not removed on the source object (e.g. the object being referenced) and remains until one of the following happens:
The object that references the object is being deleted (e.g. in the example described above – The object “CN=Jimmy Andersson” is deleted, this potently frees the last reference to “CN=Christoffer Andersson” as the attribute ‘seeAlso’ is also being deleted as a part of deleting the object.
The reference itself is being removed (e.g. in the example described above – The attribute value of ‘seeAlso’ is removed on the object “CN=Jimmy Andersson” this potently frees the last reference to “CN=Christoffer Andersson”)
Linked references – This is a linked attribute, usually those references are removed during logical deletion (e.g. during transformation into a tombstone or recycled object) – However backlinks are kept during a read-only naming context (NC) tear down.
Demotion from an object to a none-object (phantom):
An object can be converted (demoted) from a real object into a none-object also known has a phantom by two different reasons.
The object (e.g. a tombstone or recycled object) has expired (e.g. passed the tombstone lifetime or the deleted object lifetime).
In addition to the attributes listed in (a.) any attribute keeping the object (the database row) on the index being garbage collected will preserved (e.g. msDS-Entry-Time-To-Die) – otherwise the object or phantom wouldn’t be up for examination next time the garbage collector process is running.
If there is NO references pointing towards the phantom after performing (a.) then the phantom (the row in the database) is up for being physically deleted – and be will completely removed from the database at the next Garbage Collector cycle (that’s by default runs every 12 hours). However if there ARE still references pointing towards the object – it’s going to remain as a none-object also known as a phantom until all references pointing towards it have been removed.
The object is leaving a DSA as a result of a read-only naming context (NC) tear down – but still has references pointing towards it. in this case the following happens:
In addition to linked and none-linked attributes there are two other references:
If an object has descendant (child) objects and/or phantoms it has one reference pointing towards it for each and every direct descendant (child) object or phantom.
All objects (not phantoms) holds a reference to them self’s by the Obj-Dist-Name attribute (http://msdn.microsoft.com/en-us/library/ms675516(v=VS.85).aspx).
References and Reference Counting within NTDS.dit
An object (row) in the database is represented locally and independently (on each DSA) within each database as a DNT – DNT is a short for distinguished name tag. (You need to be familiar with the concept of DNTs in order to understand reference integrity at the database layer in NTDS.dit – I’ve already given an introduction to DNTs in: How the Active Directory – Data Store Really Works (Inside NTDS.dit) – Part 2
Table 22: datatable – Simplified for reference integrity
The CNT_col is the column that keeps track of how many references that point to the actual row by other rows in the database.
That might be:
One count for each DN-syntax attribute that holds the value of the object (linked, none-linked, deleted, tombstone and recycled).
One count for each direct descendant (child) objects underneath the object.
Once count if the delayed linked processing mechanism or the link cleaner is working with links on the object.
Simple references in NTDS.dit
Let’s add in a sample NTDS.dit database with a simple reference:
“Elina Andersson” has a reference count “CNT_col” of “2” because of:
“Elina Andersson”‘s own self-reference from the “Obj-Dist-Name” attribute.
“Lena Andersson” is referencing “Elina Andersson” by the “seeAlso” attribute.
Let’s logically delete “Elina Andersson” and have a look what happens in the database:
Note: X represent when the object was logically deleted ex: 2012-09-09 09:09:09
The only remarkable change to the reference in this state would be that it is represented outside the database as a delete managed RDN in the value of the “seeAlso” attribute at “Lena Andersson” ex: CN=Elina Andersson\0ADEL:1e5f5da7-af10-4d69-9c06-491c79659116
Let’s pretend that 60/180 days (or the time of tombstone lifetime has elapsed) since X = 2012-09-09 09:09:09 when “Elina Andersson” was logically deleted and have a look at the database again:
The garbage collector would have tried to physically delete “Elina Andersson” now but failed because the reference count “CNT_col” isn’t still yet zero cause “Elina Andersson” is still being referenced by “Lena Andersson”‘s “seeAlso” attribute – instead the object was converted into a none-object also known as phantom – as a result of this note the following:
“Elina Andersson” lost the attribute “Obj-Dist-Name” (that referenced “Elina Andersson” self) while it was converted to a none-object (phantom) and therefore dropped its reference count “CNT_col” from 2 to 1.
“Elina Andersson”‘s “OBJ_col” was changed from 1 to ‘NULL’ to indicate that the row in the database now represents a none-object (phantom) instead of an object.
Let’s pretend that a few weeks later “Lena Andersson” is logically deleted and have a look at the database again:
Note: Once “Lena Andersson” was being logically deleted and transformed into a tombstone or recycled object, “Lena Andersson” lost the attribute value of the “seeAlso” attribute as it’s not preserved on logical deletion by default: See Attributes preserved on logical deletion for more information.
This means that “Elina Andersson” now is free to be deleted (No more references to DNT:5524) – as “Lena Andersson” held the last reference towards “Elina Andersson” in the database, “Elina Andersson”‘s reference count “CNT_col” is now 0 , “Elina Andersson” will be deleted immediately when the garbage collector runs next time.
Linked references in NTDS.dit
Let’s add in a sample NTDS.dit database with a linked reference:
“Elina Andersson” is a member of “Group X” (the hosting object) -as the member attribute is the forward link – referencing “Elina Andersson”. The reference count “CNT_col” is increased with +1 on “Elina Andersson”.
Note: There is no reference count from the backlink towards the forward link – No dangling backlink reference is possible since if the host DN is removed it must have first been logically deleted, which implicitly removes all link attributes and their associated targets’ backlinks.
Logically deleting either “Group X” or “Elina Andersson” would decrease the reference count “CNT_col” with -1 and remove the corresponding row above in the ‘link_table’ as well removing the value “Elina Andersson” from the member attribute at “Group X”.
Cross Naming Context (NC) references
A reference can be made between two objects that reside in different naming contexts (this is regardless if the reference is simple or linked) with the following restrictions:
Objects in none domain naming contexts (NDNCs) – can have the following references:
To any object within the configuration naming context (NC).
To any object within the schema naming context (NC).
The naming context head of any none domain naming context (NDNC).
Any object within their own domain naming context (NDC)
All other references than a, b, c and d is disallowed.
Objects in a domain naming context, schema naming context, configuration naming context – can have the following references:
To any objects within any other domain naming context (NC) within the forest.
To any object within the schema naming context (NC).
To any object within the configuration (NC).
To any none domain naming context (NDNC) head.
All other references than a, b, c and d is disallowed except values of non-replicated linked attributes, that can reference any object/row present in the local database (NTDS.dit) at a given DSA.
Let’s have a look at Cross Naming Context (NC) references in NTDS.dit
First we have to explain one new column and an attribute that plays a major role in cross naming context (NC) references.
The ‘instanceType’ attribute determine if the object is writable, and or is a naming context (NC) head itself and if there is any other naming contexts above it using the following bits:
0x00000001 – The head of naming context.
0x00000002 – This replica is not instantiated.
0x00000004 – The object is writable on this directory .
0x00000008 – The naming context above this one on this directory is held.
0x00000010 – The naming context is in the process of being constructed for the first time by using replication.
0x00000020 – The naming context is in the process of being removed from the local DSA.
Let’s add in a sample NTDS.dit database with cross naming context (NC) references from a DSA that’s a GC.
Note: “Elina Andersson” has an instanceType of 0 – that means “Elina Andersson” is a read-only object at this DSA and is part of a read-only naming context, typically that is a global catalog (GC) that hosts the domain naming context (NC) that “Elina Andersson” resides in and some other DSA hosts a writable copy of “Elina Andersson”.
“Lena Andersson” however has an instanceType of 4 – that means “Lena Andersson” is writable on this DSA and is part of a naming context that is authoritatively held on this DSA, typically that is the domain naming context.
The only difference here from other scenarios already mentioned is that if “Elina Andersson” would be logically deleted – it happens (originates) on a DSA that hosts a writable copy of “Elina Andersson”
Let’s have a look at Cross database references (that implicates cross naming context (NC) references) in NTDS.dit
Cross database references are between two objects where one of the object is located in an uninstantiated naming context (NC) e.g. a naming context (NC) not held by the DSA (that’s typically not a global catalog (GC)).
Let’s consider the following scenario – a forest with two domains D1 and D2 where D1 has at least one DC that is NOT a GC e.g. D1-DC01 and D2 has at least one DC that also is a GC e.g. D2-GC1.
Let’s imagine that D1 host the following user object “CN=Gustav Morath,CN=Users,DC=D1,DC=x” that references “CN=Robin Granberg,CN=Users,DC=D2,DC=x” in the ‘seeAlso’ attribute – this relationship causes an issue on D1-DC1: Because D1-DC1 doesn’t host the object being referenced “CN=Robin Granberg,CN=Users,DC=D2,DC=x” cause the D1-DC1 doesn’t hold a copy of the naming context (NC) for the domain D2 as it’s not a global catalog (GC).
Let’s now first look at the database (NTDS.dit) on D2-GC1:
Let’s now look into the database (NTDS.dit) on D1-DC1:
This problem is solved by representing “Robin Granberg” as a none-object also known as phantom to satisfy reference integrity, during the update of the object “Gustav Morath” the distinguishedName (DN) of “Robin Granberg” is requested to be added to the ‘See-Also’ attribute, during this request a few things are validated.
Verify that the distinguishedName we’re about the reference really exist:
If the distinguishedName entered matches an existing local object within the local database (NTDS.dit): In this case it dosen’t as we don’t host the object “Robin Granberg” as the object resides in another uninstansiated naming context D2.
If the distinguishedName entered matches any object within the enterprise, this is verified by going off the local machine, in this case D1-DC01 and perform the lookup on a DC that also is a Global Catalog Server (GC) as those host all refrenable objects. If the object exists on the queried GC the objects GUID and SID (if those exists) are retrived as well.
Verifying that the reference constraints mentioned above at: Cross Naming Context (NC) references are fullfiled.
If 1b and 2 above are fullfiled, then a phantom for the object being refrenced are created in the local database (NTDS.dit) in this case at D1-DC01 representing the object “Robin Granberg” including any structal phantoms needed to represent the full (DN) of “Robin Granberg”:
Here is a sample on how the represenation of the object “Robin Granberg” as a phantom and needed structual phantoms would look at D1-DC01:
How cross-database references are maintained to not become “stale” (outdated) in NTDS.dit
We have now gone thru how reference phantoms and eventually needed structural phantoms are created on none-GCs (Global Catalog Servers) to satisfy reference integrity – the initial creation of those phantoms are done by the local DSA once a reference is being written and isn’t found within the local database on the given DSA – it performs a “enterprise/forest wide verification” by locating a Global Catalog using the ‘DsGetDCName‘ (DCLocator) API with the flag ‘DS_GC_SERVER_REQUIRED’ to find a suitable GC (Global Catalog Server) to verify names against using the RPC interface method ‘IDL_DRSVerifyNames‘ with the flag ‘DRS_VERIFY_DSNAMES’
Let’s now imagine that the object “Robin Granberg” undergoes one of the following changes:
Being logically deleted (The case we’re focusing on in this article)
Having its RDN (Relative Distinguished Name) changed. (The object is renamed)
Being moved (changes parent) so that the DN (Distinguished Name) is being changed.
Had its SID changed.
If one of the cases mentioned above takes place to “Robin Granberg” on D2-GC1 (or another DC that is authoritative/holds a writable copy of the object) the phantom used to represent the reference to “Robin Granberg” from “Gustav Morath”‘s ‘See-Also’ attribute will become outdated (stale) on D1-DC1.
The scenarios described above enter the requirement for the flexible single master operations (FSMO) role ‘Infrastructure Master’ or the “IM” that according to me originally had a better name, the ‘Phantom Master’. The IM (Infrastructure Master) is responsible to maintain phantoms within a given domain and make sure that they don’t become outdated (stale) while the “real” object they represent changes as described above in other domains, the IM (infrastructure Master) is also responsible for spreading the word of any outdated (stale) phantoms and their new state to other DCs that are none GCs (Global Catalog Servers) in their domain except when the Recycle-Bin is on and all DCs that are none GCs (Global Catalog Servers) are responsible on their owns for this task.
The Infrastructure Master (IM) process with the Recycle-Bin Off
So let’s have a look on how the IM (infrastructure Master) will update a phantom in the case if the “real” object is being logically deleted in its own domain, specifically in this case if “Robin Granberg” is being logically deleted in the D2 domain. (For simplifying this scenario we have removed the reference in the ‘see-Also’ attribute from ‘Gustav Morath’ to ‘Robin Granberg’)
The IM (Infrastructure Master) in this case (D1-DC1) will periodically run a background task (See The Stale Phantom Background Task ‘ below for details’) that will compare the phantoms within its local database (NTDS.dit) with a GC (Global Catalog Server) in this case (D2-GC1) using the RPC interface method ‘IDL_DRSVerifyNames’ with the flag ‘DRS_VERIFY_DSNAMES’ in this case “Robin Granberg” is one of the phantoms that is being verified, The phantom “Robin Granberg” is matched against the real object representing “Robin Granberg” in the D2 domain by the GUID.
The name is bought back over RPC as a return of the ‘IDL_DRSVerifyNames’, The IM (Infrastructure Master) in this case (D1-DC1) will now compare the obtained name of “Robin Granberg” from the GC (Global Catalog Server) in this case (D2-GC1) and determine that the name doesn’t match as “Robin Granberg” now (as a result of the logical deletion on D2-GC1) have a deletion mangled name “Robin Granberg\0ADEL:0f09760e-e13b-457a-b207-ddb3d7824517”.
D1-DC1 will now create a ‘InfrastructureUpdate’ object using a random GUID as RDN as a descendent/child of the “CN=Infrastructure,DC=D1,DC=X” object with the ‘dnReferenceUpdate’ attribute containing the name updated name “Robin Granberg\0ADEL:0f09760e-e13b-457a-b207-ddb3d7824517”
D1-DC1 will now immediately delete the ‘InfrastructureUpdate’ object created above and let it replicate to other DCs within D1. Note: That the ‘dnReferenceUpdate’ attribute is preserved during logical deletion (e.g. the attribute is present on the tombstone), see the ‘Attributes preserved on logical deletion ‘section above for more information.
The DSAs have special code to detect that a write occurs to the ‘dnReferenceUpdate’ either locally as in this case on (D1-DC1) or via replication, once a write is detected the local phantom is referenced using the GUID and updated accordantly, in this case update the RDN of “Robin Granberg” to “Robin Granberg\0ADEL:0f09760e-e13b-457a-b207-ddb3d7824517”.Note: An additional check is done to determine if the name is deletion mangled as in the case with “Robin Granberg\0ADEL:0f09760e-e13b-457a-b207-ddb3d7824517” this will make all DSAs to locally start removing all backlinks pointing to the phantom “CN=Robin Granberg\0ADEL:0f09760e-e13b-457a-b207-ddb3d7824517”.
Note: There is no need to set the ‘isDeleted’ attribute as such doesn’t exist on phantoms, the name is already deletion mangled.
Note: There is no need to set the ‘time_col’ column as all phantoms have such. (The only thing preventing them from being garbage collected or physically deleted are that they are still being referenced to)
However going to stage (7.) in the above sample doesn’t physically remove the database row representing the phantom for “Robin Granberg\0ADEL:0f09760e-e13b-457a-b207-ddb3d7824517”. Because it now has a ‘CNT_col’ count of 1 since the ‘InfrastructureUpdate’ object references it.
Note: This means that we will have to wait for a TSL (Tombstone Lifetime) to pass (by default 60/180 days) before the ‘InfrastructureUpdate’ object is being garbage collected, and finally the database row representing the phantom for “Robin Granberg\0ADEL:0f09760e-e13b-457a-b207-ddb3d7824517” will lose the last reference towards it within the database and have ‘CNT_col’ count of 0, next tine the garbage collector runs (by default every 12h) the row representing the phantom “Robin Granberg\0ADEL:0f09760e-e13b-457a-b207-ddb3d7824517” can be physically deleted.
Summary: It takes another 60/180 days + 12h before the database row representing “Robin Granberg\0ADEL:0f09760e-e13b-457a-b207-ddb3d7824517” is gone from D1-DC1 since step (7.) is taken within the sample above.
The Infrastructure Master (IM) process with the Recycle-Bin On
So let’s have a look on how a DC that isn’t a GC (Global Catalog) will update a phantom in the case if the “real” object is being logically deleted in its own domain, specifically in this case if “Robin Granberg” is being logically deleted in the D2 domain. (For simplifying this scenario we have removed the reference in the ‘see-Also’ attribute from ‘Gustav Morath’ to ‘Robin Granberg’)
As the Recycle-Bin is enabled in this case, all DCs that isn’t a GC (Global Catalog) are now responsible by their own, more specifically (D1-DC1) to periodically run a background task (See The Stale Phantom Background Task ‘ below for details’) that will compare the phantoms within its local database (NTDS.dit) with a GC (Global Catalog Server) in this case (D2-GC1) using the RPC interface method ‘IDL_DRSVerifyNames’ with the flag ‘DRS_VERIFY_DSNAMES’ in this case “Robin Granberg” is one of the phantoms that is being verified, The phantom “Robin Granberg” is matched against the real object representing “Robin Granberg” in the D2 domain by the GUID.
The name is bought back over RPC as a return of the ‘IDL_DRSVerifyNames’, The DC in this case (D1-DC1) will now determine if the state of the object has changed as follows:
If the name obtained from the GC (Global Catalog) in this case (D2-DC1) is different from the name of the local phantom in this case “Robin Granberg” (as a result of the logical deletion on D2-GC1) the name has changed to a deletion mangled name “Robin Granberg\0ADEL:0f09760e-e13b-457a-b207-ddb3d7824517” – this local phantom will be updated to reflect the name change.
If the phantom is logically deleted in the local database (NTDS.dit) in this case (D1-DC1) but the obtained state from the GC (Global Catalog) in this case (D2-GC1) says it isn’t anymore the following actions are taken locally on the DC, in this case (D1-DC1)
Activate Forward Links
Activate Back Links
If the phantom locally represents a live object in the database (NTDS.dit) in this case at (D1-DC1) but the obtained state from the GC (Global Catalog) in this case (D2-GC1) says it’s logically deleted, the following actions are taken locally on the DC, in this case (D1-DC1)
Forward Links are deactivated.
Back Links are deactivated.
If the phantom is recycled locally in the database (NTDS.dit) in this case at (DC1-DC1) but the obtained state from the GC (Global Catalog) in this case (D2-GC1) says it isn’t anymore the following actions are taken locally on the DC, in this case (D1-DC1)
The ‘recycle_time_col’ is set to NULL.
If the phantom isn’t locally recycled in the database (NTDS.dit) in this case at (DC-D1) but the obtained state from the GC (Global Catalog) in this case (D2-GC1) says it is, the following actions are taken locally on the DC, in this case (D1-DC1)
Forward Links are removed
Back Links are removed
The ‘recycle_time_col’ is being set.
The Stale Phantom Background Task The IM (infrastructure Master) will scan its local NTDS.dit database for phantoms used by a rouge estimate of a scan rate on how long it should take to scan and verify all phantoms based on days, by default 2 days, the default value can be configured by a registry parameter: “Days per Database Phantom Scan”.
‘Days per Database Phantom Scan’ DWORD registry values can be configured within the following key: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\<INSTANCE>\Parameters. Note: a value of 0 can’t be used and a maximum of 365 days.
This means that the DSA acting as the IM (infrastructure Master) or if the Recycle-Bin is enabled all DSAs that isn’t also acting as GCs (Global Catalog Servers) will scan it’s local database (NTDS.dit) more or less aggressively to meet the rate of ‘Days per Database Phantom Scan’ – using the following algorithm:
Make sure that next pass runs no more frequent than 15mins and no less than 1h
1. If calculatedDelay < 900 (15 minutes)
Next Scan will take place within 15min.
2. If calculatedDelay > 86400 (24h)
Next Scan will take place within 24h.
If (calculatedDelay by passes 1. and 2. above)
Next Scan will take place within <calculatedDelay>
Once a local phantom scan on the IM (Infrastructure Master) or if the Recycle-Bin is enabled, on all DSAs that isn’t also acting as a GC (Global Catalog Server) has been initiated (at least every 24 hours) it will scan the local database for phantoms and verify if they are up-to-date or have become outdated (stale) against a Global Catalog Server (GC) until.
It made maximum allowed trips (10) to a GC (Global Catalog) to verify phantoms (a maximum of 240 phantoms are verified on each trip) – the requirement of making no more than 10 trips to a GC (Global Catalog) comes from not taking up the DSA’s task queue for too long.
The following is only applicable if the Recycle-Bin is off: Maximum allowed stale phantoms (720) has been returned by the GC (Global Catalog) and has been identified as stale – the requirements of updating a maximum of 720 stale phantoms comes from the limits on 800~ values in a none-linked multi-valued attribute back in the Windows 2000 days when the stale phantoms are being written to the infrastructureUpdate object (carrier-object)’s dnReferenceUpdate attribute.
All phantoms in the database have already been verified by the GC (Global Catalog) without hitting the limits of (1.) or (2.) if the Recycle-Bin is off above.
The task will reschedule itself using the algorithm above.
The following event will be logged once the task is completed:
Table 24: Stale Phantom Cleanup Sucess Event
Event ID
Source
Category
Description
1421
ActiveDirectory_DomainService
Directory Access
Internal event: The infrastructure update task has completed with the following results.
Queried phantom references:
<Numbers of Queried phantoms>
Phantom references that exist on the local domain controller:
<Rogue estimate of phantoms in the database>
Updated phantom references:
<Phantoms updated during this run>
The infrastructure update task will resume after the following interval.
The Stale Phantom background task can be initiated manually by triggering the operational attribute: checkPhantoms http://msdn.microsoft.com/en-us/library/cc223316(v=prot.20).aspx Note: The task can only be trigged on the DC holding the flexible single master operations role: Infrastructure or on all DCs (Not Global Catalog Servers) if the Recycle-Bin is enabled and is still subject to the algorithm above but won’t automatically re-schedule itself. (E.g. you may need to call the operational attribute multiple times to verify all phantoms)
Local Lingering Phantoms
Local lingering phantoms is a phantom that dose exists locally in the database (NTDS.dit) on the IM (Infrastructure Master) or if the Recycle-Bin is enabled, on all DSAs that isn’t also acting as a GC (Global Catalog) but the real object that the local phantom represents can’t be found/verified at a GC (Global Catalog) – reasons for this can be:
The object that the phantom represents has been physically deleted (e.g. garbage collected) in its source domain, while the local phantom has not. This is common for simple references, that prevents the local phantom from being physically deleted (e.g. garbage collected) because references are pointing towards it.
Object’s in the distribution database (NTDS.dit) that are deleted during installation (DCPROMO)
The following events are logged if a local lingering phantom is detected:
Table 25: Local Lingering Phantom Event
Event ID
Source
Category
Description
2126
ActiveDirectory_DomainService
Directory Access
The phantom object <Lingering Phantom> exists in the local Active Directory Domain Services database, but doesn’t exist in the database of another GC. This may indicate that replication has not completed, or may indicate that the local AD_TERM database contains a lingering phantom. If this state persists, it indicates a lingering phantom.
Used by the phantom cleanup task to find reference phantoms to be verified for staleness.
Note: Only present in Windows Server 2008 R2 databases and later
The following Performance Monitors are available to track Stale Phantom Cleanup activity:
Table 27: Stale Phantom Cleanup Performance Counters
Category
Name
Operating System
Description
NTDS
Phantoms Visited/sec
Windows Server 2008 or later
The rate at which phantoms are visited to determine if they are stale and need to be cleaned.
NTDS
Phantoms Cleaned/sec
Windows Server 2008 or later
The rate at which stale phantoms are cleaned.
I think this is all I can think of for now when it comes to Active Directory and deletions/removal of data – if you have continued to read to the end, you can see it’s pretty much put into the fundamentals of Active Directory and is rather complex.
So what is the “Code [1-3]” all about and where is Part 4 of the series that you might expect?
Before I go ahead with Part 4 I thought it would be a good idea to sum up Part 1 to Part 3 with code (So that you know how we figured out all this stuff while we was coding on ESEDump) – Note: This most may be targeted for the developer audience more than the general Active Directory administrator.
Disclaimer: The code samples provided here is code snippets that doesn’t represent any code actual code from the DSA and or any other Microsoft products and technologies, nor those they represent the complete source of ESEDump
ESEHelper – A managed ESE wrapper around the ESE APIs
We decided that we wanted to work with ESE in C# (and when we first started this project EseManaged from codeplex wasn’t around) and even if we could have used it later on – I guess we wanted full control and decided to stick with our own wrapper. So when you see references to “EseHelper” in the code snippets below – you know it’s just a wrapper around: Extensible Storage Engine Native APIs – there is no secrets around this J
JET_RETRIVECOLUMN structure custom methods
We extended the JET_RETRIVECOLUMN structure with some additional methods to retrieve data.
Table 0: JET_RETRIVECOLUMN structure
Code Snippet
// The custom methods in JET_RETRIEVECOLUMN allow us to quickly// interpret each column’s data depending on its data type (string, integer, etc.)
internal struct JET_RETRIEVECOLUMN
{
public int columnid;
public IntPtr pvData; // Pointer to the data block in memory
public int cbData; // Size of the allocated data block
public int cbActual; // Size of the actual/used data
public int grbit;
// Offset to the first byte to be retrieved from a column of type
// JET_coltypLongBinary or JET_coltypLongText
public int ibLongValue;
// Number of values in a multi-valued column
// Can be used to retrive a specific value
public int itagSequence;
// The columnid of the tagged, multi-valued, or sparse column
// when all tagged columns are retrieved by passing 0
// as the columnid to JetRetrieveColumn.”
public int columnidNextTagged;
public int err;
public void Initialize(ColumnInfo att)
{
this.Initialize(att.ID, 0);
}
public void Initialize(ColumnInfo att, int cbData)
{
// Initialize with a data block of cbData size
this.Initialize(att.ID, cbData);
}
public void Initialize(int columnid)
{
// Initialize with an empty data block
this.Initialize(columnid, 0);
}
public void Initialize(int columnid, int cbData)
{
// Reset the fields
this.cbActual = 0;
this.cbData = 0;
this.err = 0;
// Make sure to free any previously used memory in this instance
if (this.pvData != IntPtr.Zero)
{
Marshal.FreeHGlobal(this.pvData);
this.pvData = IntPtr.Zero;
}
this.columnid = columnid;
this.itagSequence = 1;
// Allocate a new memory block if necessary (if > 0 bytes requested)
this.cbData = cbData;
if (this.cbData > 0)
this.pvData = Marshal.AllocHGlobal(this.cbData);
}
// Copies the current memory block into a byte array
public IntPtr TableId; // added for table support in caching
public ColumnInfo(int id, string name, int type, string altname, int altid, IntPtr tableid)
{
this.ID = id;
this.Name = name;
this.DataType = type;
this.AltName = altname; //added for attribute name
this.AltId = altid;
this.TableId = tableid; //added for table support in caching
}
}
Perform Initialization and Attach to NTDS.dit
First thing we had to figure out was how we attached to the database (NTDS.dit) using JetInit, JetBeginSession, JetAttachDatabase and finally calling JetOpenDatabase in addition to those callas we had to set several parameters with JetSetSystemParameter for our usage, e.g there is things that need to be turned off as we attach/open the DB as read-only due to the nature of our application.
Table 1: ESE Initialization
Code Snippet
// E.Check makes sure a JET API call is successful, i.e. JET_errSuccess (0)// If the call fails, we throw an exception/write to the Console
// Initialize ESENT. Setting JetInit will inspect the logfiles to see if the last
// shutdown was clean. If it wasn’t (e.g. the application crashed) recovery will be
// run automatically bringing the database to a consistent state.
err = E.Check(EseHelper.JetSetSystemParameter(ref instance, EseHelper.JET_sesidNil, new IntPtr(EseHelper.JET_paramDatabasePageSize), new IntPtr(0x2000), null));
// Set up the recovery option (off), the maximum number temporary tables (7) and temporary path
err = E.Check(EseHelper.JetSetSystemParameter(ref instance, EseHelper.JET_sesidNil, new IntPtr(EseHelper.JET_paramRecovery), IntPtr.Zero, “off”));
err = E.Check(EseHelper.JetSetSystemParameter(ref instance, EseHelper.JET_sesidNil, new IntPtr(EseHelper.JET_paramEnableOnlineDefrag), IntPtr.Zero, null));
err = E.Check(EseHelper.JetSetSystemParameter(ref instance, EseHelper.JET_sesidNil, new IntPtr(0xa), IntPtr.Zero, null));
err = E.Check(EseHelper.JetSetSystemParameter(ref instance, EseHelper.JET_sesidNil, new IntPtr(EseHelper.JET_paramMaxTemporaryTables), new IntPtr(7), null));
err = E.Check(EseHelper.JetSetSystemParameter(ref instance, EseHelper.JET_sesidNil, new IntPtr(EseHelper.JET_paramTempPath), IntPtr.Zero, System.IO.Path.GetTempPath()));
// Initialize ESE and begin a session
err = E.Check(EseHelper.JetInit(ref instance));
err = E.Check(EseHelper.JetBeginSession(instance, out sesid, null, null));
err = E.Check(EseHelper.JetOpenDatabase(sesid, “NTDS.dit”, null, out dbid, 1));
List the tables inside NTDS.dit
We figured out that by statically opening the “MSysObjects” and positioning over the “RootObjects” index we could enumerate the tables inside the database using the following code snippet.
Table 2: ESE Enumerate tables inside the database
Code Snippet
// Simplified:// Method to obtain a list of tables for a given JET database
In Part 3, we’re discussing the usage of the “Ancestors_col” column and how it’s used to walk subtrees in the database, the DNTs are stored as bytes within the “Acenstors_col” and are read as in the code snippet below.
Table 3: Ancestors_col
Code Snippet
if (column.err != EseErrors.ColumnNull) {
string ancestory = null;
char[] ancestor_separator = { ‘,’ };
// Walk through every ancestry record in the returned column data
// Construct the ancestry string with the returned DNTs
for (int i = 0; i < column.GetData().Length; i += sizeof(int))
{
int dnt = BitConverter.ToInt32(column.GetData(), i);
Note: This is our way to read an objects full distinguished name, given an object’s DNT (Distinguished Name Tag) – But this is not considered safe by the DSA as mentioned in Part 3 as the “Ancestors_col” is being processed by a background task and might not be in-sync all the times. (Safer would be to walk the tree up – by each PDNT until PDNT == 2)
Table 4: Get an objects distinguished name by its DNT