Skip to content

Android's 2026-09 security patches cause file permission revocation during atomic file replacement #10887

Description

@chenxiaolong

What happened?

Android's new security patches this month introduced a patch in the MediaProvider component that causes an external app's permissions to files within a shared folder to be revoked when Syncthing replaces an existing regular file. This affects the most common scenario where an app, such as the KeePassDX password manager, opens a file from the shared folder using Android's storage access framework. (This has no effect on Syncthing itself since it requires permissions to the full internal storage anyway.)

When replacing an existing regular file, sendReceiveFolder.performFinish() normally does an (almost) atomic replace where it writes the new data to a temp file, deletes the target file, and then renames the temp file to the target file. Unfortunately, this Android security patch revokes permissions during both unlink and rename, making atomic replacements impossible even if Syncthing didn't have the explicit delete in the middle and relied solely on POSIX atomic renames.

I tested this on a userdebug Android build where I added a couple log statements to MediaProvider. You can see it revoke permissions:

2026-09-15 23:32:47.271004 -0400 E/WTF     ( 4348): revokeUriPermissionGrantsOnDocumentsProvider(/storage/emulated/0/a)
2026-09-15 23:32:47.311661 -0400 E/WTF     ( 4348): revokeUriPermissionGrantsOnPathChange(/storage/emulated/0/b, /storage/emulated/0/a)
2026-09-15 23:32:47.314857 -0400 E/WTF     ( 4348): revokeUriPermissionGrantsOnDocumentsProvider(/storage/emulated/0/b)

when I run:

mustang:/sdcard $ touch a b
mustang:/sdcard $ strace mv b a
execve("/system/bin/mv", ["mv", "b", "a"], 0xef57e1ff5ed0 /* 26 vars */) = 0
...
newfstatat(AT_FDCWD, "a", {st_mode=S_IFREG|0660, st_size=0, ...}, 0) = 0
umask(000)                              = 000
newfstatat(AT_FDCWD, "a", {st_mode=S_IFREG|0660, st_size=0, ...}, 0) = 0
renameat2(AT_FDCWD, "b", AT_FDCWD, "a", 0) = 0
exit_group(0)                           = ?
+++ exited with 0 +++

In BasicSync, I'm working around this with chenxiaolong@668fe24 (chenxiaolong/BasicSync#245). It's an ugly hack to force the fallback path of osutil.RenameOrCopy() to be taken when performFinish() replaces a regular file on Android's MediaProvider FUSE filesystem.

I don't expect something like this to be merged upstream, but I'm curious if anyone has other ideas for making Syncthing optionally overwrite regular files in place.

Syncthing version

v2.1.5

Platform & operating system

Android arm64

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugA problem with current functionality, as opposed to missing functionality (enhancement)needs-triageNew issues needed to be validated

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions