test: shallow snapshot retains deleted rich-text style values (privacy leak) - #1057
Open
nightscape wants to merge 1 commit into
Open
test: shallow snapshot retains deleted rich-text style values (privacy leak)#1057nightscape wants to merge 1 commit into
nightscape wants to merge 1 commit into
Conversation
Exporting a shallow snapshot at the current frontiers is documented as a content-redaction mechanism. Deleted character content is correctly dropped from the export, but the value of a rich-text style op whose entire range has been deleted is still shipped verbatim, and survives import + re-export. The test asserts both, so the passing character control documents the asymmetry; the style assertion is red today.
nightscape
marked this pull request as ready for review
July 30, 2026 12:10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains only a failing test. I can try to come up with a fix, but want to leave that up to you.
Summary
A shallow-snapshot export retains the value of a rich-text style op whose entire marked range has been deleted. Deleted character content is correctly dropped from the same export, so this is an asymmetry rather than "shallow snapshots keep history".
Minimal repro (single peer, no concurrency needed):
Every read API says the data is gone:
text.to_string()is"", and neither secret appears inget_deep_value(). Only the raw exported bytes still carry the style value. It also survivesimportinto a fresh doc followed by a re-export, so it cannot be laundered by round-tripping.The shallow-snapshots concepts page has a "Content Redaction" section that presents this exact export as the way to sanitize a document (https://loro.dev/docs/concepts/shallow_snapshots, source:
loro-dev/loro-docspages/docs/concepts/shallow_snapshots.mdx). Its example is structurally identical to the repro:That promise holds for the plain-text case the docs show, and breaks the moment the secret is a mark value instead of characters.
redactAPI from feat: redact #504 does null outTextOp::Markvalues — but only on the JSON-updates path (crates/loro-internal/src/encoding/json_schema.rs).crates/loro-internal/src/encoding/shallow_snapshot.rshas no equivalent redaction pass: feat: redact #504So the two documented sanitization routes disagree about whether a dead style value is sensitive.
Suspected mechanism
calc_shallow_doc_start(crates/loro-internal/src/encoding/shallow_snapshot.rs:318) walks a multi-head frontier back to the LCA, and the retained op tail from that start is shipped unredacted. Ops at the start frontier are always re-included, and a start landing on aStyleStartis advanced so the Start/End pair stays together — which keeps the mark op, and with it its value, in the export. In the single-peer repro above the start is already the tip, and the style value still ships, so the retained-tail encoding path looks like the place to fix rather than the frontier calculation itself.With a two-peer concurrent frontier (2 heads), the LCA regression is much broader. In my probing the deleted character content leaked too, because history gets retained back to the common ancestor. That is arguably expected given the frontier can't be trimmed to a multi-head point, but it means "export at current frontiers to sanitize" is unsafe in exactly the collaborative case.
Red test output
The failure is on the style assertion. The deleted-character control assertion immediately above it passes, which is the point: the export does redact character content, just not style values.
Context
I hit this in production: a local-first app that prunes document history before sharing a document with another peer. The pruning uses exactly the documented
shallow_snapshot(doc.frontiers())flow, and mark values on deleted text are user-authored comment/annotation content — so the shared bytes carried data the UI had already shown as deleted.