[#739] Fix alias dereferencing dropping entries and accumulating DNs#742
Conversation
maximthomas
left a comment
There was a problem hiding this comment.
Test coverage gaps
Both are non-blocking follow-ups; the three added tests already cover the core fixes.
-
DirectoryExceptioncatch branch is untested.test_dangling_aliasexercises thealiasedEntry == nullpath, but not thecatch (DirectoryException)branch inreturnAliasedEntry. Add a test with a malformedaliasedObjectName(invalid DN syntax) to lock in the "log and skip, don't fail the whole search" behavior. -
No test pins dedup in the alias-first / target-in-scope order. The original regression was order-sensitive, and the existing
always/subtests only cover the target-before-alias ordering. Add a test where an alias is visited before an in-scope target and assert the target is returned exactly once, guarding the dedup path against future regressions.
…nd accumulating DNs The set guarding against alias loops in SearchOperationBasis.returnEntry recorded the DN of every entry the search returned, and was never cleared. The DN of an alias target was recorded before recursing onto it, so the recursion found the DN already there and dropped the entry: an alias in the search results returned neither itself nor its target, unless the target was in the scope of the search on its own. A persistent search keeps its operation for its whole lifetime, so the set also outlived the search phase: the second and later notifications for an entry were silently dropped, and the set was written from the worker thread of every operation that notified it. Track the aliases of the current chain in a set local to that chain, and keep the DNs already returned only for the duration of the search phase. Skip an alias whose target does not exist instead of dereferencing null, and report a DirectoryException instead of wrapping it in a RuntimeException.
…lias deref branches Add the two tests requested in review on OpenIdentityPlatform#742 and fix a gap one of them exposed: - returnAliasedEntry only caught DirectoryException, but Entry.getAliasedDN() parses aliasedObjectName with the SDK DN, which throws the unchecked LocalizedIllegalArgumentException for a malformed value. A malformed alias therefore still failed the whole search with result code 80. Catch it too so the alias is logged and skipped. - test_alias_with_malformed_target_is_skipped stores a malformed aliasedObjectName (syntax enforcement relaxed to WARN for the add) and asserts the search still succeeds, exercising that catch branch. - test_deref_alias_before_target_returns_target_once pins the alias-before-target ordering, guarding the dedup path against the original order-sensitive regression. Also make test_persistent_search_reports_repeated_changes deterministic: it issued the first modify before searchAsync had registered the persistent search, so the first notification could be missed. Wait until the backend reports the search as registered before modifying.
e04fff9 to
56d708c
Compare
Fixes #739.
What was wrong
SearchOperationBasis.returnEntrykept a singleSet<DN> dereferencedfor the whole operation, meant to break alias loops. It recorded the DN of every entry the search returned and was never cleared. Three defects came out of that.The dereferenced target was never returned. The target DN was added to the set on line 575, immediately before recursing onto it; the recursion then found its own DN in the set on line 581 and returned early without sending the entry. So an alias in the search results yielded neither itself nor its target — the result path only ever suppressed entries. The existing tests did not catch this because in all of them the target was independently in scope and happened to be returned before the alias (
cn=John Doesorts beforecn=President), so the dereferencing path never had to deliver anything.Persistent searches silently dropped changes.
PersistentSearchholds itsSearchOperationfor the lifetime of the search and callsreturnEntryper notification, so the set outlived the search phase. The first change to an entry put its DN in the set; every later change to the same entry hitdereferenced.contains(entry.getName())and was dropped without reaching the client. This also bounds the growth reported in #739 — the set only ever grew by distinct DNs, so it is not the unbounded/OOM shape described there, but a correctness bug in change notification, which matters more for the sync clients that persistent search exists for.The set was raced. Notifications run from the post-response callback of every modifying operation, i.e. from that operation's worker thread, so a plain
HashSetwas mutated concurrently.Plus the two adjacent defects from the issue: a dangling alias produced an NPE (
DirectoryServer.getEntryreturnsnull, passed straight into the recursion) and aDirectoryExceptionwas wrapped in aRuntimeException— both degrading into result code 80 via the genericcatch (Exception e)inLocalBackendSearchOperation.Regression from f771315 (#369). The CVE-2025-27497 fix (08aee47) guards the base-DN path with its own properly scoped set and does not touch this code.
What changed
aliasChainis threaded through the recursion and allocated lazily, only once an alias is actually met, so a search without aliases allocates nothing. Scoped guard modelled on 08aee47.returnedDNs, live only for the search phase.LocalBackendSearchOperationcalls the newSearchOperation.endSearchPhase()afterbackend.search(this), which clears the set and stops matching against it, so persistent search notifications neither accumulate nor get dropped. The set is concurrent because a persistent search is registered before the search phase starts and its notifications can overlap it.null.DirectoryExceptionfrom reading an alias is logged and the alias skipped, rather than wrapped in aRuntimeExceptionthat fails the whole search. Propagating it as such would mean changing theSearchOperation.returnEntrysignature, which is implemented bySearchOperationBasis, wrapped bySearchOperationWrapperand called fromPersistentSearchand several backends.Tests
Three tests added to
AliasTestCase, each verified to fail without the fix:test_deref_target_outside_scope— an alias whose target is outside the scope: the target is now returned. Fails on master (target missing).test_persistent_search_reports_repeated_changes— a persistent search withderefAliases: alwaysreceives every change to the same entry. Fails on master (second notification never arrives, 30s timeout).test_dangling_alias— a dangling alias is skipped and its neighbours still returned, instead of error 80.AliasTestCase18/18. Also green:SearchOperationTestCase,PersistentSearchControlTest,ChangelogBackendTestCase,LDIFBackendTestCase— 151 tests total.Open point for review
Deduplication across the result set is kept, so a search with
always/searchstill retains one DN per returned entry for the duration of the search phase — #739 asks for this to shrink to the number of aliases. It cannot, while the "same entry is returned once" behaviour holds: the traversal order is arbitrary, so suppressing a duplicate requires knowing every DN already sent. That behaviour is whattest_one_always,test_sub_searchandtest_sub_alwaysassert, following the Oracle OID documentation those tests cite. Dropping it (duplicates are permitted by the spec, and OpenLDAP returns them) would reduce the state to the length of the alias chain but changes observable behaviour and rewrites those three tests. Happy to go that way if preferred.One narrow pre-existing gap is left untouched: while the initial search phase is still running, an already-registered persistent search's notifications still go through the deduplication.