Skip to content

Avoid unlink/rename when replacing regular files on Android - #245

Merged
chenxiaolong merged 1 commit into
masterfrom
android-sep-2026
Sep 16, 2026
Merged

chenxiaolong merged 1 commit into
masterfrom
android-sep-2026

Conversation

@chenxiaolong

@chenxiaolong chenxiaolong commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Android's September 2026 security patches [0] changed the MediaProvider behavior so that it revokes app permissions on unlink and rename within its FUSE filesystem, which is used for both internal storage (/sdcard) and external storage (SD cards and USB drives). For rename, permissions for both paths are revoked as it does: revoke destination -> rename -> revoke source.

Unfortunately, this means it is now impossible to keep app permissions to files in a shared folder while performing atomic file replacement.

This commit works around the problem by performing in-place overwrites when replacing regular files. This essentially forces the use of the fallback path of osutil.RenameOrCopy() when the destination is on a FUSE filesystem on Android. The statfs magic check is sufficient since the only FUSE filesystem openable by path on Android is MediaProvider's.

[0] https://android.googlesource.com/platform/packages/providers/MediaProvider/+/91dddac65b6ef48ae54302fa852029c2fcf010aa

@chenxiaolong chenxiaolong self-assigned this Sep 16, 2026
Android's September 2026 security patches [0] changed the MediaProvider
behavior so that it revokes app permissions on unlink and rename within
its FUSE filesystem, which is used for both internal storage (/sdcard)
and external storage (SD cards and USB drives). For rename, permissions
for both paths are revoked as it does: revoke destination -> rename ->
revoke source.

Unfortunately, this means it is now impossible to keep app permissions
to files in a shared folder while performing atomic file replacement.

This commit works around the problem by performing in-place overwrites
when replacing regular files. This essentially forces the use of the
fallback path of osutil.RenameOrCopy() when the destination is on a
FUSE filesystem on Android. The statfs magic check is sufficient since
the only FUSE filesystem openable by path on Android is MediaProvider's.

[0] https://android.googlesource.com/platform/packages/providers/MediaProvider/+/91dddac65b6ef48ae54302fa852029c2fcf010aa

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
chenxiaolong added a commit that referenced this pull request Sep 16, 2026
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
@chenxiaolong
chenxiaolong merged commit ff60304 into master Sep 16, 2026
1 check passed
@chenxiaolong
chenxiaolong deleted the android-sep-2026 branch September 16, 2026 03:49
@chenxiaolong

Copy link
Copy Markdown
Owner Author

I reported this to upstream Syncthing to see if they have any better ideas for optionally performing in-place file replacements: syncthing/syncthing#10887

@PanderMusubi

Copy link
Copy Markdown

Can this workaround be an option per folder (default off)?

@chenxiaolong

Copy link
Copy Markdown
Owner Author

I might consider adding a global option, but I won't be adding a per-folder option because it would introduce an incompatibility in the config file format with the upstream Syncthing project.

@PanderMusubi

Copy link
Copy Markdown

And an upstream feature request? I would support that.

@chenxiaolong

Copy link
Copy Markdown
Owner Author

Yeah, I already have in syncthing/syncthing#10887

@kt767

kt767 commented Sep 18, 2026

Copy link
Copy Markdown

Please excuse my ignorance but does this problem apply if I've granted All files access permission to BasicSync?

@chenxiaolong

Copy link
Copy Markdown
Owner Author

Please excuse my ignorance but does this problem apply if I've granted All files access permission to BasicSync?

Yep, the permissions of BasicSync (or any other file sync app) don't matter. With the way Android's new security patch works, if a file is renamed (even temporarily) or deleted as part of the sync process, other apps that previously had access to the file will lose access.

@kt767

kt767 commented Sep 22, 2026

Copy link
Copy Markdown

Please excuse my ignorance but does this problem apply if I've granted All files access permission to BasicSync?

Yep, the permissions of BasicSync (or any other file sync app) don't matter. With the way Android's new security patch works, if a file is renamed (even temporarily) or deleted as part of the sync process, other apps that previously had access to the file will lose access.

Thank you for the explanation. I have a question about "the chance of file corruption" due to this workaround, mentioned in your change log for 3.17:

