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.
I’ve been both speaking about reference integrity and blogging on it pretty extensively in the past – but it’s many years ago but you can read the part “Phantoms and reference integrity” in the “How the Active Directory–Data Store Really Works (Inside NTDS.dit)–Part 4” blog post
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):
// Special code to corrupt "See-Also" ref
EseHelper.JET_SETINFO jET_SETINFO = new EseHelper.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 DB
byte[] 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:
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”
$RootDSE = [ADSI]"LDAP://RootDSE"
$RootDSE.Put("dumpReferences", "CN=GOOSE,OU=Corrupt,DC=ese,DC=nttest,DC=chrisse,DC=com")
$RootDSE.SetInfo()
$Content = (Get-Content "C:\Windows\NTDS\ntds.ref.dmp")[3]
$Content.Substring(0, $Content.IndexOf(" "))
Remove-Item "C:\Windows\NTDS\ntds.ref.dmp" -ForceOpening 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
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.
$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, systemMustContain
if (-not $ClassObj) { break }
[void]$ValidClassNames.Add($ClassObj.lDAPDisplayName)
# Collect container attributes for this level of the class hierarchy
foreach ($prop in @('mayContain', 'mustContain', 'systemMayContain', 'systemMustContain')) {
if ($ClassObj.$prop) { foreach ($val in $ClassObj.$prop) { [void]$ValidClassNames.Add($val) } }
}
# Check auxiliary classes if present
foreach ($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
$UserDNAttributesOr you can choose to 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
$UserDNAttributesSo 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”)
# 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”)
$header = Get-Content C:\Windows\NTDS\ntds.dmp -Encoding UTF8 | Select-Object -First 7 -Skip 1
$content = Get-Content C:\Windows\NTDS\ntds.dmp -Encoding UTF8
#$header
#$content | ? { $_ -match "^5228" }
$header | Set-Content C:\Debug\DN.txt
$content | ? { $_ -match "^5228" } | Add-Content C:\Debug\DN.txtThe DN.txt file will look something like this:

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
$content = Get-Content C:\Windows\NTDS\ntds.dmp -Encoding UTF8
# DNT of CN=GOOSE,OU=Corrupt,DC=ese,DC=nttest,DC=chrisse,DC=com
$content | ? { $_ -match "^5228" }
# DNT within CN=GOOSE,OU=Corrupt,DC=ese,DC=nttest,DC=chrisse,DC=com seeAlso attr
$content | ? { $_ -match "^9696" }CheckAndFixDNReference
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.
$RootDSE = [ADSI]"LDAP://RootDSE"
$RootDSE.Put("CheckAndFixDNReference", "CN=GOOSE,OU=Corrupt,DC=ese,DC=nttest,DC=chrisse,DC=com:seeAlso:0")
$RootDSE.SetInfo()Let’s look for the event:
# 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.
$RootDSE = [ADSI]"LDAP://RootDSE"
$RootDSE.Put("CheckAndFixDNReference", "CN=GOOSE,OU=Corrupt,DC=ese,DC=nttest,DC=chrisse,DC=com:seeAlso:1")
$RootDSE.SetInfo()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.
