{"record":{"id":"e7b9058176a29aec","repo":"unslothai/unsloth","slug":"native-path-changed-after-it-was-selected","errorCode":null,"errorMessage":"Native path changed after it was selected.","messagePattern":"Native path changed after it was selected\\.","errorType":"validation","errorClass":"NativePathLeaseError","httpStatus":400,"severity":"error","filePath":"studio/backend/utils/native_path_leases.py","lineNumber":362,"sourceCode":"    grant: NativePathGrant, identity_options: tuple[tuple[int, int], ...]\n) -> tuple[int, int] | None:\n    try:\n        st = os.lstat(grant.canonical_path)\n    except OSError as exc:\n        raise NativePathLeaseError(\"Native path is no longer accessible.\") from exc\n    if _stat_module.S_ISLNK(st.st_mode):\n        raise NativePathLeaseError(\"Native path is no longer a regular file.\")\n    if grant.path_type == \"file\":\n        if not _stat_module.S_ISREG(st.st_mode):\n            raise NativePathLeaseError(\"Native path is no longer a regular file.\")\n    elif grant.path_type == \"directory\":\n        if not _stat_module.S_ISDIR(st.st_mode):\n            raise NativePathLeaseError(\"Native path is no longer a directory.\")\n    else:\n        raise NativePathLeaseError(\"Native path grant has an unsupported path type.\")\n\n    if grant.size_bytes is not None and st.st_size != grant.size_bytes:\n        raise NativePathLeaseError(\"Native path changed after it was selected.\")\n    current_modified_ms = int(st.st_mtime_ns // 1_000_000)\n    if grant.modified_ms is not None and current_modified_ms != grant.modified_ms:\n        raise NativePathLeaseError(\"Native path changed after it was selected.\")\n    if grant.path_kind == \"document-folder\" and not identity_options:\n        raise NativePathLeaseError(\"Native path grant is missing its folder identity.\")\n    current_identity = (st.st_dev, st.st_ino)\n    expected_identity = _runtime_identity(identity_options)\n    if expected_identity is not None and current_identity != expected_identity:\n        raise NativePathLeaseError(\"Native path changed after it was selected.\")\n    return current_identity if expected_identity is not None else None\n\n\ndef _consume_nonce(nonce: str, expires_at_ms: int) -> None:\n    now_ms = int(time.time() * 1000)\n    with _REPLAY_LOCK:\n        for key, expiry in list(_USED_NONCES.items()):\n            if expiry <= now_ms:\n                _USED_NONCES.pop(key, None)","sourceCodeStart":344,"sourceCodeEnd":380,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/utils/native_path_leases.py#L344-L380","documentation":"The file's current size (st_size) differs from the size_bytes recorded in the signed grant, so the backend concludes the path changed after the user selected it. _validate_current_stat() pins size and mtime to detect any mutation between selection and use. This protects read consistency and prevents TOCTOU swaps of file content under a valid-looking lease.","triggerScenarios":"verify_native_path_lease() where grant.size_bytes is not None and st.st_size != grant.size_bytes: the file was appended to, truncated, overwritten, or replaced with a different-sized file between the picker and job run; a still-downloading file was selected mid-write.","commonSituations":"Selecting a model file while the browser/downloader is still writing it; logs or datasets that grow after selection; editors saving in place; a sync client (Dropbox/OneDrive) replacing placeholder files with real content after selection.","solutions":["Wait for writes to finish, then re-select the file to get a grant with the final size.","stat -c '%s' <path> and compare with size_bytes from the decoded payload to confirm which snapshot is stale.","Exclude actively-written files from selection (copy to a stable location first), then re-select.","For sync clients, wait for the 'synced' state before picking the file."],"exampleFix":"# before\npick file while downloader still writes it -> size grows -> grant stale\n\n# after\n$ while [ \"$(stat -c %s model.gguf)\" != \"$LAST\" ]; do LAST=$(stat -c %s model.gguf); sleep 2; done\n# download settled; re-select file and resubmit","handlingStrategy":"validation","validationCode":"import base64, json, os\n\ndef size_matches_grant(lease: str) -> bool:\n    payload = json.loads(base64.urlsafe_b64decode(lease.split('.')[0] + '=='))\n    expected = payload.get('size_bytes')\n    if expected is None:\n        return True\n    try:\n        return os.lstat(payload['canonical_path']).st_size == int(expected)\n    except OSError:\n        return False","typeGuard":null,"tryCatchPattern":"try:\n    grant = verify_native_path_lease(lease, operation='read')\nexcept NativePathLeaseError as exc:\n    if 'changed after it was selected' in str(exc):\n        lease = request_fresh_grant()  # file mutated; re-pin size/mtime\n        grant = verify_native_path_lease(lease, operation='read')\n    else:\n        raise","preventionTips":["Only select files once all writers (downloaders, sync clients) have finished.","Mint the grant as the last step before the API call, not at page load.","Copy in-flight files to a stable location and select the copy."],"tags":["native-path-lease","toctou","file-changed","filesystem"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}