unslothai/unsloth · error · NativePathLeaseError

Native path changed after it was selected.

Error message

Native path changed after it was selected.

What it means

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.

Source

Thrown at studio/backend/utils/native_path_leases.py:362

    grant: NativePathGrant, identity_options: tuple[tuple[int, int], ...]
) -> tuple[int, int] | None:
    try:
        st = os.lstat(grant.canonical_path)
    except OSError as exc:
        raise NativePathLeaseError("Native path is no longer accessible.") from exc
    if _stat_module.S_ISLNK(st.st_mode):
        raise NativePathLeaseError("Native path is no longer a regular file.")
    if grant.path_type == "file":
        if not _stat_module.S_ISREG(st.st_mode):
            raise NativePathLeaseError("Native path is no longer a regular file.")
    elif grant.path_type == "directory":
        if not _stat_module.S_ISDIR(st.st_mode):
            raise NativePathLeaseError("Native path is no longer a directory.")
    else:
        raise NativePathLeaseError("Native path grant has an unsupported path type.")

    if grant.size_bytes is not None and st.st_size != grant.size_bytes:
        raise NativePathLeaseError("Native path changed after it was selected.")
    current_modified_ms = int(st.st_mtime_ns // 1_000_000)
    if grant.modified_ms is not None and current_modified_ms != grant.modified_ms:
        raise NativePathLeaseError("Native path changed after it was selected.")
    if grant.path_kind == "document-folder" and not identity_options:
        raise NativePathLeaseError("Native path grant is missing its folder identity.")
    current_identity = (st.st_dev, st.st_ino)
    expected_identity = _runtime_identity(identity_options)
    if expected_identity is not None and current_identity != expected_identity:
        raise NativePathLeaseError("Native path changed after it was selected.")
    return current_identity if expected_identity is not None else None


def _consume_nonce(nonce: str, expires_at_ms: int) -> None:
    now_ms = int(time.time() * 1000)
    with _REPLAY_LOCK:
        for key, expiry in list(_USED_NONCES.items()):
            if expiry <= now_ms:
                _USED_NONCES.pop(key, None)

View on GitHub (pinned to 203007d190)

Solutions

  1. Wait for writes to finish, then re-select the file to get a grant with the final size.
  2. stat -c '%s' <path> and compare with size_bytes from the decoded payload to confirm which snapshot is stale.
  3. Exclude actively-written files from selection (copy to a stable location first), then re-select.
  4. For sync clients, wait for the 'synced' state before picking the file.

Example fix

# before
pick file while downloader still writes it -> size grows -> grant stale

# after
$ while [ "$(stat -c %s model.gguf)" != "$LAST" ]; do LAST=$(stat -c %s model.gguf); sleep 2; done
# download settled; re-select file and resubmit
Defensive patterns

Strategy: validation

Validate before calling

import base64, json, os

def size_matches_grant(lease: str) -> bool:
    payload = json.loads(base64.urlsafe_b64decode(lease.split('.')[0] + '=='))
    expected = payload.get('size_bytes')
    if expected is None:
        return True
    try:
        return os.lstat(payload['canonical_path']).st_size == int(expected)
    except OSError:
        return False

Try / catch

try:
    grant = verify_native_path_lease(lease, operation='read')
except NativePathLeaseError as exc:
    if 'changed after it was selected' in str(exc):
        lease = request_fresh_grant()  # file mutated; re-pin size/mtime
        grant = verify_native_path_lease(lease, operation='read')
    else:
        raise

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15). Data as JSON: /api/errors/e7b9058176a29aec. Report an issue: GitHub.