Skip to content

Data loss when a Team folder has the same name as a personal folder #5124

Description

@rhuitl

How to use GitHub

  • Please use the 👍 reaction to show that you are affected by the same issue.
  • Please don't comment if you have no relevant information to add. It's just extra noise for everyone subscribed to this issue.
  • Subscribe to receive notifications on status change and new comments.

Steps to reproduce

  1. User A has a populated top-level personal folder named collision.
  2. An administrator creates a Team folder also named collision and grants User A access to it.
  3. Trigger mount initialization for User A, for example by opening Files or through concurrent WebDAV/OCS requests. In the observed incident, an OCS Recommendations request resolving a file shared by User A triggered it.

Using a large personal folder and parallel requests makes the race more likely. The affected folder contained about 1.2 GB and 2,300 file-cache entries.

Expected behaviour

  • Creating a Team folder must never delete personal data.
  • Creating a Team folder should never modify or delete an existing personal folder.
  • Ideally, personal folders should not be renamed; doing so during mount initialization is a dangerous design decision.
  • At minimum, a name collision must be resolved safely (or ask the user to resolve it explicitly)

Actual behaviour

  • All files in the personal folder have been permanently deleted.
  • The UI does not reflect the deletion, so it goes unnoticed until a file is accessed.
  • The personal folder is renamed to collision (1) in the file cache, but the physical collision (1) directory is empty.
  • Cached files initially remain visible but fail when opened. occ files:scan later removes the stale entries from the UI.
  • The deletion bypasses the trash bin and activity log.
  • files_versions/collision is not renamed and remains disconnected from the renamed cache path.

Two provider snapshots confirm the result:

  • Before: data/<user>/files/collision contains approximately 1.2 GB.
  • After: the original directory is absent, data/<user>/files/collision (1) is empty, while the database still contains the original file IDs below files/collision (1) and reports approximately 1.2 GB.

Server configuration

Operating system: Unknown; Hetzner-managed Storage Share

Web server: Unknown; Hetzner-managed Storage Share

Database: MySQL/MariaDB

PHP version: Unknown; Hetzner-managed Storage Share

Nextcloud version: 33.0.8.2

Team folders version: 21.0.15

Updated from an older Nextcloud/ownCloud or fresh install: Existing installation; upgrade history unknown

Where did you install Nextcloud from: Hetzner Storage Share managed service

Are you using external storage, if yes which one: No. Local data directory; underlying provider storage is believed to be ZFS-backed. The Team folder uses separate-storage under data/__groupfolders/<id>/.

Are you using encryption: No

Are you using an external user-backend, if yes which one: No

Client configuration

Browser: Chrome 152

Operating system: macOS

Logs

Web server error log

Web server error log

Not available on the managed hosting service.

Nextcloud log (data/nextcloud.log)

Nextcloud log

Names, addresses, and unrelated paths are redacted. All timestamps below are UTC and belong to the same request.

2026-09-08T20:33:50+00:00
reqId: O5VLaScsmckHWBWh6mg7
GET /ocs/v2.php/apps/recommendations/api/v1/recommendations

failed to rename
/var/www/html/data/<user>/files/collision
to
/var/www/html/data/<user>/files/collision (1),
trying copy+delete fallback instead
2026-09-08T20:34:31+00:00
reqId: O5VLaScsmckHWBWh6mg7

opendir(/var/www/html/data/<user>/files/collision):
Failed to open directory: No such file or directory
at /var/www/html/lib/private/Files/Storage/Local.php#130
TypeError: readdir(): Argument #1 ($dir_handle) must be of type resource or null, false given

#0  lib/private/Files/Storage/Common.php(202): readdir(false)
#1  lib/private/Files/Storage/Local.php(401):
    Common->copy('files/collision', 'files/collision (1)')
#2  lib/private/Files/Storage/Local.php(381):
    Local->copy('files/collision', 'files/collision (1)')
#3  lib/private/Files/Storage/Wrapper/Wrapper.php(136):
    Local->rename('files/collision', 'files/collision (1)')
#4  custom_apps/groupfolders/lib/Mount/MountProvider.php(85):
    Wrapper->rename('files/collision', 'files/collision (1)')
#5  GroupFolders\Mount\MountProvider::getMountsForUser()
#6  lib/private/Files/SetupManager.php(485): updateNonAuthoritativeProviders(...)

The empty destination directory has mtime 2026-09-08 20:34:31 UTC, exactly matching the opendir/readdir failure.

Browser log

Browser log

Not available.

Analysis

Team folders resolves the collision with three unlocked operations:

$userStorage->rename("files/$originalFolderName", "files/$folderName");
$userCache->move("files/$originalFolderName", "files/$folderName");
$userStorage->getPropagator()->propagateChange("files/$folderName", time());

The return value of the physical rename is ignored, and the cache move is unconditional.

The evidence indicates two concurrent mount-initialization requests. One request renamed the source and moved the cache. The logged request then entered Local::rename()'s copy fallback, which calls remove($target) before reopening the source. It removed the destination containing the other request's successful rename, then found the source missing and recreated an empty destination before throwing on readdir(false).

Atomic filesystem rename does not prevent this: the destructive operation is the later recursive remove($target) performed by the competing request.

Relevant source:

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

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions