Skip to content

linux: a wrap that starts over reads the disk again - #644

Merged
ronleizrowice-ant merged 2 commits into
mainfrom
fix/linux-wrap-restart-reads-again
Oct 3, 2026
Merged

ronleizrowice-ant merged 2 commits into
mainfrom
fix/linux-wrap-restart-reads-again

Conversation

@ronleizrowice-ant

@ronleizrowice-ant ronleizrowice-ant commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes the blocker found in review after #638 was merged. It is on main and in no release.

What was wrong

Since #638 a wrap that starts over kept the directories it had listed and each pattern's finished walk. So, while a command is being wrapped: the host writes proj/key.pem, then calls updateConfig() with a rule that denies proj/**/*.pem. The wrap starts over, walks the new rule over a listing of proj/ made before the file was there, and hands out a command that reads the file. The same with the same rules installed again.

#638 called that "as old as one long walk". That was wrong: the rule was installed after the file existed, and the wrap claims to apply it.

What changes

  • A wrap that starts over keeps nothing. The invariant, now written down and tested: what a wrap hands out was read from the disk after its configuration was installed. sandbox-utils.ts and read-deny-glob.ts are again what they were before linux: a wrap that starts over keeps what it has walked, and ends #638.
  • The bound stays. After two restarts it walks without giving the thread back, as in linux: a wrap that starts over keeps what it has walked, and ends #638. That keeps timers and I/O callbacks out. It is not a proof: a promise continuation, or anything that runs while the network is still being initialized, can still replace the configuration between the walk and the last check, and the wrap starts over once more.
  • New: an attempt is left as soon as its configuration is seen to be replaced, not walked to its end for nothing. A symbol stands in for the result, so the type check makes both call sites handle it. Never an empty list.

Not kept for unchanged rules either: a rule dropped and installed again within one attempt looks unchanged from both ends.

Who sees a difference

A caller whose configuration is replaced twice during one wrap: the third attempt holds the thread for one whole walk, as every wrap did before #628. One replacement holds nothing.

Measured

The two cases above, real bubblewrap, 400,150 files, 3 runs each: denied before #638, read on main, denied here.

Six ** rules over the same tree, Node 22 / Bun 1.4.2, as multiples of an undisturbed wrap (about 1 s), on a busy machine:

updateConfig() during the wrap before #638 main here
once, halfway 2.2 / 1.9 1.0 / 1.0 1.2 / 1.4
every 250 ms 6.9 / 6.7 1.0 / 1.0 1.2 / 1.6, thread held 1.2 / 0.7 s
a new rule every 150 ms 6.9 / 9.5 1.3 / 1.2 1.0 / 1.4, thread held 1.1 / 0.8 s

Testing

Four new cases: each of the two above, with the replacement in the middle of the walk and in its last listing. #638's four cases on keeping are gone, and its case on the bound is adapted. Seven mutations, each failing a test; five cases fail on main's source. Linux full suite: nothing that passes on main fails here, those four aside.

Since #638 a wrap that starts over kept the directories it had listed and
each pattern's finished walk. A host that wrote a file and then installed
a rule denying it, while a command was being wrapped, got a command that
could read the file: the rule was walked over a listing made before the
file was there.

A wrap that starts over now keeps nothing, so what it hands out was read
after its configuration was installed. It still ends: after two restarts
it walks without giving the thread back. And an attempt is left as soon as
its configuration is seen to be replaced, not walked to its end.
ronleizrowice-ant added a commit that referenced this pull request Oct 2, 2026
It undoes the join of #638: the kept walks, the anchor in their key and
their charge to the budget go with the walks themselves. The budget and
the listings are again made by each attempt. Of the three join cases one
stays, under another name: a budget that arrives during the denyRead walk
is the one the wrap ends by.
Comments only. The walk without a turn keeps timers and I/O callbacks out,
not promise continuations, nor anything that runs while the network is
still being initialized, so it is not a proof that the wrap ends. And a
configuration installed after the last check is the next wrap's.

@antdres antdres left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/stamp

@antdres antdres left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers. This fixes the problem reported on #638.

Checked on Linux with real bubblewrap, head 26469d8, same steps as in the #638 review (a wrap is in flight on a 63,000-entry tree; the host writes proj/key.pem, then calls updateConfig() with a rule that denies proj/**/*.pem):

result of cat proj/key.pem
before #638 (60e5e9f) Permission denied
main (c7adb1e) prints the file's contents
this PR Permission denied

Same result on every run (3 of 3 each side).

With the same rules installed again in place of a new rule, at 4 ms steps across the wrap: the file is read in 22 of 23 timings on main and 3 of 24 here. Those 3 are replacements that land after the wrap's last configuration check, while the mandatory-deny search runs. The code before #638 lets them through as well (6 of 23), and so does v0.0.78, so that is not this PR's.

The new tests do their job: with main's source under them, five fail (the four new cases and the adapted one on the bound); with this PR's source all pass.


Generated by Claude Code

@ronleizrowice-ant
ronleizrowice-ant merged commit 9e93406 into main Oct 3, 2026
16 checks passed
@ronleizrowice-ant
ronleizrowice-ant deleted the fix/linux-wrap-restart-reads-again branch October 3, 2026 12:33
@ronleizrowice-ant ronleizrowice-ant mentioned this pull request Oct 4, 2026
ronleizrowice-ant added a commit that referenced this pull request Oct 4, 2026
#620, #621, #622 and #644 as merged upstream. The bundle held all four,
joined with the budget, so of the nine blocks git marks it keeps its own
side. What is new to it is the second commit of #644: comments.
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.

2 participants