Skip to content

[Bug]: Interrupted chunk assembly leaves a 0-byte file while oc_filecache keeps the original size/etag (silent data loss, later propagated to sync clients) #63071

Description

@sprior

Follow-up to #28589 ("Error assembling chunks - chunks deleted too soon?"), which was closed in 2023 for lack of a reproduction on a current version. The last maintainer comment there (@szaimen, 2023-01-23) invited a new report with up-to-date information if it could be reproduced later. This is that report, on Nextcloud 34 — and it adds a consequence that #28589 never described.

Summary

When the final MOVE .../.file of a chunked upload is interrupted, the destination file can be left 0 bytes on disk, while oc_filecache still reports the original size and the original etag.

Because the etag is unchanged, no client ever notices. The file looks correct in the web UI and in every sync client, and downloading it silently returns nothing. Nextcloud does not re-stat the filesystem on its own — filesystem_check_changes defaults to 0, and the ScanFiles background job only visits unindexed entries — so the divergence is permanent rather than self-healing.

The dangerous part is what happens next. When someone eventually runs occ files:scan — the documented remedy for unrelated catalog drift, so it gets run routinely and with no reason to suspect anything — the catalog flips to 0 bytes and the etag changes. Sync clients read that as "remote modified, local unmodified" and download the empty file over a good local copy. No conflict copy is created, because a conflict requires both sides to have changed.

So a failed upload can destroy the user's original, on their own machine, at an arbitrary later time, with no error surfaced at any point.

#28589 reported the assembly failure itself. It did not report the truncated destination, the stale catalog entry, or the propagation path — which is why this is raised as a new report rather than a duplicate.

Steps to reproduce

The trigger is any interruption of the assembly MOVE. Ours was a reverse-proxy timeout, but a container restart, network blip or storage stall should do equally well.

  1. Put Nextcloud behind a reverse proxy whose response timeout is shorter than chunk assembly takes (we hit this with 30s).
  2. From the desktop client, upload a file large enough that assembly exceeds that timeout. A 2.5 GB file onto NFS-backed storage took ~60–75s to assemble here.
  3. Let the proxy return 502 to the client while the server continues assembling.
  4. Observe the client PROPFIND the upload directory, retry the MOVE (which returns 423 Locked, because the original assembly is still running), and then DELETE the upload session while that assembly is still reading chunks from it.
  5. Compare the destination file on disk against occ info:file.

Actual behaviour

Server-side access log (sanitised; times are local):

00:48:51  MOVE   /remote.php/dav/uploads/<user>/<session>/.file   502
00:49:47  DELETE /remote.php/dav/uploads/<user>/<session>         204
          -> destination file mtime 00:50, size 0

No other write touched that path. The 502 is emitted by the proxy at its timeout; the server-side request keeps running — an earlier, identical assembly completed with 201 after 60.4s.

The destination was then:

on disk:        0 bytes
occ info:file:  size: 2.5 GB      (original)
                etag: 656354c1…   (original)

That the chunk directory is removed while the assembly is still streaming from it matches the direct evidence in #28589, which captured the assembly failing with failed to open stream: No such file or directory on a chunk path. To be precise about what I actually observed: in my case the interleaving is inferred from request timing and the resulting file mtime, not from such a log line.

The catalog divergence, isolated

This half reproduces on its own — no failed upload, no proxy, no large file — and shows both why the condition is invisible and why scanning is what weaponises it:

$ occ info:file "/<user>/files/decoy.bin"
  size: 2.9 MB   etag: c953a5b2…

$ truncate -s 0 <datadir>/<user>/files/decoy.bin

$ occ info:file "/<user>/files/decoy.bin"
  size: 2.9 MB   etag: c953a5b2…      <-- unchanged; no client has any reason to re-fetch

$ occ files:scan --path="<user>/files"

$ occ info:file "/<user>/files/decoy.bin"
  size: 0 B      etag: 3d0bf539…      <-- flipped, and the etag change is what clients act on

Expected behaviour

An interrupted assembly should not be able to destroy an existing file, and must not leave the catalog asserting something the storage contradicts. Concretely, any of:

  • Assembly writes to a temporary file and renames into place, so an interruption leaves the previous file (or no file) rather than a truncated one.
  • Deleting an upload session is refused, or blocks, while an assembly for that session is in flight — the session directory is in use.
  • On assembly failure, the destination's oc_filecache entry is invalidated or refreshed, so the state is at least visible even if the write cannot be made atomic. This alone would turn silent corruption into a detectable error, and looks like the cheapest of the three.

Impact

  • Silent. The corrupted file is undetectable through any Nextcloud interface.
  • Delayed and remote. Destruction of the user's local original happens later, on a different machine, triggered by routine maintenance.
  • No conflict copy, so there is no client-side recovery once it has synced down.
  • The trigger is not exotic. Any proxy timeout, restart or storage stall during a long assembly can cause it, and large files are the most exposed because their assembly window is longest.

Server configuration

  • Nextcloud Server version: 34 (official nextcloud:34-apache image)
  • Operating system: Debian (container), Kubernetes
  • Web server: Apache 2.4
  • PHP version: 8.5
  • Database: PostgreSQL 17
  • Storage: NFS-backed data directory
  • Desktop client: mirall 34.0.0 (Windows)
  • filesystem_check_changes: unset (default 0)
  • A reverse proxy sits in front of the instance. The 30s timeout that triggered this was my own misconfiguration and is fixed; it is reported here only as the mechanism that exposed the failure mode, not as the bug.

Possible companion issue (nextcloud/desktop)

The client treats a 502 on the assembly MOVE as a definite failure and cleans up the upload session. A 502 means unknown, not failed — and the client demonstrably knows this, since it first PROPFINDs to check whether the assembly actually happened. It then deletes the session anyway, which is what removes the chunks out from under the running assembly. Happy to raise that separately against the desktop client if maintainers agree the server-side fix does not fully cover it.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    Status
    To triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions