Skip to content

Remote task delivery can silently truncate files to a few bytes (reject/reset + copy-back race) #639

Description

@mrjacobarussell

SummaryTwo separate bugs in the remote-task delivery pipeline (unmanic/libs/installation_link.py / unmanic/libs/postprocessor.py) can silently replace a good source file with a tiny (~55 byte) truncated stub, destroying the original media with no error surfaced to the user. Found via unmanic.db history logs and confirmed by re-checking on-disk file sizes after "successful" tasks.Unmanic version: 0.4.0+e838573 (remote installation) / 0.4.1~1c324b8 (main installation)Plugin: video_transcoder (Transcode Video Files)## Bug 1: "Reject File if Larger than Original" reset reads from an expired staging pathWhen a remote-worker transcode produces output larger than the source, the reject_files_larger_than_original plugin resets the task back to the original file. On long-running remote tasks (60-90+ min, e.g. an overloaded/slow hardware encoder), the "original" it resets to is a temp staging copy under unmanic_remote_pending_library-<random> — created for the remote handoff — which appears to no longer be intact/available by the time the reset runs. The result written back to the library is a few dozen bytes instead of the real source.Example from our logs (paths redacted):Resetting task file back to original source as current cache file is larger than the original file: - Original File: 463218706 bytes '.../unmanic_remote_pending_library-<redacted>/<file>.mkv' - Cache File: 1137643964 bytes '.../unmanic_file_conversion-<redacted>/<file>-WORKING-2-1.mkv'Runner did not request for Unmanic to execute a commandTask was marked task_success: true. On-disk file after this task: 55 bytes.## Bug 2: Remote task result copy-back only checks size > 0Separately, and more concerning: even when the reject/reset path is not triggered and the remote encode succeeds cleanly (smaller than source, no reject warning in the log), the resulting on-disk file can still end up truncated to ~55 bytes.In installation_link.py, after a remote task completes, the finished file is staged by the remote installation into a unmanic_remote_pending_library-* directory on the shared library path, and the originating installation copies it back with:pythonoutput = shutil.copy(task_cache_path, correct_cache_file_path)if os.path.exists(output) and os.path.getsize(output) > 0: # treated as successThis only checks that the copied file is non-empty — it never compares the copied size against the expected/reported size from the remote task, and never waits for the remote-side write to fully settle before reading. Over a network/shared filesystem (NFS in our case) this is a real time-of-check-to-time-of-use race: the copy can read a stale/partially-written staged file and still pass the size > 0 check, silently corrupting media that was actually encoded successfully.## ImpactWe found 400+ TV episodes and dozens of movies replaced with ~55 byte files across our library, spanning weeks, before diagnosing this. Files pass task_success: true with no error surfaced anywhere in the Unmanic UI or history.## Suggested fixes1. For bug 1: don't reset from a path that may be a transient remote staging copy: cache the actual original file size/checksum before starting the remote task and re-verify against a durable copy, not a possibly-expired staging directory.2. For bug 2: after the shutil.copy in installation_link.py, verify the copied file's size matches the size reported by the remote task's completion data (or a checksum, if enable_checksum_validation is on) before accepting it as the delivered result. Fail loudly (and leave the original untouched) if it doesn't match.Happy to provide more logs/task IDs if useful — we have full unmanic.db history for both cases.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions