Guard bookmark folder traversal against cyclic folder relations - #9729
Open
theabhishekchandra wants to merge 2 commits into
Open
Conversation
A folder that ends up inside one of its own descendants makes every recursive walk over the relations table loop until the stack overflows: the folder picker (getFolderTree), deleting or fetching a folder branch (traverseBranch) and the HTML export (populateNode). Each walk now keeps the set of folders already on its path and skips any folder it has seen. Re-inserting an existing folder also replaced its parent relation instead of adding a second one, since Relation's primary key is autogenerated and REPLACE never deduplicated it. Fixes duckduckgo#5928
The sync data provider walks folder contents to build the upload and the sync repository walks up through parents after deduplication. Both recurse over the same relations table as the bookmark screens, so a loop between two folders overflowed the stack there as well. Each walk now skips a folder or entity it has already visited.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Task/Issue URL: #5928
Tech Design URL (if applicable):
API Proposals URL(s) (if applicable): None
Description
A bookmark folder that ends up inside one of its own descendants (for example FolderA contains FolderB and FolderB contains FolderA) makes every recursive walk over the
relationstable loop until the stack overflows, and the app is killed with aStackOverflowError. Three walks were affected:traverseFolderWithDepth, used by the folder picker (getFolderTree)traverseBranch, used when fetching or deleting a folder branch (getFolderBranch/deleteFolderBranch)populateNodeinRealSavedSitesExporter, used by the bookmarks HTML exportgetRequestEntriesForinSavedSitesSyncDataProvider, which walks folder contents to build the sync uploadtraverseParentsinRealSyncSavedSitesRepository, which walks up to the root after sync deduplicationEach walk now carries the set of folder ids already on its path and skips any folder it has seen, so a cycle is visited once and the walk terminates. Output for normal, acyclic trees is unchanged.
insert(folder)also now replaces the folder's existing parent relation instead of adding a second one.Relation's primary key is autogenerated, soOnConflictStrategy.REPLACEnever deduplicated it, and re-inserting a folder could create the multi-parent state that leads to cycles.Sync is also the most likely way such a loop appears in the first place, since folder moves made on two devices can merge into a state where each folder contains the other, so the two sync walks are guarded too and covered by unit tests.
Reproduced on a physical device by inserting one cyclic row into
relations: the folder picker, the export and the folder delete each crashed with several thousand frames of the corresponding traversal. All three complete with this change on the same data.Related: #9704 guards
traverseBranchonly. This change covers all five recursions plus the insert path.Steps to test this PR
Cyclic folder relations no longer crash
relationswithfolderId = FolderB.idandentityId = FolderA.id(e.g. pullapp.db, edit withsqlite3, push back while the app is force-stopped)Normal folder operations are unchanged
UI changes
Note
Medium Risk
Touches core bookmark tree, export, and sync traversal; behavior for valid trees should be unchanged, but incorrect visit logic could omit folders or leave bad relations after insert.
Overview
Prevents
StackOverflowErrorwhen bookmark folder relations form a cycle (e.g. Folder A contains B and B contains A). Recursive walks over therelationstable now keep a visited-folder set and skip folders already on the current path, so each folder is handled once and traversal stops.The same cycle guard is applied across folder picker (
getFolderTree/traverseFolderWithDepth), branch fetch/delete (traverseBranch), HTML export (populateNode), sync payload building (getRequestEntriesFor), and parent walks for dedup sync (traverseParentsinRealSyncSavedSitesRepository).insert(BookmarkFolder)now deletes the folder’s existing parent relation before inserting the new one, so re-inserting a folder cannot accumulate multiple parents (a path that could create cycles, sinceRelationuses an autogenerated PK andREPLACEdid not dedupe).Tests cover cyclic data for those flows plus no duplicate relation when the same folder is inserted twice.
Reviewed by Cursor Bugbot for commit f34a049. Bugbot is set up for automated code reviews on this repo. Configure here.