Repository navigation
Conversation
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>
b60ace8 to
c3ac261
Compare
|
Postponed to 0.8.1: old draft, not part of 0.8.0. |
…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
|
Round 3 review: fixes needed (C+). Ticket, headers and size limits are fixed. |
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
|
Round 4 at Generated by Claude Code |
|
CI red at baaf15c: Generated by Claude Code |
|
Round 4 at baaf15c: fixes needed (C+). Old record without custody journal: save its hash only if the file matches the published CID. |
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
…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)
|
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
|
Merged develop ( Generated by Claude Code |
|
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. |
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
|
Generated by Claude Code |
|
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. |
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
|
Merged develop 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
|
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
|
Generated by Claude Code |
Last merged
developatc97c549ein4df92a9f(earlier atbfefe92fin0efcc74dand at8ea4aac9inbf96fa06). develop has since moved to3a043849(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 anauth.rsconflict, 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) and7151dc87(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 byf0b9e679),89d62f90(port of619c0961from #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):fix/zip-9-rustsec(deps: move zip to 9.x for RUSTSEC-2026-0367/0368/0369 #417) at7e1b4d9f, so CI'scargo-auditcan run;1890c243, a cherry-pick of develop's50515055(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) of619c0961from Reclaim completed Browser bridge workers and preserve finite shutdown #411's branch: develop'shome_external_link_names_the_launched_app_and_the_exact_addresslaunches Browser, which CI and this host cannot launch (403 vs 200); the port uses the System fixture, assertions unchanged.0efcc74d: merge oforigin/developatbfefe92f. 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 in89d62f90is superseded. This branch's ownhome_system.rstests are unchanged. At0efcc74d: Library JS tests 38 pass;cargo test -p elastos-server --lib -- home_external_link background plain_publish_cid image_bounds29 passed.4df92a9f: merge oforigin/developatc97c549e. 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 inauth.rs(it removedread_principal_root_object_bytesanddecrypt_principal_root_object_bytes, addedopen_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 ofprotect_legacy_plaintext_principal_root_object); this branch's oldread_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'sread_principal_root_object_locked,read_protected_principal_root_object_fileandread_principal_root_object_restare now no-limit wrappers of_limitedvariants, 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'sopen_principal_root_object_file(no final link, regular file only).desktop_background.rsis unchanged by the merge. zip stays 9.0.4 (elastos-server/Cargo.toml, both lock files); develop addsmemchr.4f1bd73eaddsauth::tests::principal_root_object_limited_read_refuses_stored_bytes_past_the_limitandauth::tests::principal_root_object_rest_read_refuses_a_file_that_grows_past_the_limit_after_open;a968ffc5addsauth::tests::principal_root_object_limited_read_refuses_a_link_to_a_valid_protected_objectandauth::tests::principal_root_object_limited_read_refuses_links_and_non_regular_objects(below).What a person sees
capsules/library/browser/src/thumbnails.js,image-header.js). The thumbnail cache is bounded by bytes (below).model.jsisDesktopBackgroundCandidate); Runtime still decides. When Runtime refuses, Library shows Runtime's plain reason (for example, which protected file to move back).Why the wallpaper was broken
Cross-Origin-Embedder-Policy: require-corp; the wallpaper response carried noCross-Origin-Resource-Policy, so the browser dropped it. Nowcross-origin+nosniff.url()cannot carry theSameSite=Strictsession. They now fetch with their launch token and apply an object URL (with caching, no retry storms).GET /api/apps/home/appearance/background-imageonly accepted the Home host token; the active shell that renders the desktop was refused. It now usesrequire_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
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.rsissue_desktop_background_ticket; at most 8 pending per session). Library postshome:set-desktop-background { uri, requestId, ticket }; the Home host accepts it fromlibraryonly (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.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 oldlibrary_tokenfield all answer 403 or 4xx (tested).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.rsread_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.require_unprotectedrefuses 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.protected_content_runtime.rsprepare_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 bysame_authority_as) and stages that same buffer for the media provider (stage_runtime_media_provider_input).publish_runtime_custody_library_sourcereturns that journal digest (also on a replay of a consumed preparation),runtime_custody_publish_via_gatewaypasses it on, andlibrary_publishstores it asLibraryPublishRecord::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.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/addwith 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 throughwrite_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.spawn_blocking(gateway_home_system.rsread_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.rsProtectionCheckBudget); 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 iskeep_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". Beforef0b9e679the blocking thread saved after an atomicLIVE → SAVINGstep; 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.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).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 exactlyElastOS cannot check an older protected file. To use this picture, upload it in System > Background.(desktop_background.rsDesktopBackgroundSourceRefusal::OlderProtectedFileUncheckable, fromLegacySource::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.model.jsdesktopBackgroundRefusalMessage).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 = trueinconfig.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) dropssource_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.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.image_bounds.rsand JSimage-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.Decisions recorded
7151dc87; see "Recovery").c15d375b). 8192 pixels per side and 24 million pixels (image_bounds.rsIMAGE_MAX_SIDE/IMAGE_MAX_PIXELS, the one source of truth;image-header.jscopies them andimage-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 intest_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).thumbnails.jsTHUMBNAIL_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 (nocreateImageBitmapdownscale), 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.Open questions for review
require_home_active_shell_token_contextaccepts 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.Follow-ups
Verification on
7151dc87(round 7, team decision)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_recoversand..._legacy_records_not_in_place_are_countedstill get the "move it back" text.return Ok(LegacySource::Uncheckable)lines indesktop_background.rsLegacyCheck::source_digestsback toLegacySource::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 (theif uncheckablerefusal moved after the "move it back" one) →..._comes_before_move_backfails with the "move it back" text formoved.png; (3) theif uncheckablerefusal 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 newauthtests).a968ffc5, review follow-up). Both tests callread_principal_root_object_limitedwith 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 forPictures/linked.pngthroughwrite_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 (mknod1:3, only where the test may make one: it is skipped with a note whenmknodis not permitted, as on a non-root CI runner) are all refused.read_principal_root_object_locked_limitedandread_protected_principal_root_object_file_limited,open_principal_root_object_file(path)replaced byFile::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") andread 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 twoprincipal_root_object_*tests pass with the bypass, as expected.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 fromdesktop_background.rs's own link check (:330) beforeauth.rsis reached, so that test does not catch theauth.rsbypass; the two tests above do.4f1bd73e's tests (one build, then restored):Some(max_stored_bytes)→Noneinread_principal_root_object_limited, and the limit ignored inread_principal_root_object_rest_limited→ both fail (unwrap_err()on anOk: 10 bytes returned at limit 9; 4106 bytes returned at limit 10 after the file grew past it).4f1bd73e,-- background plain_publish_cid image_bounds home_external_link auth library principal_rootwithout 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 andELASTOS_TEST_{PROTECT,DECRYPT,CUSTODY}_PROVIDER_BINset, those 5 pass, and-- runtime_custody background home_external_linkgives 116 pass, 1 fail (runtime_custody_open_pending_survives_active_write_failure_and_failed_cleanup, the root-host case noted under89d62f90). The sixth,gateway_tests::remote_model::denial_authority::a_denial_that_could_not_be_saved_cancels_nothing, makes a folder0o500and 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 --offlineforelastos/andcapsules/object-provider/: consistent. Ona968ffc5:cargo fmt --all -- --check,cargo clippy -p elastos-server --all-targets -- -D warnings,git diff --check: pass. On4f1bd73ealsonode 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.4f1bd73e(run 39219919850): every job passed, includingtest-elastos,cargo-audit,lint,source-home-macosandsource-home-linux(ubuntu-24.04 and ubuntu-24.04-arm).Verification on
f0b9e679(round 6)All Rust runs went through the shared-target wrapper (
/home/user/cargo-wt.sh).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: anOkanswer has the hash saved, a 400 answer has no hash saved.0efcc74dwith only the seam added (the save passed in askeep, still called on the check's thread afterbegin_save), it fails withthe handler said the check did not finish, but a hash was saved,left: Some("sha256:c0cf…"),right: None.read_desktop_background_source_within,tokio::task::spawn_blocking(check)replaced by a closure that callskeep(found)on the check's thread, and the handler'sspawn_blocking(move || keep(found)).awaitremoved → the new test and..._check_answering_late_saves_nothingfail (both with recorded digestSome("sha256:c0cf…"), expectedNone).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
89d62f90All 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 withELASTOS_TEST_{PROTECT,DECRYPT,CUSTODY}_PROVIDER_BINset (-- runtime_custody home_external_link: 99 pass, includingtest_runtime_custody_publish_records_the_bytes_custody_protectedand bothhome_external_linktests). 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 directory0o500and expects the write to fail); it is untouched by this PR.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 throughread_desktop_background_source_withinwith 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 inimage-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.rswrite_test_legacy_publish_record);..._legacy_check_is_boundednow tests the byte limit with two sources and checks that a source larger than the whole budget fails closed.desktop_background.rsLegacyCheck::source_digests, theplain_publish_file_cid(&bytes) != Some(record.cid)early return removed; (2) the custody-without-journalreturn Ok(LegacySource::NotInPlace)removed (falls through to the file); (3) ingateway_home_system.rsread_desktop_background_source_within,&& live.begin_save()removed (code since replaced byf0b9e679) →..._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 digestSome("sha256:c0cf…"), expectedNone) fail; the other 9test_home_background_from_objecttests pass. JS: the byte bound removed fromthumbnails.jsremember→ the cache test fails;IMAGE_MAX_SIDEback to 16384 inimage-header.js→ "a tiny file that declares a huge canvas" and "the limits are the ones Runtime applies" fail.baaf15c5, then restored): (A)library_publishback to a second read before custody, the legacy caps set tofalse, 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 digestNone) 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_recoversand..._not_in_place_are_countedfail. JS:actions.jsback to its fixed message → the new test indesktop-background.test.mjsfails.581426c2with 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, andimage_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) inrequire_unprotected, the loop overrecordsreplaced by an empty list; (2) inconsume_desktop_background_ticket,tickets.remove(..)replaced bytickets.get(..); (3)image_bounds.rspng_size/jpeg_size/gif_size/webp_sizerestored to the previous head. For JS,image-header.jsrestored to the previous head → the four header tests inimage-header.test.mjsfail.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.Earlier history: on
63818af0(before the first round of fixes)gateway_tests::home_system101 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-smokepass;library-menu-smokefails the same way on plain develop ("Protect and List..." menu item), unrelated. CI on63818af0(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