{"record":{"id":"1bb9af52d74867e3","repo":"unslothai/unsloth","slug":"linked-folder-changed-after-it-was-selected","errorCode":null,"errorMessage":"Linked folder changed after it was selected","messagePattern":"Linked folder changed after it was selected","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"studio/backend/core/rag/folder_sync.py","lineNumber":268,"sourceCode":"                \"SELECT * FROM linked_folders WHERE scope=?\", (scope,)\n            ).fetchall()\n            for row in existing:\n                existing_key = _path_key(row[\"path\"])\n                if existing_key == normalized_key or _same_file(row[\"path\"], normalized):\n                    if row[\"status\"] == \"retired\" or row[\"delete_remove_index\"] is not None:\n                        raise ValueError(\"Linked folder is still being removed\")\n                    conn.rollback()\n                    return _reauthorize_folder(row[\"id\"], normalized, expected_identity)\n                if _paths_overlap(existing_key, normalized_key):\n                    raise ValueError(\"Linked folders in the same scope cannot overlap\")\n            try:\n                current_identity = _root_identity(normalized)\n            except RuntimeError as exc:\n                raise ValueError(str(exc)) from exc\n            if current_identity != (root_device, root_inode) or (\n                expected_identity is not None and current_identity != expected_identity\n            ):\n                raise ValueError(\"Linked folder changed after it was selected\")\n            conn.execute(\n                \"INSERT INTO linked_folders(id, scope_type, scope_id, scope, path, name, \"\n                \"root_device, root_inode, auto_sync, status, created_at, updated_at) \"\n                \"VALUES(?,?,?,?,?,?,?,?,?,?,?,?)\",\n                (\n                    folder_id,\n                    scope_type,\n                    scope_id,\n                    scope,\n                    normalized,\n                    (name or Path(normalized).name or normalized).strip(),\n                    *_store_identity((root_device, root_inode)),\n                    int(auto_sync),\n                    \"pending\",\n                    now,\n                    now,\n                ),\n            )","sourceCodeStart":250,"sourceCodeEnd":286,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/core/rag/folder_sync.py#L250-L286","documentation":"Raised inside create_folder's transaction when re-computed _root_identity(normalized) differs from the identity captured at function entry (root_device, root_inode), or from the caller-supplied expected_identity. This is a TOCTOU guard: between the user 'selecting' a folder and the row being inserted, the directory was replaced (new inode) or remounted (new device).","triggerScenarios":"A swap: rm -rf dir && mv other dir between the pre-check and the INSERT; concurrent create_folder calls where one already re-authorized the row; the folder being on removable media that unmounted/remounted mid-call; expected_identity captured in the UI flow from a stale stat.","commonSituations":"Installer/updater scripts replacing directories atomically; two tabs linking the same folder simultaneously; USB/network drives with unstable device ids; a frontend sending an expected_identity captured before a file-system migration.","solutions":["Retry create_folder without expected_identity (or with a freshly captured identity) after confirming the directory is the intended one.","If the swap was intentional (deployment replaced the folder), capture the new st_dev/st_ino and pass it as expected_identity.","Serialize concurrent creates for the same path with the scope lock / UI-level disabling of the button."],"exampleFix":"# before\nexpected = stat_dir(path)  # captured long before\ncreate_folder(..., expected_identity=expected)\n\n# after\nexpected = os.stat(path), then immediately:\ncreate_folder(..., expected_identity=(s.st_dev, s.st_ino))\n# on ValueError 'changed after it was selected': re-stat and retry once","handlingStrategy":"try-catch","validationCode":"# Capture identity immediately before the call to shrink the window:\nst = os.stat(path)\nexpected = (st.st_dev, st.st_ino)\ncreate_folder(..., expected_identity=expected)","typeGuard":null,"tryCatchPattern":"try:\n    folder = create_folder(..., expected_identity=expected)\nexcept ValueError as e:\n    if \"changed after it was selected\" in str(e):\n        st = os.stat(path)  # re-capture and retry once\n        folder = create_folder(..., expected_identity=(st.st_dev, st.st_ino))\n    else:\n        raise","preventionTips":["Capture expected_identity right before create_folder, not at page load.","Disable the submit button while a create is in flight to avoid double submits.","Do not run directory-swapping deploys while users are linking folders."],"tags":["rag","folder-sync","race-condition","toctou","filesystem"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}