Skip to content

chore: sync develop with main - #542

Merged
chodeus merged 1 commit into
developfrom
main
Aug 14, 2026
Merged

chore: sync develop with main#542
chodeus merged 1 commit into
developfrom
main

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

@coderabbitai ignore

develop has drifted behind main (releases, fixes, dependency bumps). Do not merge this PR — it reports the drift, it does not fix it.

develop requires branches be up to date, and head:main can never satisfy that without pulling develop's extension files into main, which the branch invariant forbids. Squash or rebase would also leave main unreachable from develop, so this workflow would just open another PR next push.

Sync locally instead:

git checkout develop && git merge origin/main && git push

Then verify git diff main develop is added extension files plus deploy/docker/Dockerfile only. GitHub marks this PR merged on its own once develop contains main's tip. Opened by the sync-develop workflow.

* fix(security): confine and anchor every cleanup delete

The dirs were authorized once at enqueue, but each destructive op still
resolved its own path, so a symlink swap between scan and mutation could
send shutil.move/os.remove/shutil.rmtree outside the roots. Every sink now
re-confines its target immediately before acting, and the two delete sinks
go further: they open the confined PARENT with O_NOFOLLOW, pin it by
(st_dev, st_ino) the way prune_old_backups does, and operate by name
through that descriptor — so the name cannot be swapped for a link after
the check. rmtree takes dir_fd since CPython 3.11, so recursive delete is
anchored too, no reimplementation.

Moves stay at re-confinement only: shutil.move has no dir_fd and hardening
it would mean reimplementing the cross-filesystem fallback. Deletes can no
longer escape the authorized root; the leaf name is still resolved at the
syscall, which is the honest remaining gap.

* fix(security): open the delete parent one component at a time

O_NOFOLLOW guards only the last component and the inode pin re-resolves the
same string, so an intermediate symlink swapped in after confinement landed
the descriptor outside the roots. Walk down from the containing root
instead. Empty-dir sweep gains a base_dir floor for the bloat pass, which
passes no config. Refusals aggregate to one line per pass.

* fix(security): anchor moves and rmdir, reload config per item

os.rename takes src_dir_fd/dst_dir_fd, so same-filesystem moves are now
anchored at both ends; shutil.move survives only as the cross-device
fallback, the one path that still re-resolves strings. rmdir gets the same
descriptor treatment when a config is available. Both executors re-read
config per destructive item instead of trusting a pre-loop snapshot.
Makefile now gates at 3.11, which is what rmtree(dir_fd=) needs.

* test: bind de-authorization asserts to the refusing pass

Share the tightening-config stub as a conftest fixture. assert errors also
passed on an empty-dir-sweep refusal; key on the pass label and count.
@chodeus
chodeus merged commit ba76672 into develop Aug 14, 2026
33 of 39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant