20 KiB
LIAM NTFS Provider Findings
Umfang der Prüfung
Die Prüfung umfasst den kompletten NTFS-Provider-Code im Projekt LiamNtfs, also nicht nur den aktuellen Provider-Einstieg, sondern auch die darunterliegenden Engine- und Hilfsklassen.
Geprüft wurden insbesondere:
LiamNtfs/C4IT.LIAM.Ntfs.csLiamNtfs/cNtfsBase.csLiamNtfs/cActiveDirectoryBase.csLiamNtfs/C4IT_IAM_SET/DataArea_FileSystem.csLiamNtfs/C4IT_IAM_SET/SecurityGroup.csLiamNtfs/C4IT_IAM_SET/DataArea.csLiamNtfs/C4IT_IAM_SET/DataArea_FileSystem_GET.csLiamNtfs/C4IT_IAM_SET/cNetworkConnection.csLiamNtfs/C4IT_IAM_SET/Helper.csLiamNtfs/C4IT_IAM_SET/ResultToken.cs
Nicht im Detail geprüft wurden andere Provider wie Exchange, AD oder Teams in diesem Dokument. Verweise dorthin wurden nur dann berücksichtigt, wenn sie für das Verhalten des NTFS-Providers direkt relevant waren.
Priorisierte Findings
1. Kritisch: Traverse-Fehler können als Erfolg zurückgegeben werden
Der gefährlichste Punkt liegt im Schreibpfad für die Ordneranlage und das Ensure von Berechtigungen.
Im Create-Pfad wird zunächst die AD-Gruppenanlage ausgeführt, danach der Ordner erstellt und danach SetTraversePermissions() aufgerufen. Das Problem ist, dass das Ergebnis der Ordnererstellung anschließend vollständig durch das Ergebnis des Traverse-Schritts überschrieben wird. Wenn SetTraversePermissions() intern in einen Fehlerzustand läuft, aber trotzdem ein ResultToken mit resultErrorId = 0 zurückgibt, sieht der Aufrufer am Ende formal einen Erfolg.
Genau das passiert in SetTraversePermissions() an mehreren Stellen. Dort werden Fehler zwar geloggt, aber häufig nur mit return resultToken; beendet, ohne einen fachlichen Fehlercode zu setzen. Damit ist die Methode in vielen Fällen „logisch fehlgeschlagen“, aber technisch weiterhin erfolgreich.
Die Folge ist gefährlich:
- Ordner kann erstellt worden sein, aber Traverse-Gruppen fehlen teilweise oder vollständig.
- AD-Gruppen können erzeugt worden sein, aber Traverse-Nesting ist unvollständig.
- Aufrufende Schichten wie Workflow, Diagnostics oder API können einen Erfolg anzeigen, obwohl das Ergebnis fachlich inkonsistent ist.
Betroffene Stellen:
2. Hoch: Die Security-Group-Templates werden mutiert und danach wiederverwendet
SecurityGroups.GenerateNewSecurityGroups() verändert die übergebenen Templates direkt. Dabei werden NamingTemplate, DescriptionTemplate und WildcardTemplate in-place mit konkreten Pfaden, Tags und Platzhaltern überschrieben.
Das ist problematisch, weil dieselbe Template-Liste innerhalb derselben Engine-Instanz mehrfach verwendet wird:
- im Retry-Loop bei Namenskollisionen während der Gruppenerzeugung
- später erneut beim Traverse-Handling
- potenziell auch zwischen Ensure- und Create-Pfaden innerhalb derselben Session
Sobald das Template einmal materialisiert wurde, ist es kein Template mehr. Ein zweiter Lauf arbeitet nicht mehr mit der ursprünglichen Konfiguration, sondern mit bereits ersetzten Strings. Dadurch entstehen mehrere Risiken:
- der Loop-Counter arbeitet nicht mehr auf dem Originalmuster
- spätere Durchläufe können bereits ersetzte Namen nochmals verarbeiten
- Wildcards passen nicht mehr sauber zum ursprünglichen Naming-Schema
- Traverse-Gruppen können auf anderen Namensmustern basieren als Owner/Write/Read
Das ist kein kosmetisches Problem, sondern ein echter Zustandsfehler. Konfiguration wird in Laufzeitstatus verwandelt und danach weiterbenutzt.
Betroffene Stellen:
- SecurityGroup.cs#L146
- SecurityGroup.cs#L184
- DataArea_FileSystem.cs#L1016
- DataArea_FileSystem.cs#L411
Status:
- Am 2026-03-10 umgesetzt.
GenerateNewSecurityGroups()arbeitet jetzt auf einer pro Aufruf geklonten Template-Liste statt auf der übergebenen Originalsammlung.- Die Konfigurationstemplates im Engine-Kontext bleiben dadurch unverändert und können in Retry-Loops und später im Traverse-Pfad erneut korrekt materialisiert werden.
- Der Fix adressiert bewusst nur die Zustandsmutation. Die fachliche Gruppenerzeugung und das Naming-Verhalten selbst wurden dabei nicht geändert.
3. Hoch: Drei Minuten harter Blocker im Traverse-Pfad
Im Traverse-Pfad steckt ein explizites Thread.Sleep(180000). Das blockiert den ausführenden Thread für drei Minuten.
Das ist in einer Bibliothek dieser Art ein massiver Risikofaktor:
- Workflows können in Timeouts laufen
- GUI-Aktionen wirken eingefroren
- Web-/Service-Threads werden unnötig belegt
- parallele Verarbeitung skaliert sehr schlecht
Der Code dokumentiert das zusätzlich falsch mit „60 Sekunden warten“, obwohl tatsächlich 180 Sekunden geschlafen wird. Das erschwert Diagnose und Betrieb zusätzlich.
Wenn dieses Sleep als AD-Replikations-Workaround gedacht war, ist es trotzdem der falsche technische Schnitt. Dann müsste stattdessen gezielt auf die Verfügbarkeit der erzeugten Gruppen geprüft werden, idealerweise mit Polling und Timeout statt blindem Warten.
Betroffene Stelle:
Status:
- Am 2026-03-18 umgesetzt.
- Das harte
Thread.Sleep(180000)wurde entfernt. - Zunächst wurde der Wait auf die bloße Auflösbarkeit neu erzeugter Gruppen umgestellt. Nach fachlicher Rückmeldung wurde der Fix auf den tatsächlich kritischen Folgeschritt verschoben: die Membership-Änderung an der Traverse-Gruppe.
- Die Traverse-Logik retryt jetzt direkt
Members.Contains(...),Members.Add(...)undSave()mit sofortigem Erstversuch und kurzem Backoff. - Die maximale Obergrenze bleibt bewusst bei 3 Minuten, damit das bisherige Sicherheitsfenster für langsame AD-Konsistenz erhalten bleibt.
- Im Normalfall endet die Wartezeit jetzt deutlich früher, sobald die Membership-Änderung erfolgreich durchläuft.
4. Hoch: Unterschiedliches SMB-Verhalten zwischen Lesen und Schreiben
Der Provider verwendet für das Lesen des NTFS-Baums cNtfsBase.LogonAsync(). Dort wird der bekannte SMB-Fehler 1219 abgefangen, die bestehende Verbindung getrennt und anschließend ein neuer Versuch gestartet.
Die Create- und Ensure-Pfade verwenden dagegen cNetworkConnection, und diese Klasse wirft bei jedem Fehler direkt eine Exception. Ein Retry für 1219 findet dort nicht statt.
Das führt zu einem inkonsistenten Betriebsverhalten:
- Ein und dieselbe Konfiguration kann beim Lesen funktionieren.
- Dieselbe Konfiguration kann beim Schreiben scheitern, obwohl die Ursache nur eine bestehende Session auf den Share ist.
Das ist besonders tückisch, weil es im Betrieb wie ein „sporadischer“ Fehler aussieht, tatsächlich aber ein systematischer Unterschied zwischen Read- und Write-Pfad ist.
Betroffene Stellen:
5. Hoch: Geerbte ACLs werden im Ensure-Pfad als „vorhanden“ behandelt
Fachlich war bereits geklärt, dass geerbte Rechte hier keine Rolle spielen sollen. Genau das macht der neue Ensure-Pfad aktuell aber nicht konsequent.
Bei der Prüfung, ob eine ACL bereits vorhanden ist, werden Regeln mit GetAccessRules(true, true, ...) geladen. Das zweite true bedeutet, dass auch geerbte Regeln berücksichtigt werden. Damit kann eine Berechtigung, die nur von oben geerbt wurde, dazu führen, dass der Provider glaubt, die explizite ACL sei bereits gesetzt.
Im Ergebnis kann das Ensure-Feature damit stillschweigend zu wenig tun:
- fehlende explizite ACLs werden nicht gesetzt
- das Ergebnis sieht erfolgreich aus
- späteres Mapping oder Downstream-Logik kann trotzdem unerwartetes Verhalten zeigen
Das betrifft nicht nur Traverse, sondern auch die allgemeine ACL-Sicherstellung im neuen Ensure-Pfad.
Betroffene Stellen:
6. Hoch: Per Call übergebene CustomTags erfüllen die Pflichtanforderungen nicht wirklich
Im Provider wird in CreateFilesystemEngine() zwar eine gemergte Tag-Sammlung aufgebaut, in der Provider-Tags und per Aufruf übergebene Tags zusammengeführt werden. Das klingt zunächst korrekt.
Die Pflichtwerte für groupPrefix, groupOwnerTag, groupWriteTag, groupReadTag, groupTraverseTag, groupDLTag und groupGTag werden aber nicht aus dieser gemergten Sammlung gelesen, sondern weiterhin direkt aus Provider.CustomTags.
Die Folge ist eine verdeckte Inkonsistenz:
- im Debug-/GUI-Output sehen die Tags vorhanden aus
- im Engine-Aufbau schlagen Pflichtprüfungen trotzdem fehl
- Call-spezifische Overrides wirken nur teilweise
Das ist besonders ungünstig für Workflow- oder Diagnostics-Szenarien, in denen Tags bewusst pro Aufruf ergänzt oder überschrieben werden sollen.
Betroffene Stellen:
7. Hoch: ReplaceNtfsCustomTags() ist zu früh, zu strikt und verändert Konfiguration dauerhaft
Bereits im Provider-Konstruktor wird ReplaceNtfsCustomTags() ausgeführt. Die Methode greift direkt per CustomTags[...] auf Dictionary-Einträge zu und ersetzt Platzhalter in den Naming-Conventions.
Das erzeugt drei Probleme gleichzeitig:
- Fehlende Tags schlagen schon beim Provider-Aufbau fehl, bevor eine gezielte Validierung mit verständlicher Fehlermeldung greifen kann.
- Die Naming-Conventions werden direkt verändert. Danach ist nicht mehr klar, was Originalkonfiguration und was bereits materialisierter Laufzeitzustand ist.
- Alias-Verhalten wie
ADGroupPrefixwird hier noch nicht sauber berücksichtigt, obwohl es später teilweise unterstützt wird.
Damit wird Konfiguration zu früh und zu aggressiv „kompiliert“.
Betroffene Stellen:
8. Mittel-Hoch: LoadDataArea() behandelt UNC-Pfade fachlich falsch
Die Einzel-Ladefunktion LoadDataArea() ist sichtbar unfertig. Das ist nicht nur ein Stilproblem, sondern erzeugt reales Fehlverhalten.
Der Code splittet den Pfad mit UID.Split(Path.DirectorySeparatorChar) ohne RemoveEmptyEntries. Bei UNC-Pfaden wie \\server\share entstehen dadurch führende leere Segmente. Das führt dazu, dass ein Share-Pfad nicht sauber als Share erkannt wird, sondern in den Default-Branch läuft.
Zusätzlich wird DisplayName aus Path.GetDirectoryName(UID) ermittelt. Das ist für den Anwendungsfall falsch, weil damit der Elternpfad statt des Blattnamens verwendet wird.
Konkrete Folgen:
- Share-Roots können beim Einzel-Laden als Folder erscheinen
- Anzeigenamen stimmen nicht
- Root-Level und Child-Level verhalten sich inkonsistent
Betroffene Stellen:
9. Mittel-Hoch: Depth = 0 verliert die Root-DataArea
Der Provider baut in getDataAreasAsync() zunächst korrekt das Root-Objekt auf. Danach wird die Unterordnerliste über ntfsBase.RequestFoldersListAsync() geladen. Wenn Depth == 0 ist, liefert RequestFoldersListAsync() aber null statt einer leeren Liste zurück.
Im Provider wird null dann als harter Fehler interpretiert und es wird insgesamt null zurückgegeben. Das bereits erzeugte Root-Objekt geht damit verloren.
Für einen Aufrufer, der bewusst nur Ebene 0 sehen möchte, ist das klar fehlerhaft.
Betroffene Stellen:
10. Mittel: AD-Gruppenabfragen können still unvollständig sein
cActiveDirectoryBase.privRequestSecurityGroupsListAsync() baut die Gruppenliste aus LDAP-Ergebnissen auf. Dort wird jedoch GroupPrincipal.FindByIdentity(...).GroupScope ohne Null-Check verwendet.
Wenn eine Gruppe aus dem LDAP-Result zwar gefunden wird, aber nicht sauber als GroupPrincipal auflösbar ist, entsteht ein Fehler. Dieser wird anschließend durch einen allgemeinen catch verschluckt, und es wird einfach die bis dahin gesammelte Teilliste zurückgegeben.
Das ist problematisch, weil der Aufrufer nicht klar zwischen „Liste vollständig“ und „Liste vorzeitig abgebrochen“ unterscheiden kann.
Betroffene Stellen:
11. Mittel: Zentrale Fehlerschnittstelle des Providers deckt Schreibfehler nicht ab
GetLastErrorMessage() sammelt nur Fehler aus ntfsBase und activeDirectoryBase. Die eigentlichen Fehler aus DataArea_FileSystem laufen aber über ResultToken und landen nicht in dieser zentralen Fehlerschnittstelle.
Das bedeutet:
Logon- und Read-Fehler sind überGetLastErrorMessage()erkennbar- Create-/Ensure-Fehler dagegen oft nicht
Für aufrufende Schichten, die nur diese zentrale Schnittstelle kennen, entsteht damit ein unvollständiges Fehlerbild.
Betroffene Stellen:
12. Mittel: ACL-Mapping ist stark von vollständigen Naming-Conventions abhängig und fällt auf Sentinel-Werte zurück
Die Permission-Auflösung in ResolvePermissionGroupsAsync() verwendet .First(...) für die Conventions von Owner, Write und Read. Fehlt eine passende Konvention, wirft der Code sofort eine Exception.
Parallel dazu sind die Zielwerte mit S-1-0-0 vorbelegt. Das führt zu zwei unterschiedlichen Fehlermustern:
- bei fehlender Konfiguration: harter Laufzeitfehler
- bei nicht gefundenem Match: scheinbar gültige SID, fachlich aber nur ein Platzhalter
Das macht Ergebnisse schwer interpretierbar und erhöht das Risiko, dass Fehler spät erkannt werden.
Betroffene Stellen:
13. Mittel: Mitgliederauflösung bricht intern mit NullReference ab und wird dann nur noch als null sichtbar
GetMembersAsync() ruft privGetMembersAsync(sid).ToList() auf, bevor geprüft wird, ob überhaupt ein Result vorliegt. Wenn die Gruppe nicht gefunden wird, kommt null zurück und .ToList() wirft intern.
Die Exception wird zwar gefangen, aber nach außen sieht der Aufrufer nur noch null. Damit verschwimmt der Unterschied zwischen:
- Gruppe nicht vorhanden
- AD-Auflösung fehlgeschlagen
- echte Kommunikationsstörung
Betroffene Stellen:
14. Mittel/Niedrig: Share-Objekte können keine Kinder nachladen
cLiamNtfsShare.getFolders() und getChildrenAsync() liefern aktuell immer nur leere Listen zurück. Das bedeutet, dass Share-Objekte zwar initial erzeugt werden können, ein späteres Nachladen über die Objektmethoden aber praktisch nicht funktioniert.
Wenn diese Methoden nicht mehr verwendet werden, ist das nur Altlast. Wenn doch, ist das ein funktionaler Bruch.
Betroffene Stellen:
15. Niedrig, aber inkonsistent: Der alte GET-Pfad verhält sich anders als der aktuelle Providerpfad
Im älteren DataArea_FileSystem_GET-Pfad wird die Root-Struktur anders aufgebaut als im aktuellen Providerpfad. Ebene 0 bekommt dort kein Root-ACL-Mapping, und das Verhalten unterscheidet sich insgesamt von der neueren Logik im eigentlichen Provider.
Wenn dieser Altpfad noch produktiv verwendet wird, gibt es damit zwei unterschiedliche Wahrheiten für denselben fachlichen Bereich.
Betroffene Stellen:
Fazit
Der NTFS-Provider ist an mehreren Stellen funktionsfähig, aber der Schreibpfad ist deutlich fragiler als der Lesepfad.
Die wichtigsten Risiken sind:
- stille Erfolgsrückgaben trotz unvollständiger Traverse-Verarbeitung
- mutierende Templates mit Folgeschäden für Naming und Wiederholungslogik
- blockierende Wartezeiten und inkonsistentes SMB-Verbindungsverhalten
- Berücksichtigung geerbter ACLs im neuen Ensure-Pfad trotz fachlicher Gegenanforderung
- unvollständige oder irreführende Fehlerkommunikation nach außen
Für eine technische Bereinigung würde ich die Themen in dieser Reihenfolge angehen:
- harte Erfolgs-/Fehlersemantik im Create-/Ensure-/Traverse-Pfad
- Entfernen des
Thread.Sleepund Ersatz durch kontrolliertes Polling - Templates unveränderlich behandeln und pro Lauf kopieren
- ACL-Prüfungen im Ensure-Pfad auf explizite Regeln einschränken
- Tag- und Konfigurationsauflösung sauber zentralisieren