I currently have BasicSync set to run only when connected to power. If I update a file on my Mac and I unplug my Android phone while that file is mid-sync (causing BasicSync to stop abruptly), I assume the file ends up corrupted (partially written) on Android. Will BasicSync automatically detect the mismatch and repair the file the next time it runs, or does that require manual intervention?

Also, since I don't use external apps (like KeePassDX) that are affected by the permission issue, I'd love to avoid the corruption risk altogether. Would you consider adding a global setting to toggle this workaround off for users who don't need it?

Thanks again for your time and for keeping this app running so well!

@chenxiaolong

Copy link
Copy Markdown
Owner Author

I currently have BasicSync set to run only when connected to power. If I update a file on my Mac and I unplug my Android phone while that file is mid-sync (causing BasicSync to stop abruptly), I assume the file ends up corrupted (partially written) on Android. Will BasicSync automatically detect the mismatch and repair the file the next time it runs, or does that require manual intervention?

Previously/normally, Syncthing overwrites files like this:

  1. Create .syncthing.<name>.tmp
  2. Delete <name>
  3. Rename .syncthing.<name>.tmp to <name>

The new behavior is:

  1. Create .syncthing.<name>.tmp
  2. Delete <name>
  3. Create <name> and copy contents from .syncthing.<name>.tmp
  4. Delete .syncthing.<name>.tmp

It's specifically step 3 that could potentially cause an issue, but only if the app were to crash during the middle of it. Normal stopping does not interrupt writes. It would still leave the .syncthing.<name>.tmp behind in the event of a crash.

Would you consider adding a global setting to toggle this workaround off for users who don't need it?

Yep, I'll likely add a global toggle for this.

@PanderMusubi

Copy link
Copy Markdown

For this important folder, I have added a 14-day TrashCan in File Versioning.

@chenxiaolong

Copy link
Copy Markdown
Owner Author

For this important folder, I have added a 14-day TrashCan in File Versioning.

That's a good idea. And on that topic, BasicSync only avoids renames to work around this issue when overwriting files during a normal sync. For all other operations, like moving files to the .stversions trash can folder, renames are still used.

@kt767

kt767 commented Sep 22, 2026

Copy link
Copy Markdown

The new behavior is:

1. Create `.syncthing.<name>.tmp`

2. Delete `<name>`

3. Create `<name>` and copy contents from `.syncthing.<name>.tmp`

4. Delete `.syncthing.<name>.tmp`

It's specifically step 3 that could potentially cause an issue, but only if the app were to crash during the middle of it. Normal stopping does not interrupt writes. It would still leave the .syncthing.<name>.tmp behind in the event of a crash.

Thank you so much for the detailed explanation. If the app crashes during step 3, what happens when the app restarts? Will it propagate the partially-written/corrupt file <name> as well as .syncthing.<name>.tmp to other devices?

@chenxiaolong

chenxiaolong commented Sep 22, 2026

Copy link
Copy Markdown
Owner Author

Thank you so much for the detailed explanation. If the app crashes during step 3, what happens when the app restarts? Will it propagate the partially-written/corrupt file <name> as well as .syncthing.<name>.tmp to other devices?

No problem! I just gave this a quick test by intentionally making it crash exactly half-way through the writes. I overwrote test.bin (1 MiB) on the computer side, allowed the phone to crash, and then restarted the app on the phone. The end result is:

On the phone:

  • test.bin - 0.5 MiB (partial write from crash)
  • test.bin.sync-conflict-20260922-124332-IR7TV45 - 1 MiB (resynced from computer after restart)
  • .syncthing.test.bin.tmp - 1 MiB (from original sync before crash)

On the computer:

  • test.bin - 0.5 MiB (synced from phone after restart)
  • test.bin.sync-conflict-20260922-124332-IR7TV45 - 1 MiB (renamed from test.bin due to conflict)

The temp file does not get propagated.

So it looks like the original good data never gets lost. It just annoyingly requires the user to deal with the sync conflict, and the temp file gets left behind on the phone.

@chenxiaolong

Copy link
Copy Markdown
Owner Author

I've added a new global toggle in #248. I've decided to revert back to the upstream Syncthing behavior of using safe overwrites by default. Folks who want to prevent permissions from being revoked will need to turn off the new "Overwrite files safely" option. I'll mention this in the changelog too.

I'll wait a couple days before releasing a new version so that folks have a chance to translate the new strings.

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.

3 participants