Skip to content

App locking hardening - #31

Open
rawe0 wants to merge 13 commits into
masterfrom
app-locking-hardening
Open

App locking hardening#31
rawe0 wants to merge 13 commits into
masterfrom
app-locking-hardening

Conversation

@rawe0

@rawe0 rawe0 commented Aug 11, 2026

Copy link
Copy Markdown
Member

No description provided.

rawe0 and others added 11 commits August 11, 2026 15:43
- FileLockedException now maps to a FileLockedError (OSError subclass) so
  lock conflicts are distinguishable from file-not-found and other errors.
- acquire_or_refresh_lock returns held/foreign/no_file instead of a bool;
  new ensure_write_lock (423 on foreign holder), lock_after_create and
  foreign_lock_holder (exact holder match via the revived get_lock).
- lock_holder property computed after traitlets config, fixing the init
  ordering that made the per-user suffix a no-op; the suffix is now opt-out
  via lock_holder_suffix_client_id so migrating to a shared holder is a
  config change.
- cs3_token initialized before reading the token file (no AttributeError
  when the file is missing); config key lock_as_attr renamed to
  lock_by_setting_attr, the key cs3client actually reads.
- unlink/rename now carry the lock id; new vfs_touch; sync copyfile_sync.
- SessionTracker.user_left returns None for unknown sessions (no more
  spurious unlocks) and sweep() supports the new background refresher that
  keeps open documents' locks alive and releases stale ones.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
POST/DELETE now require path+session_id, map the API path to the storage
path once, run all gRPC calls via run_sync, report the foreign holder on
conflicts, 404 unknown files/sessions instead of unlocking, and start the
background lock refresher.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hybrid manager:
- open() locks only for write modes and no longer falls back to FUSE (the
  fallback double-yielded on flush errors and turned lock conflicts into
  unlocked local writes).
- New files/notebooks/directories and empty-file touch are created via CS3
  and locked right after, instead of writing through the FUSE mount where
  reva could not see (or lock) them.
- hybrid_lstat/exists/isfile/isdir fall back to a CS3 stat on a local miss,
  fixing the create-then-stat race while FUSE catches up; exists/file_exists/
  dir_exists/_base_model/get/_dir_model/_save_directory use them.
- rename/delete reimplemented via CS3 (carrying the lock id) with a
  foreign-holder pre-check, since cephmount enforces nothing on writes.
- lock_is_ours (FUSE xattr substring match) replaced by CS3 GetLock exact
  holder matching, only checked when content is requested so listings
  don't pay one GetLock per entry.
- sync save() drops the upstream checkpoint block that awaited nothing,
  and delegates chunked uploads to LargeFileManager whose appends now
  resolve to the CS3 open (chunks no longer mix CS3 and FUSE backends).

Pure manager:
- saves acquire the lock first (423 when foreign), then lock new files
  after creation; empty new files use touch instead of a zero-byte PUT.
- models mark foreign-locked files read-only; delete/rename pre-check the
  holder and translate lock conflicts to 423.
- chunk-aware save() (copied from AsyncLargeFileManager) so chunked uploads
  assemble instead of each chunk truncating the file.

Checkpoints: restoring the "0_0" placeholder is a no-op and a locked
restore surfaces as 423.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tests cover the tri-state acquire against an in-memory CS3 fake with
EOS-like lock semantics, holder exact-matching and the shared-holder
migration, session tracking, the /lock endpoints through a real jupyter
server, both managers' lock-aware save/delete/rename/model flows, the
FUSE-miss fallbacks, chunked uploads, and the background refresher.

README documents the locking traits and why tus_enabled must stay off
(cs3-python-client sends a misspelled X-Lock_Holder header on its TUS
branch; to be fixed upstream).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New locking plugin hooks IDocumentWidgetOpener.opened: each opened
document POSTs /lock with a per-widget session id, heartbeats every
expiration/3 (expiration read from GET /lock), follows renames, and
DELETEs on close so the server can unlock when the last session leaves.
A missed close (crashed tab) is covered by the server-side sweep.
Foreign-locked documents are reported and open read-only via the
contents model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Creating a notebook failed with a 500 because acquire_or_refresh_lock
assumed a missing file surfaces as NOT_FOUND. It does not: the EOS driver
wraps the not-found while resolving the reference, so the storage
provider's type switch falls through to a generic INTERNAL error, which
cs3client raises as UnknownException and our handler turned into a bare
OSError that escaped the acquire.

RefreshLock is now treated as a pure optimization - any failure falls
through to SetLock, which is authoritative. SetLock's own failures are
disambiguated by an existence check, so a file that is not there yet
reports no_file (it gets locked right after creation) while a genuine
storage error still propagates. Foreign locks are unaffected: both
"not the holder" and "already locked" come back as FAILED_PRECONDITION.

The same opacity is handled where it could break the surrounding
operation: lock_after_create stays best-effort, foreign_lock_holder fails
open rather than failing the read or delete it annotates, and the
background refresher survives storage errors instead of dying. The
unknown-error fallback now keeps the original message, which is the only
thing distinguishing these conditions.

The test fake now mirrors reva's real error shapes (per-driver via
missing_file_error), which is why this slipped through before; without
the fix its new-notebook test reproduces the reported 500.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The config example used c.CS3FileManagerMixin.*, which the hybrid manager
does not inherit from - those settings were silently ignored for it.
CS3Mixin is the only class in both managers' MRO.

Also document the labextension's locking plugin and what happens without
it installed (locks taken on save, released only by expiry).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rawe0
rawe0 force-pushed the app-locking-hardening branch 2 times, most recently from 95f5429 to e5ae6d4 Compare August 11, 2026 14:43
@tomasr8
tomasr8 self-requested a review August 12, 2026 12:18
@rawe0
rawe0 force-pushed the app-locking-hardening branch from e5ae6d4 to f0e9594 Compare August 17, 2026 08:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant