Skip to content

Wallpaper from Library: CORP + shell-token wallpaper path, image thumbnails, Set as Desktop Background - #67

Draft
SashaMIT wants to merge 35 commits into
developfrom
feat/library-set-desktop-background
Draft

SashaMIT wants to merge 35 commits into
developfrom
feat/library-set-desktop-background

Conversation

@SashaMIT

@SashaMIT SashaMIT commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Last merged develop at c97c549e in 4df92a9f (earlier at bfefe92f in 0efcc74d and at 8ea4aac9 in bf96fa06). develop has since moved to 3a043849 (7 Home/changelog commits, #451/#452); not merged here. Lands after 0.8.0 (Q1), after #417.

Head: 7151dc87. Round 7 changes: 4df92a9f (develop merge with an auth.rs conflict, below), 4f1bd73e (tests that hold the ported limited read to its bound), a968ffc5 (review follow-up: tests that the limited read refuses links and non-regular objects) and 7151dc87 (team decision: a separate plain message for an older protected file that no file can prove; see "Recovery" below). Round 6 change: f0b9e679 (found digests are saved only after the check answered within the 15 s limit, by the handler, before it answers; see "Bounded, off the async thread, and nothing saved late" below). Round 5 changes: bf96fa06 (develop merge), c15d375b (limit B: 8192 px / 24 MP, byte-bounded thumbnail cache), 8b0b8100 (a legacy digest is saved only from a file with the published CID, and only while the request is still live; the "still live" state machine is replaced by f0b9e679), 89d62f90 (port of 619c0961 from #411, below). Round 3 fixes: e4b0a1a7 (protection follows the bytes), 9aef6b9e (single-use ticket), cee323e0 (Rust headers), 581426c2 (JS headers). Round 4 fixes: e64a3255 (the record names the bytes custody protected), 6a90d14f (bounded legacy check, kept digests, recovery message), baaf15c5 (Library shows Runtime's reason). This branch also carries (not part of the wallpaper change):

  • the zip 9 patch, merged from fix/zip-9-rustsec (deps: move zip to 9.x for RUSTSEC-2026-0367/0368/0369 #417) at 7e1b4d9f, so CI's cargo-audit can run;
  • 1890c243, a cherry-pick of develop's 50515055 (model-provider: journal tests fail when a child process holds a copied journal lock #381, model-provider held-lock journal test retry);
  • 0f9bfd4a, the test(people): the first-run layout smoke waits for the rendered Profile text #422 port (People first-run layout smoke waits for the rendered Profile text);
  • 89d62f90, a cherry-pick (-x) of 619c0961 from Reclaim completed Browser bridge workers and preserve finite shutdown #411's branch: develop's home_external_link_names_the_launched_app_and_the_exact_address launches Browser, which CI and this host cannot launch (403 vs 200); the port uses the System fixture, assertions unchanged.
  • 0efcc74d: merge of origin/develop at bfefe92f. develop fixed the Chat and app web links require Home confirmation of the exact destination (0.8.0 F1 gate) #430 Home link test itself (it uses Library); every conflicting hunk was inside that one test and takes develop's side, so the test is identical to develop's and the port in 89d62f90 is superseded. This branch's own home_system.rs tests are unchanged. At 0efcc74d: Library JS tests 38 pass; cargo test -p elastos-server --lib -- home_external_link background plain_publish_cid image_bounds 29 passed.
  • 4df92a9f: merge of origin/develop at c97c549e. develop (Home cannot start, update or show Recovery with a file over 16 MiB (or 64 MiB total) in Library folders #444) rewrote the principal-root object read path in auth.rs (it removed read_principal_root_object_bytes and decrypt_principal_root_object_bytes, added open_principal_root_object_file, classifies a protected file before any decrypt, and caps lazy legacy protection at 16 MiB). Both conflicting hunks take develop's side (open_principal_root_object_file, and the body of protect_legacy_plaintext_principal_root_object); this branch's old read_principal_root_object_bytes{,_limited} are gone. read_principal_root_object_limited (this branch's bounded read for the desktop background) is ported onto develop's path: develop's read_principal_root_object_locked, read_protected_principal_root_object_file and read_principal_root_object_rest are now no-limit wrappers of _limited variants, so develop's other callers behave as on develop. With a limit, a file longer than the limit at open is refused before any byte is read, and the rest read stops one byte past the limit and refuses. Under protection every develop check stays (classification, not-encrypted and damaged refusals, whole-envelope AES-GCM). Both the limited and the no-limit path open the object through develop's open_principal_root_object_file (no final link, regular file only). desktop_background.rs is unchanged by the merge. zip stays 9.0.4 (elastos-server/Cargo.toml, both lock files); develop adds memchr. 4f1bd73e adds auth::tests::principal_root_object_limited_read_refuses_stored_bytes_past_the_limit and auth::tests::principal_root_object_rest_read_refuses_a_file_that_grows_past_the_limit_after_open; a968ffc5 adds auth::tests::principal_root_object_limited_read_refuses_a_link_to_a_valid_protected_object and auth::tests::principal_root_object_limited_read_refuses_links_and_non_regular_objects (below).

What a person sees

  • Uploading a wallpaper in System now actually changes the desktop, and System's own preview shows it.
  • The System window no longer jumps when the file picker closes.
  • Library shows image thumbnails instead of the generic icon (lazy, ≤ 4 MB per file). Thumbnails are made only for PNG, JPEG, GIF and WebP whose header is complete and well formed and declares at most 8192 pixels per side and 24 million pixels; anything else keeps its icon (capsules/library/browser/src/thumbnails.js, image-header.js). The thumbnail cache is bounded by bytes (below).
  • Library's transient status lives in the footer and never pushes the toolbar controls.
  • Right-click an image in Library → Set as Desktop Background. The wallpaper changes in place; System does not open. The menu item is hidden for protected, blocked, unreadable, Trash and Runtime-private objects (model.js isDesktopBackgroundCandidate); Runtime still decides. When Runtime refuses, Library shows Runtime's plain reason (for example, which protected file to move back).
  • A picture whose header declares a canvas too large to decode, or whose header is broken, is refused on upload and on Set as Desktop Background ("use a picture of at most 8192 pixels per side and 24 million pixels"). A picture of that kind stored earlier is not served (422), so Home shows the default desktop and System's Reset still works. With limit B a 6000×4000 (24 MP) camera picture is accepted; a 48 MP phone picture (8000×6000) is refused.

Why the wallpaper was broken

  1. The gateway enforces Cross-Origin-Embedder-Policy: require-corp; the wallpaper response carried no Cross-Origin-Resource-Policy, so the browser dropped it. Now cross-origin + nosniff.
  2. Home GUI and System render from opaque sandboxed frames, so a CSS url() cannot carry the SameSite=Strict session. They now fetch with their launch token and apply an object URL (with caching, no retry storms).
  3. GET /api/apps/home/appearance/background-image only accepted the Home host token; the active shell that renders the desktop was refused. It now uses require_home_active_shell_token_context, the same authority the shell already uses to write preferences. Ordinary app tokens still get 403 (tested).

Set as Desktop Background — authority model

  • Library only expresses intent and never gains appearance authority; a Library token used as the caller's authority gets 403 (tested).
  • Library's intent is a single-use ticket, not its launch token. Library asks POST /api/apps/library/desktop-background-ticket { source_uri } with its own launch token (Library, launched by Home). Runtime returns a random 256-bit ticket bound to that person, session and object, valid for 60 seconds, kept in memory by its SHA-256 (gateway_home_system.rs issue_desktop_background_ticket; at most 8 pending per session). Library posts home:set-desktop-background { uri, requestId, ticket }; the Home host accepts it from library only (exact keys, localhost://Users/ URI, 43-character base64url ticket) and carries only the ticket. Home no longer sends Library's 12-hour launch token anywhere.
    • Why single-use rather than a shorter launch token: Library uses its launch token for all its own requests, so shortening it or consuming it on use would break Library or allow only one wallpaper change per Library window. A per-action ticket is single-use (removed on first presentation, whatever the outcome), short-lived, and names one object, so a captured request cannot be replayed and Home never holds a Library capability it could reuse for another file. A Runtime restart drops pending tickets; the person chooses the action again.
  • POST /api/apps/home/appearance/background-image { source_uri, library_ticket } needs Home's appearance authority on the request and a ticket issued for the same person, session and object (home_background_image_set_from_object → consume_desktop_background_ticket). Home's or the shell's token alone, another app's token, another person's or another session's Library token, a truncated token, a replayed ticket, an expired ticket, a ticket for another object, a made-up ticket, and the old library_token field all answer 403 or 4xx (tested).
  • Runtime then reads the file through Library's scope (elastos/crates/elastos-server/src/library/desktop_background.rs): the principal's own root only, never Trash or .AppData, no ./.. segments in plain, %2e, or double-encoded form, no \, encoded //\, NUL or control characters, no symbolic link at any component (checked, then opened; see Library desktop background read: open each path part without following links #425 for the swap race), and never protected content (below). The read is bounded through one open descriptor (auth.rs read_principal_root_object_limited), which itself refuses a final link and anything that is not a regular file, and each request is audited as a Library object operation. Bytes never traverse a capsule; Home never reads or decrypts the object.
  • Protection follows the bytes. A copy, rename or move gives a protected file a new URI with no publish record, so a check by URI alone let it through. Now require_unprotected refuses an object when its own record is protected (as before) or when its bytes' SHA-256 equals the source digest of any protected record the person holds.
    • The digest is of the bytes custody protected. Custody reads the source once (protected_content_runtime.rs prepare_runtime_custody_library_source → read_runtime_media_source), hashes that buffer (runtime_media_source_digest), binds the digest into its journal record (RuntimeMediaPreparationRecord::source_object_digest, compared again on every retry by same_authority_as) and stages that same buffer for the media provider (stage_runtime_media_provider_input). publish_runtime_custody_library_source returns that journal digest (also on a replay of a consumed preparation), runtime_custody_publish_via_gateway passes it on, and library_publish stores it as LibraryPublishRecord::source_sha256. There is no second read of the source path, so a file that changes during or after the publish cannot make the record name other bytes.
    • Legacy records (written before source_sha256): for a Runtime custody record the digest comes from custody's media preparation journal, so it does not matter where the file is now. Otherwise the record's own file is hashed only if it still has the record's published CID (8b0b8100): Runtime computes the CID the way Library's plain publish gets it from Kubo's /api/v0/add with no options (CIDv0, SHA-256, dag-pb UnixFS, 256 KiB chunks, balanced, ≤ 174 links; library/plain_publish_cid.rs, checked against Kubo 0.40.1 output for 0 B, 12 B, 300 000 B and 1 000 000 B). A file that changed after it was published, a CID this check does not compute, or a source larger than one request may read (32 MiB) saves nothing and fails closed with the older-protected-file message (below). A custody record names the CID of its encrypted payload, so without custody's journal its file can never prove its bytes: it is refused the same way, and its file is not read. A digest found is kept in the record (owner-only atomic write through write_principal_root_object_if_revision, only if the record is unchanged since it was read), so this is done once and the file can move later.
    • Bounded, off the async thread, and nothing saved late: the read and check run in spawn_blocking (gateway_home_system.rs read_desktop_background_source → read_desktop_background_source_within(timeout, check, keep)). One request hashes at most 8 legacy sources and 32 MiB and stops after 5 s (desktop_background.rs ProtectionCheckBudget); the handler waits at most 15 s for the check. The check and its blocking thread write nothing: the check returns the digests it found (DesktopBackgroundRead::found). Only when the timed join answers in time does the handler save them (keep, which is keep_found_source_digests, on another blocking thread), and it waits for that save before it answers; a check that answers after the timeout is dropped and saves nothing (f0b9e679). So a hash is never saved after the answer "did not finish". Before f0b9e679 the blocking thread saved after an atomic LIVE → SAVING step; a slow save already in progress when the timeout passed let the handler answer "did not finish" while the save completed afterwards. The save after a timely check (a few small record writes) is not under the 15 s limit. Past a limit: "Library did not finish the check of your protected files. Try again." (fail closed; digests found by a check that stopped at its own 5 s / 8 / 32 MiB budget are kept, so the next try goes on). More than 16 384 records, or a record that cannot be read, refuses every picture.
    • Recovery (two messages; only older records, with no saved hash, reach either one; a record with a hash is checked by it and never needs a file):
      • The file is not at its place (moved, deleted, or not a regular file there): the refusal names the file and the next step, because moving it back lets the check find and keep its hash: Library cannot check your protected file "clip.png" because it is not in "Pictures". Move it back to "Pictures", then try again. Or upload the picture in System > Background > Choose image. (plus "Then do the same for N more protected files." when there are more).
      • No file can prove the bytes (team decision, 7151dc87): a custody record with no digest in custody's journal, a file that no longer has the record's published CID (changed in place, or a CID form this check does not compute), or a file larger than one request may read. Here the file is in place or moving it cannot help, so the refusal is exactly ElastOS cannot check an older protected file. To use this picture, upload it in System > Background. (desktop_background.rs DesktopBackgroundSourceRefusal::OlderProtectedFileUncheckable, from LegacySource::Uncheckable). It comes before the "move it back" message when both kinds are present, because such a record blocks every picture whatever is moved back.
      • Home passes Runtime's 400 sentence to Library (at most 600 characters) and Library shows it only when it is one line with no control characters (model.js desktopBackgroundRefusalMessage).
    • Accepted gap (Sash's decision): protection follows exact bytes. An edited or re-encoded copy of protected content has a new SHA-256, so it is not refused (documented in desktop_background.rs's module comment); nothing short of a protection provider marking the bytes can catch that. Protected content is opt-in (protected_content = true in config.toml), and Library offers Protect only for video (isRuntimeCustodyProtectableVideo), so legacy records exist only on test installs. An older Runtime that rewrites a record (share, unpublish) drops source_sha256; the record then takes the legacy path again. Library's menu still offers Set as Desktop Background on a copy (it has no protection metadata); Runtime refuses it.
  • Format and canvas come from the bytes (elastos/crates/elastos-server/src/image_bounds.rs, header-only, no allocation in proportion to the declared size). The System upload's label must match the bytes.
  • Header reading fails closed (Rust image_bounds.rs and JS image-header.js, same rules): PNG needs the full 33-byte signature + IHDR (length 13, type, defined bit depth / colour type / compression / filter / interlace, sides ≤ 2^31−1) and a matching IHDR CRC. JPEG walks at most 1024 marker segments to the first frame header; every segment must fit in the file; stuffed bytes, restart markers, a second SOI, EOI or SOS before the frame are refused; the frame needs precision 2–16, 1–4 components and the matching length. WebP's RIFF size must not exceed the file, the first chunk must fit in the RIFF payload, VP8 must be a key frame with version ≤ 3 and a first partition inside the chunk, VP8L must have version 0, VP8X must be 10 bytes and be followed by another chunk. GIF needs the 13-byte header, colour tables and sub-blocks that fit, at least one frame inside the logical screen, and the trailer. Bytes after a WebP RIFF payload are ignored, as decoders ignore them.
  • System's Reset still clears it.

Decisions recorded

  • Protected content stays refused as wallpaper (decided by the team: option (a)). This includes the owner's own protected picture. Revisit only when a protection provider can grant a "display as wallpaper" use.
  • Older protected records that no file can prove get their own plain message (team decision, 7151dc87; see "Recovery").
  • Q4 — decode limits: limit B, decided by Sash (c15d375b). 8192 pixels per side and 24 million pixels (image_bounds.rs IMAGE_MAX_SIDE / IMAGE_MAX_PIXELS, the one source of truth; image-header.js copies them and image-header.test.mjs "the limits are the ones Runtime applies" reads the Rust file and fails when they differ; the refusal text is checked against the constants in test_background_image_refuses_a_decompression_bomb). One decode at the pixel limit is about 96 MB as RGBA (24 M × 4 bytes). The wallpaper decodes one at a time (two while System's preview is open).
  • Thumbnail cache bounded by bytes (thumbnails.js THUMBNAIL_CACHE_MAX_BYTES = 256 MiB, plus the old count limit of 160): each entry is charged width × height × 4 (from the header) plus its file size, and the oldest entries are released (their object URLs revoked) once the total passes 256 MiB; the newest entry always stays. That keeps at most two 24 MP pictures, or about forty 1920×1080 ones. Not done: thumbnails still decode at full size (no createImageBitmap downscale), and up to 3 load at a time (≈ 290 MB in flight at 24 MP); the bound covers what the cache keeps alive, not what the browser holds for <img> elements on screen.
  • Edited or re-encoded copies pass (above); accepted by Sash.

Open questions for review

  • require_home_active_shell_token_context accepts Home, System, Home GUI and Home CLI tokens without checking which shell is currently active. Same as the existing preferences write; tightening it would change every route that shares it.
  • Set as Desktop Background applies without a Home-owned confirmation. Add one, or keep Library's own menu click as the consent?

Follow-ups

Verification on 7151dc87 (round 7, team decision)

  • New message ElastOS cannot check an older protected file. To use this picture, upload it in System > Background. for an older protected record with no saved hash that no file can prove. Tests: gateway_tests::home_system::test_home_background_from_object_legacy_custody_record_without_journal_fails_closed (the exact new text, and no "Move it back"), ..._legacy_record_changed_after_publish_keeps_nothing (changed in place: the new text), ..._legacy_check_is_bounded (too large for any request: OlderProtectedFileUncheckable), and new ..._older_protected_file_message_comes_before_move_back (a custody record with no journal next to a moved older record: the new text, no "Move it back"). ..._legacy_record_not_in_place_names_the_file_and_recovers and ..._legacy_records_not_in_place_are_counted still get the "move it back" text.
  • Reverted to prove them (then restored): (1) the three return Ok(LegacySource::Uncheckable) lines in desktop_background.rs LegacyCheck::source_digests back to LegacySource::NotInPlace → 4 fail, e.g. left: "Library cannot check your protected file \"legacy.png\" because it is not in \"Pictures\". Move it back to \"Pictures\", then try again. Or upload the picture in System > Background > Choose image." right: "ElastOS cannot check an older protected file. To use this picture, upload it in System > Background."; (2) only the order changed (the if uncheckable refusal moved after the "move it back" one) → ..._comes_before_move_back fails with the "move it back" text for moved.png; (3) the if uncheckable refusal removed → the custody and changed-file tests answer 200 instead of 400, so that line is also what keeps these records failing closed.
  • cargo test -p elastos-server --lib -- background plain_publish_cid image_bounds home_external_link principal_root_object_limited principal_root_object_rest: 35 pass, 0 fail. cargo fmt --all -- --check, cargo clippy -p elastos-server --all-targets -- -D warnings, git diff --check: pass. Library JS: 39 pass.

Verification on a968ffc5 (round 7)

All Rust runs went through the shared-target wrapper (/home/user/cargo-wt.sh).

  • cargo test -p elastos-server --lib -- background plain_publish_cid image_bounds home_external_link principal_root_object_limited principal_root_object_rest: 34 pass, 0 fail (the 30 from round 6 plus the 4 new auth tests).
  • Links and non-regular objects (a968ffc5, review follow-up). Both tests call read_principal_root_object_limited with a 1 MiB limit, far above every file, so only the open can refuse. ..._refuses_a_link_to_a_valid_protected_object: under protection, an envelope is written for Pictures/linked.png through write_principal_root_object, moved outside the data folder, and a symbolic link to it is put at the object's path; read where it is, it opens to the plaintext, and read through the link it is refused. ..._refuses_links_and_non_regular_objects (no protection): a link to a regular file, a link to /dev/null, a directory, and a character device (mknod 1:3, only where the test may make one: it is skipped with a note when mknod is not permitted, as on a non-root CI runner) are all refused.
    • Reverted to prove them (one build, then restored): in read_principal_root_object_locked_limited and read_protected_principal_root_object_file_limited, open_principal_root_object_file(path) replaced by File::open(path) plus its length when a limit is set (the review's bypass, limited path only) → both fail: a link at the object path was followed: Ok([108, 105, 110, 107, 101, 100, 32, 115, 101, 99, 114, 101, 116, ...]) (the plaintext "linked secret image") and read anyway: ["to-file.png (13 bytes)", "to-device.png (0 bytes)", "device.png (0 bytes)"]. The directory is refused with the bypass too (reading a directory fails), so it pins the rule but does not catch the bypass. The other two principal_root_object_* tests pass with the bypass, as expected.
    • Through the wallpaper route, a link to the person's own protected picture is already refused by test_home_background_from_object_refuses_traversal_encoding_and_links (gateway_tests/home_system.rs:7071-7086, alias.png, with protection stored for that root). That refusal comes from desktop_background.rs's own link check (:330) before auth.rs is reached, so that test does not catch the auth.rs bypass; the two tests above do.
  • Reverted to prove 4f1bd73e's tests (one build, then restored): Some(max_stored_bytes) → None in read_principal_root_object_limited, and the limit ignored in read_principal_root_object_rest_limited → both fail (unwrap_err() on an Ok: 10 bytes returned at limit 9; 4106 bytes returned at limit 10 after the file grew past it).
  • Wider run on 4f1bd73e, -- background plain_publish_cid image_bounds home_external_link auth library principal_root without the provider binaries: 536 pass, 6 fail. 5 (gateway_tests::library::test_runtime_custody_* ×3, protected_content_runtime::tests::runtime_custody_library_publish_* ×2) panic with "missing test binary env: ELASTOS_TEST_PROTECT_PROVIDER_BIN" (CI sets it, .github/workflows/ci.yml:364-366); with the three provider binaries built from that head and ELASTOS_TEST_{PROTECT,DECRYPT,CUSTODY}_PROVIDER_BIN set, those 5 pass, and -- runtime_custody background home_external_link gives 116 pass, 1 fail (runtime_custody_open_pending_survives_active_write_failure_and_failed_cleanup, the root-host case noted under 89d62f90). The sixth, gateway_tests::remote_model::denial_authority::a_denial_that_could_not_be_saved_cancels_nothing, makes a folder 0o500 and expects the save to fail, which this host (root) does not honour; its file is identical to develop's and this PR does not touch that code (reasoned from the code, not re-run on develop).
  • node --test capsules/library/browser/src/*.test.mjs: 39 pass (develop added one). node --test capsules/home/browser/*.test.mjs: 31 pass. cargo metadata --locked --offline for elastos/ and capsules/object-provider/: consistent. On a968ffc5: cargo fmt --all -- --check, cargo clippy -p elastos-server --all-targets -- -D warnings, git diff --check: pass. On 4f1bd73e also node scripts/check-product-data.mjs, node --test scripts/check-product-data.test.mjs (99 pass), cargo fmt --manifest-path capsules/chain-provider/Cargo.toml -- --check, node scripts/public-copy-entropy-check.mjs: pass.
  • CI on 4f1bd73e (run 39219919850): every job passed, including test-elastos, cargo-audit, lint, source-home-macos and source-home-linux (ubuntu-24.04 and ubuntu-24.04-arm).
  • Not run this round: the browser smokes.

Verification on f0b9e679 (round 6)

All Rust runs went through the shared-target wrapper (/home/user/cargo-wt.sh).

  • New negative test gateway_tests::home_system::test_home_background_from_object_slow_save_never_follows_check_unfinished: the real check runs first, so the injected check answers at once; the injected save (keep) sleeps 1 s; the timeout is 200 ms. After the runtime shuts down (which waits for a save still running) it asserts: an Ok answer has the hash saved, a 400 answer has no hash saved.
  • Fails before the fix: on 0efcc74d with only the seam added (the save passed in as keep, still called on the check's thread after begin_save), it fails with the handler said the check did not finish, but a hash was saved, left: Some("sha256:c0cf…"), right: None.
  • Reverted to prove it (one build, then restored): in read_desktop_background_source_within, tokio::task::spawn_blocking(check) replaced by a closure that calls keep(found) on the check's thread, and the handler's spawn_blocking(move || keep(found)).await removed → the new test and ..._check_answering_late_saves_nothing fail (both with recorded digest Some("sha256:c0cf…"), expected None).
  • cargo test -p elastos-server --lib -- background plain_publish_cid image_bounds home_external_link: 30 pass, 0 fail. node --test capsules/library/browser/src/*.test.mjs: 38 pass. cargo fmt --all -- --check, cargo clippy -p elastos-server --all-targets -- -D warnings, git diff --check, node scripts/public-copy-entropy-check.mjs: pass.

Verification on 89d62f90

All Rust runs below went through the shared-target wrapper (/home/user/cargo-wt.sh), which rebuilds the workspace crates from this worktree.

  • cargo test -p elastos-server --lib -- background plain_publish_cid image_bounds home_external_link: 29 pass, 0 fail. -- image_bounds desktop_background background plain_publish_cid gateway_tests::library gateway_tests::home_system runtime_custody_library: 230 pass; the 5 that failed there need the provider helper binaries and pass with ELASTOS_TEST_{PROTECT,DECRYPT,CUSTODY}_PROVIDER_BIN set (-- runtime_custody home_external_link: 99 pass, including test_runtime_custody_publish_records_the_bytes_custody_protected and both home_external_link tests). One failure there, protected_content_runtime::tests::runtime_custody_open_pending_survives_active_write_failure_and_failed_cleanup, is this host running as root (the test makes a directory 0o500 and expects the write to fail); it is untouched by this PR.
  • New round 5 tests: gateway_tests::home_system::test_home_background_from_object_{legacy_record_changed_after_publish_keeps_nothing, legacy_custody_record_without_journal_fails_closed, check_answering_late_saves_nothing} (the last injects a slow check through read_desktop_background_source_within with a 20 ms timeout, then waits for the blocking thread by shutting its runtime down, and checks no hash was saved; the same check in time keeps it), library::plain_publish_cid::tests::{matches_kubo_default_add, a_file_too_large_for_one_level_is_unknown}, and in image-header.test.mjs "the limits are the ones Runtime applies" and "the thumbnail cache evicts once its pictures pass the byte limit". Legacy fixtures now have a record CID that matches their file (library.rs write_test_legacy_publish_record); ..._legacy_check_is_bounded now tests the byte limit with two sources and checks that a source larger than the whole budget fails closed.
  • Reverted to prove the round 5 negative tests (one build, then restored): (1) in desktop_background.rs LegacyCheck::source_digests, the plain_publish_file_cid(&bytes) != Some(record.cid) early return removed; (2) the custody-without-journal return Ok(LegacySource::NotInPlace) removed (falls through to the file); (3) in gateway_home_system.rs read_desktop_background_source_within, && live.begin_save() removed (code since replaced by f0b9e679) → ..._legacy_record_changed_after_publish_keeps_nothing (copy: 200, expected 400), ..._legacy_custody_record_without_journal_fails_closed (200, expected 400) and ..._check_answering_late_saves_nothing (recorded digest Some("sha256:c0cf…"), expected None) fail; the other 9 test_home_background_from_object tests pass. JS: the byte bound removed from thumbnails.js remember → the cache test fails; IMAGE_MAX_SIDE back to 16384 in image-header.js → "a tiny file that declares a huge canvas" and "the limits are the ones Runtime applies" fail.
  • Reverted to prove the round 4 negative tests (three builds on baaf15c5, then restored): (A) library_publish back to a second read before custody, the legacy caps set to false, the custody journal lookup removed → test_runtime_custody_publish_records_the_bytes_custody_protected (copy: 200, expected 400), ..._legacy_check_is_bounded (200, expected 400) and ..._legacy_custody_record_uses_custody_journal (400 "cannot check", expected 200) fail; (B) keeping the digest turned off → ..._checked_once_and_kept, ..._custody_journal, ..._not_in_place_names_the_file_and_recovers (recorded digest None) and ..._check_is_bounded (second try 400) fail; (C) not-in-place back to the old "cannot confirm" refusal → ..._not_in_place_names_the_file_and_recovers and ..._not_in_place_are_counted fail. JS: actions.js back to its fixed message → the new test in desktop-background.test.mjs fails.
  • Round 3 negative count: 9 Rust negative tests. Re-run on 581426c2 with the three round 3 reverts in one build; these 9 fail: test_home_background_from_object_refuses_protected_bytes_after_copy_rename_and_move, test_home_background_from_object_legacy_protected_record_fails_closed (since replaced), test_home_background_from_object_ticket_is_single_use_and_short_lived, and image_bounds::tests::{truncated_png_headers_are_refused, forged_png_headers_are_refused, truncated_and_forged_jpeg_headers_are_refused, jpeg_segment_walk_is_bounded, truncated_and_forged_webp_headers_are_refused, truncated_and_forged_gif_headers_are_refused}. The round 3 reverts: (1) in require_unprotected, the loop over records replaced by an empty list; (2) in consume_desktop_background_ticket, tickets.remove(..) replaced by tickets.get(..); (3) image_bounds.rs png_size/jpeg_size/gif_size/webp_size restored to the previous head. For JS, image-header.js restored to the previous head → the four header tests in image-header.test.mjs fail.
  • node --test capsules/library/browser/src/*.test.mjs: 32 pass. node --test capsules/home/browser/*.test.mjs: 34 pass.
  • git diff --check, node scripts/check-product-data.mjs, node --test scripts/check-product-data.test.mjs (99 pass), node scripts/public-copy-entropy-check.mjs, cargo fmt --all -- --check, cargo fmt --manifest-path capsules/chain-provider/Cargo.toml -- --check, cargo clippy -p elastos-server --all-targets -- -D warnings: pass.
  • Not re-run this round: the browser smokes. No browser test drives Set as Desktop Background end to end (JS test: Home carries only Library's desktop background ticket #427). The 15 s timeout itself is not exercised (the late-save tests inject 20 ms and 200 ms ones through the same function).

Earlier history: on 63818af0 (before the first round of fixes) gateway_tests::home_system 101 pass; Library unit tests 48 pass; home-shell-regression-smoke, home-agent-shell-smoke, library-product-behavior-smoke, system-uiux-behavior-smoke, home-library-picker-smoke pass; library-menu-smoke fails the same way on plain develop ("Protect and List..." menu item), unrelated. CI on 63818af0 (run 38932131083): source-home-linux (ubuntu-24.04) stopped in the 1 GiB journey with "installed Runtime exited before health"; the evidence does not keep the cause (no OOM; arm64 and develop passed the same journey). Nothing in this PR runs before health.

Part of #93.

🤖 Generated with Claude Code

https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K

SashaMIT and others added 9 commits September 29, 2026 21:58
The gateway enforces Cross-Origin-Embedder-Policy: require-corp and the
Home GUI desktop renders from an opaque sandboxed frame. The background
image route only set Content-Type, so Chrome blocked the uploaded
wallpaper with NotSameOriginAfterDefaultedToSameOriginByCoep even though
the bytes were saved and served. Mirror the capsule asset route and
assert the header in the existing round-trip test.

Co-authored-by: Cursor <cursoragent@cursor.com>
The Home session cookie is SameSite=Strict, so a CSS url() request from
the opaque Home GUI frame cannot be relied on to carry it. Fetch the
image with x-elastos-home-token like every other Home request, apply it
as an object URL keyed by the versioned summary URL, revoke on change,
and fall back to the default wallpaper if the fetch fails.

Co-authored-by: Cursor <cursoragent@cursor.com>
The dialog heading already names the file; writing 'Previewing …' into
the toolbar pushed the search and view controls. Clear the status on
preview and cap #status-text so no transient message can shift layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
Render image files with their own thumbnail instead of the generic icon.
Only on-screen items are fetched (IntersectionObserver), at most three
at a time, capped at 4 MB, through the download authority Library
already holds; object URLs are released past 160 entries. Prefers
thumbnail_uri when the Runtime provides one.

Co-authored-by: Cursor <cursoragent@cursor.com>
The wallpaper GET only accepted the Home host token, yet the desktop is
drawn by the active shell (home-gui) from an opaque frame with its own
launch token, so every fetch returned 403 and the default wallpaper stayed.
Accept the same authority the appearance-preferences POST already grants
the active shell; ordinary app tokens are still refused.

Co-authored-by: Cursor <cursoragent@cursor.com>
…er preview

Chrome refocuses a file input when its picker closes and scrolls every
scrollable ancestor to reveal it, including overflow:hidden
.settings-container, which shifted the whole System window up. Hidden
file inputs are now viewport-anchored so nothing needs to scroll.

The background preview used a CSS url() that cannot carry the session
from an opaque frame; load it through the launch token like other
System requests.

Co-authored-by: Cursor <cursoragent@cursor.com>
Status text in the toolbar displaced the search and view controls
whenever a message appeared. Move it to the statusbar, where it takes the
spare middle space and truncates first, and drop the file name from the
preview loading message.

Co-authored-by: Cursor <cursoragent@cursor.com>
…bjects

POST /api/apps/home/appearance/background-image { source_uri } lets the
appearance authority (Home host, System, active shells) make a stored object
the wallpaper. The Runtime reads the protected object itself under the
caller's principal root, sniffs PNG/JPEG/WebP/GIF from the bytes, applies the
same size rule as the byte upload and saves through the same path. App
tokens are refused; foreign, missing, non-image and non-localhost sources
answer 400.

Co-authored-by: Cursor <cursoragent@cursor.com>
Library expresses the intent (home:set-desktop-background) and never gains
appearance authority. The Home host accepts it from Library only, with exact
message keys and a localhost://Users/ URI, performs the change with its own
authority and refreshes the summary so the desktop repaints. The menu item is
offered for PNG/JPEG/WebP/GIF objects within the wallpaper size limit.

Co-authored-by: Cursor <cursoragent@cursor.com>
@andersalm

Copy link
Copy Markdown
Contributor

Postponed to 0.8.1: old draft, not part of 0.8.0.

@andersalm andersalm modified the milestones: 0.8.0, After 0.8.0 Oct 13, 2026
SashaMIT and others added 6 commits October 16, 2026 22:39
…esktop-background

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	capsules/home-gui/browser/shell-core.js
A small file can declare a canvas that takes gigabytes to decode. The
byte limit alone did not stop that, so one such picture could break the
desktop and the System preview used to reset it.

Runtime now reads only the image header (PNG IHDR, JPEG frame header,
GIF screen and frames, WebP VP8/VP8L/VP8X), allocates nothing in
proportion to the declared size, and refuses more than 16384 pixels per
side or 50 million pixels. An unreadable header is refused. The check
runs on the System upload (the label must also match the bytes), on the
set-from-object path, and on serving: a picture stored earlier that
fails it answers 422, so Home shows the default desktop and System can
still reset it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
Library made a thumbnail from any image of 4 MB or less. A tiny file
can declare a huge canvas, and the browser decodes the full canvas for
an <img>, so one such picture could stall or crash the Library window.

Library now reads the header of the bytes it already downloaded and
keeps the file icon when the canvas is over 16384 pixels per side or 50
million pixels, or when the header cannot be read. Thumbnails are made
only for PNG, JPEG, GIF and WebP; SVG and other formats keep the icon.
The limits match Runtime's wallpaper check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
…bility

The set-from-object route let Home's appearance authority read and
decrypt any file under the person's root, including Trash, Runtime
records under .AppData, and protected content, into a persistent
wallpaper copy. That is a read Library itself would not make, and it
bypassed Library's capability (PRINCIPLES 3, 7, 15, 16).

Now two authorities must both be present for the same person and
session: Home's appearance authority on the request, and the launch
token of the Library frame that asked, which Home carries in the body
(the same pattern as the wallet connector and passkey step-up). The
token must be a Library token launched by Home. Runtime then reads the
file through Library's own scope: the principal's root, never Trash or
Runtime-private areas, never through a link, and only content with no
publish record or a plain, keyless publish record. A record that cannot
be read counts as protected. The read is bounded through one open
descriptor. Each request is audited as a Library object operation.

Library's menu also hides the action for protected, blocked, unreadable
and Runtime-private objects; Runtime still decides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
The set-from-object route reads a file from disk on Home's request, so
each way to name a file outside the person's own Library has a negative
test: plain `..` and `.` segments, `%2e%2e` and double-encoded
`%252e%252e`, encoded `/` and `\` separators, Windows backslash paths,
raw and encoded NUL and other control characters, another person's
object, and links (a file link out of the data directory, a link to the
person's own file, a directory link into another person's Pictures, and
a directory link at the top of the root). A Unicode name matches only
its stored bytes (NFC accepted; NFD refused on Linux). Trash and
Runtime-private areas are covered by the capability test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
cargo-audit fails on every PR: zip 2.4.2 in elastos/Cargo.lock and in
capsules/object-provider/Cargo.lock (through its elastos-server path
dependency) has three advisories against ZipArchive::extract and the
stream extract API. The runtime calls neither; it reads each entry
itself. zip 9.0.4 is the patched release.

- elastos-server uses zip 9.0.4, still without default features. 9.x has
  no separate flate2 feature; deflate-flate2 now enables the dependency.
- Both lockfiles change only for zip: nix 0.31.3 and typed-path come in,
  derive_arbitrary goes. `cargo update -p zip` also moved unrelated range
  dependencies (windows-sys, getrandom); those lines stay as they were,
  and `cargo metadata --locked` accepts both files.
- ZipFile::name now returns a Result. The ZIP readers report an error as
  "invalid ZIP archive entry".
- Whole-archive extract keeps its rule for entry names. zip 9
  enclosed_name turns "/a.txt" into "a.txt" and folds "x/../a.txt" into
  "a.txt", so an entry that extract refuses today would be written. The
  runtime now checks the name with the rule zip 2.x applied: no NUL byte,
  no root or drive prefix, no climb above the archive root.

test_library_provider_extract_zip_refuses_absolute_dotdot_and_symlink_entries
covers absolute, "..", inner "..", backslash and symlink entries. It
passes on zip 2.4.2 and with this change, and fails on the Absolute.zip
case when extract uses zip 9 enclosed_name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
@SashaMIT

Copy link
Copy Markdown
Contributor Author

Round 3 review: fixes needed (C+). Ticket, headers and size limits are fixed.
Record the digest of the bytes custody protects; hash legacy records off the async handler with a cap.
Add a recovery path when a moved legacy record blocks wallpapers. Body: 9 Rust negatives, not 11.

The record's source_sha256 came from a second read of the source, made
before custody read it. If the file changed in between, the record named
other bytes than the ones custody protected, and a copy of the protected
bytes could be set as the desktop background.

Custody already binds the SHA-256 of the exact plaintext it reads and
stages for the media provider into its media preparation record
(RuntimeMediaPreparationRecord::source_object_digest). The custody publish
now returns that digest, also on a replay of a consumed preparation, and
the publish record stores it. The second read is gone.

Test: test_runtime_custody_publish_records_the_bytes_custody_protected
changes the source after Library starts the publish and before custody
reads it (a one-shot hook in the mock Chain provider), then shows a copy of
the protected bytes is refused, also after the source changes again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
A protected record written before records named their digest (a legacy
record) was checked by hashing its source on the async request thread,
with no limit, on every request. If its file had been moved, every picture
was refused for good, with no way out.

Now:
- Library's read and protection check run on a blocking thread. One request
  hashes at most 8 legacy sources and 32 MiB, and stops after 5 seconds;
  the handler waits at most 15 seconds and saves nothing from a late read.
  Past a limit it refuses: "Library did not finish the check of your
  protected files. Try again."
- A legacy Runtime custody record takes its digest from custody's media
  preparation journal: the bytes custody protected, wherever the file is.
- A digest found is kept in its record (owner-only atomic write, only if the
  record is unchanged since it was read), so the work is done once and the
  file can move later.
- A legacy record whose bytes cannot be found still refuses every picture,
  and the refusal names the file, its folder and the next step: move it
  back, or upload the picture in System > Background > Choose image.

Tests: legacy record checked once and kept; custody journal used when the
file is gone; not-in-place refusal names the file, then recovers after the
person moves it back; several not-in-place records are counted; the check
is bounded by sources, bytes and time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
Library showed only "Could not set this image as the desktop background",
so the person never saw Runtime's next step (for example, which protected
file to move back).

Home now passes Runtime's 400 sentence back to Library (at most 600
characters; any other failure stays a plain "not accepted"). Library shows
it only when it is one short line with no control characters, and keeps its
own message otherwise.

Tests: desktop-background.test.mjs checks which text is shown and that
long, multi-line or control-character text is not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K

Copy link
Copy Markdown
Contributor Author

Round 4 at baaf15c5: the protected record now stores custody's own journal digest of the bytes it protected (e64a3255, test test_runtime_custody_publish_records_the_bytes_custody_protected changes the source mid-publish). Legacy records are checked off the async thread within limits (8 sources, 32 MiB, 5 s), take their digest from the custody journal, keep it, and when the file is gone the refusal names it and says how to recover; Library shows that text (6a90d14f, baaf15c5, tests test_home_background_from_object_legacy_*).
The body now says 9 round 3 Rust negatives, not 11, and lists them; every round 4 negative test fails when its fix is reverted (see Verification).


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

CI red at baaf15c: source-home-linux (ubuntu-24.04) job 117508216882, model-repair stage=model_interrupted_get_retry code=timeout check=restarted_home. This is the same failure #415 and #417 hit (tracked in #433).
Why it is not this PR's: home-failure.png shows Home ready after the restart, desktop visible and Marketplace opening. The 60 s wait that ran out is the Marketplace step after that, and this PR does not touch Marketplace. No fix exists yet. I can't re-run (403), so it needs one manual re-run of run 39051786892.


Generated by Claude Code

@SashaMIT

Copy link
Copy Markdown
Contributor Author

Round 4 at baaf15c: fixes needed (C+). Old record without custody journal: save its hash only if the file matches the published CID.
A check finishing after the 15 s timeout must not save a hash. Merge develop (67 behind).
Decision (Sash): limit B (8192 px / 24 MP) plus a byte-limited cache; an edited copy passing is acceptable if documented.

No textual conflicts. The branch keeps zip 9 from #417; develop still
pins zip 2.4.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
claude and others added 3 commits October 18, 2026 02:09
…y bytes

Sash chose limit B. Runtime refuses a desktop background, and Library
keeps the file icon for a thumbnail, when the header declares more than
8192 pixels per side or 24 million pixels. One decode at the pixel limit
is about 96 MB as RGBA (was about 200 MB at 50 MP).

image_bounds.rs holds the limits; image-header.js copies them and a node
test fails when the copies differ. The refusal text is checked against
the constants.

Library's thumbnail cache is now bounded by bytes as well as by count:
each entry is charged width x height x 4 plus its file size, and the
oldest entries are released past 256 MiB (at most two 24 MP pictures).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
…d CID, in time

Two ways a legacy protected record (one with no source digest) could be
given the wrong digest, or one nobody waited for:

- The record's own file was hashed while in place, even when it had
  changed after it was published. Now, when custody's journal has no
  digest, the file is hashed only if it still has the record's published
  CID, computed the way Library's plain publish gets it from Kubo
  (CIDv0, 256 KiB chunks, balanced, 174 links; plain_publish_cid.rs,
  checked against Kubo 0.40.1). Otherwise nothing is saved and every
  picture is refused with the existing recovery message. A custody
  record names the CID of its encrypted payload, so without custody's
  journal its file cannot prove its bytes and it is refused the same
  way. A source larger than one request may read is refused too,
  instead of being skipped as "cannot match".

- The check saved digests from its blocking thread, also after the
  handler's 15 s timeout had given up. Now the check writes nothing and
  returns what it found; the blocking thread saves it only after an
  atomic step shows the handler still waits, and the handler takes that
  step away when it times out.

Tests: legacy_record_changed_after_publish_keeps_nothing,
legacy_custody_record_without_journal_fails_closed,
check_answering_late_saves_nothing (slow check injected), and the Kubo
CID vectors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
Use the installed System fixture for valid and invalid Home-link requests so the test exercises link authority on every host. Keep all assertions and production launch checks unchanged.

Verified the exact Home-link test passes, reverting only the System token to Browser fails the intended 403-versus-200 assertion, and restoring the fixture passes again.

Co-authored-by: Codex
(cherry picked from commit 619c096)

Copy link
Copy Markdown
Contributor Author

89d62f90: develop merged (bf96fa06). A legacy record's digest is now saved only if its file still has the published CID, and only while the request is live (8b0b8100). Limit B (8192 px / 24 MP) and a 256 MiB thumbnail cache (c15d375b).
Proof: test_home_background_from_object_{legacy_record_changed_after_publish_keeps_nothing, legacy_custody_record_without_journal_fails_closed, check_answering_late_saves_nothing} fail with the CID check and begin_save reverted, and pass with them in place. JS: "the thumbnail cache evicts once its pictures pass the byte limit".
Also ports 619c0961 (Home link test uses System). Edited copies still pass; this is the accepted gap, and the body says so.


Generated by Claude Code

develop now carries its own fix for the #430 Home link test (bfefe92:
the test uses Library, which every platform launches). This branch had
ported #411's 619c096 for the same test (System instead of Browser),
and the two edits conflicted in gateway_tests/home_system.rs. Every
conflicting hunk was inside that one test, so each takes develop's side;
the test is now identical to develop's and 89d62f9's change is
superseded. This branch's own home_system.rs tests are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K

Copy link
Copy Markdown
Contributor Author

Merged develop (0efcc74d). develop fixed the Home link test itself in bfefe92f (Library), and that conflicted with the port in 89d62f90 (System). Every conflict was inside that one test, so the test is now develop's.
Library JS tests 38 pass; home_external_link background plain_publish_cid image_bounds 29 passed.


Generated by Claude Code

@SashaMIT

Copy link
Copy Markdown
Contributor Author

Re-review at 0efcc74: FAIL (C). Only a develop merge; the late-save race is still open (gateway_home_system.rs:8175-8191). A slow save that overlaps the 15 s timeout still saves a hash after CheckUnfinished.
Fix: wait for an in-progress save before returning, or persist only after a successful timed join; add that slow-save test and show it fails without the fix.

The check's thread saved the digests it found once it had moved the
request from LIVE to SAVING. A slow save could still be running when the
15 s timeout passed: the handler could no longer give up, but it answered
"Library did not finish the check" anyway, and the save finished after
that answer.

Now the check's thread writes nothing. The handler saves the found
digests only when the timed join answered, and it waits for that save
before it answers. A check that answers late is dropped. This removes the
shared LIVE/SAVING/GAVE_UP state. The save still uses the record's
revision precondition.

New test: test_home_background_from_object_slow_save_never_follows_check_unfinished
(an instant check with a 1 s save against a 200 ms timeout). It fails when
the save is moved back onto the check's thread, inside the timed join.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K

Copy link
Copy Markdown
Contributor Author

f0b9e679: the check's thread no longer writes. The handler saves found digests only after the check answered within 15 s, and waits for that save before it answers, so no hash is saved after "did not finish".
Proof: test_home_background_from_object_slow_save_never_follows_check_unfinished (1 s save, 200 ms timeout) fails on 0efcc74d and with the save moved back onto the check's thread; it passes now (30 Rust, 38 Library JS tests pass).


Generated by Claude Code

@SashaMIT

Copy link
Copy Markdown
Contributor Author

Independent review (GPT) at f0b9e67: PASS (A-). Late-save race closed: a timeout saves nothing; a found digest is saved only after a timely check. Moving the save back inside the timed check fails the slow-save test.
CI green. Before landing: merge develop (trial merge conflicts in auth.rs). Open team item: separate message for old protected records.

develop (c97c549, #444) rewrote the principal-root object read path in
auth.rs: it removed read_principal_root_object_bytes and
decrypt_principal_root_object_bytes, added open_principal_root_object_file
(O_NOFOLLOW, regular file, length at open), classifies a protected file
from a bounded head before any decrypt (plaintext is refused as not
encrypted, a damaged file is named), and makes lazy legacy protection
read at most 16 MiB through the open descriptor. This branch (06b6240)
had added read_principal_root_object_limited for the desktop background:
the old read, but the stored bytes are bounded through the one open
descriptor so a file that grows after the size check is refused.

Conflicting hunks in auth.rs:
- The old read_principal_root_object_bytes{,_limited} signatures against
  develop's open_principal_root_object_file: take develop's side. The old
  functions are gone; nothing calls them after this merge.
- The old bounded read_to_end body against develop's
  protect_legacy_plaintext_principal_root_object body: take develop's
  side unchanged (its 16 MiB cap and classification stay as they are).

read_principal_root_object_limited (merged cleanly) still called the
removed functions, so it is ported onto develop's path instead of the
old one. It now calls read_principal_root_object_locked_limited, which
is develop's read_principal_root_object_locked with an optional limit;
read_principal_root_object_locked is that function with no limit, and
read_protected_principal_root_object_file is likewise a no-limit wrapper
of read_protected_principal_root_object_file_limited, so develop's other
callers behave exactly as on develop. With a limit, a file longer than
the limit at open is refused before any byte is read, and
read_principal_root_object_rest_limited (develop's
read_principal_root_object_rest plus the limit) reads at most one byte
past it and refuses. Under protection the limited read keeps every
develop check (classification, not-encrypted and damaged refusals,
whole-envelope AES-GCM); protection is loaded before any byte is read,
as develop does. desktop_background.rs needs no change: it maps every
read error to its refusal, as before.

zip stays 9.0.4 (elastos-server/Cargo.toml, elastos/Cargo.lock,
capsules/object-provider/Cargo.lock); develop adds memchr 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K
…p's read path

The develop merge ported read_principal_root_object_limited onto develop's
new read path. These tests hold it there: without protection and under
protection a stored object one byte past the limit is refused (and one at
the limit is read), plaintext inside the limit under protection is still
refused as not encrypted, and a file that grows after open is refused by
read_principal_root_object_rest_limited while the no-limit path still
reads the whole file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K

Copy link
Copy Markdown
Contributor Author

Merged develop c97c549e in 4df92a9f: both auth.rs hunks take develop's #444 read path; read_principal_root_object_limited is ported onto it as no-limit wrappers, so develop's callers are unchanged and the desktop background keeps its one-descriptor bound.
4f1bd73e adds principal_root_object_limited_read_refuses_stored_bytes_past_the_limit and ..._rest_read_refuses_a_file_that_grows_past_the_limit_after_open (both fail with the limit removed). -- background plain_publish_cid image_bounds home_external_link principal_root_object_*: 32 pass; Library JS 39 pass; fmt, clippy -D warnings, zip 9.0.4 with --locked: pass.


Generated by Claude Code

…ular objects

Review found that opening the object with File::open on the limited path
(following links, accepting any file type) left every test green. These
tests call read_principal_root_object_limited with a limit far above the
file sizes, so only the open can refuse:
- a symbolic link at the object path to an envelope that is bound to that
  object and opens when read where it is (protected root);
- a link to a regular file, a link to /dev/null, a directory, and a
  character device made with mknod when the test may make one (root
  without protection).
With File::open on the limited path the protected link returns the
plaintext and the links and device return their bytes; the directory is
refused either way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K

Copy link
Copy Markdown
Contributor Author

a968ffc5 adds principal_root_object_limited_read_refuses_a_link_to_a_valid_protected_object and ..._refuses_links_and_non_regular_objects (1 MiB limit, so only the open can refuse): a link to a correctly bound envelope, a link to a file and to /dev/null, a directory, and a mknod device (skipped where not permitted).
With File::open on the limited path (the review's bypass), both fail: the plaintext comes back through the link, and the file link, /dev/null link and device read 13/0/0 bytes. The directory is refused either way. Restored: 34 pass; fmt, clippy -D warnings and diff --check pass. CI run 39219919850 on 4f1bd73e: all jobs green.


Generated by Claude Code

… prove

Team decision for #67. An older protected record (written before records
kept a source hash) whose bytes no file can prove now refuses with:
"ElastOS cannot check an older protected file. To use this picture,
upload it in System > Background."

That covers a custody record with no digest in custody's journal, a file
that no longer has the record's published CID (changed in place, or a
CID form the check does not compute), and a file too large for any one
request to read. For these the old "it is not in <folder>, move it back"
text was wrong: the file is in place, or moving it cannot help. The old
text stays for an older record whose file is missing (or not a regular
file) at its place, where moving it back lets the check find its hash.
Records that keep a hash never reach either message. When both kinds
are present the new message comes first, because the record no file can
prove blocks every picture whatever is moved back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W6Qn9RX7LDqrwiAfNC2F2K

Copy link
Copy Markdown
Contributor Author

7151dc87: an older protected record (no saved hash) that no file can prove now refuses with "ElastOS cannot check an older protected file. To use this picture, upload it in System > Background." This covers a custody record with no journal digest, a file changed since it was published (CID mismatch), and a file too large to check. A missing or moved file keeps the "move it back" text.
Test: test_home_background_from_object_legacy_custody_record_without_journal_fails_closed checks for the exact new text and no "Move it back". It, plus 3 related tests including the new ..._older_protected_file_message_comes_before_move_back, fails when the three LegacySource::Uncheckable returns are reverted to NotInPlace. Restored: 35 pass; fmt, clippy -D warnings and diff --check pass.


Generated by Claude Code

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.

4 participants