Skip to content

main: refuse to write the output into the input file - #428

Open
woahwhattheheck wants to merge 8 commits into
Tarsnap:masterfrom
woahwhattheheck:scrypt-refuse-same-file
Open

woahwhattheheck wants to merge 8 commits into
Tarsnap:masterfrom
woahwhattheheck:scrypt-refuse-same-file

Conversation

@woahwhattheheck

@woahwhattheheck woahwhattheheck commented Sep 6, 2026 •

Copy link
Copy Markdown

Fixes #427.

scrypt enc secret.txt secret.txt previously opened the output with fopen(..., "wb") before reading the input, silently replacing the source with a 128-byte encryption of empty data. scrypt dec f f could destroy the ciphertext similarly, and stdin aliases are also reachable (scrypt enc - f < f). This PR supports the original first-reporter bug-bounty submission in #427 by @woahwhattheheck; that claimant and request for sponsor classification/payment remain unchanged.

Current implementation

  • same_file() checks the already-open input before asking for a passphrase. It compares against either the named output or the already-open standard output. Regular-file aliases are identified by device/inode; block-device aliases by their underlying device. Character devices such as /dev/null remain permitted.
  • open_output() opens a named output without O_TRUNC, compares the actual open descriptors, and only then truncates a distinct regular output. This binds the destructive check to the opened object rather than trusting the earlier pathname check, closing the path-swap seam.
  • Errors retain the existing cleanup paths and preserve errno when closing an unsuccessful output descriptor. An obvious same-file failure occurs before prompting.
  • scrypt.1 documents the restriction and its limit: the program cannot undo truncation already performed by the invoking shell.

Executed acceptance — October 9, 2026

The unchanged contribution head ff39f6231fb12fc7171492d2b063b960b4bb2664 built with Autotools and GCC 13.3.0 (-std=c99 -O2) on Ubuntu 24.04. Its existing same-file scenario passed through the project runner:

autoreconf -fi
./configure CFLAGS='-std=c99 -O2'
make -j2
N=12 USE_VALGRIND=0 VERBOSE=1 sh tests/test_scrypt.sh ./scrypt

Successful exact-head acceptance run.

The existing scenario covers ordinary same-file encryption/decryption, hard links, symlink aliases, stdin, .. path spelling, stdout aliases, input preservation, different-file output, and a FIFO-gated pathname swap after the early check. The workflow checks that the production/test source remains unchanged. It lives on an isolated acceptance branch and adds no CI-only files to this upstream PR.

The first acceptance job ran the scenario successfully but was marked failed by an overly strict log matcher: verbose progress split the success message across lines. The linked corrected job uses the runner's exit status and is green. No production or regression-source changes were made for either run. This receipt is for the focused Linux scenario, not a full-suite, Valgrind or cross-platform result; sponsor acceptance and payment are separate.

Test numbering

The scenario is numbered 12 to avoid tests/11-info.sh from #426. The adjacent EXTRA_DIST entries may need reconciliation depending on merge order.

Original-reporter claim and follow-through

The original reporter and PR author is @woahwhattheheck. This corrective contribution is submitted in support of the existing bug-bounty report and claim in #427. I request sponsor review, the applicable bounty classification and any eligible payment to the original claimant, and will address feedback on this PR. Original contributor credit and earlier compensation requests remain intact; no award or received payment is assumed.

"scrypt enc secret.txt secret.txt" opens secret.txt for reading, reads a
passphrase, then opens the same path with fopen(..., "wb"), which truncates
it.  scryptenc_file() then reads from the now-empty file, gets EOF on the
first fread(), writes a valid 128-byte scrypt file containing an encryption
of no data, and returns SCRYPT_OK; scrypt exits 0.  The file the user asked
to encrypt is gone, with no error and nothing to indicate that anything went
wrong.

"scrypt dec f f" destroys the ciphertext the same way, and both are also
reachable with the input on stdin, e.g. "scrypt enc - f < f".

Compare the open input file and the output path with fstat() and stat()
before opening the output, and exit with an error if they are the same
regular file.  Devices are excluded: opening /dev/null for writing discards
nothing.  The check runs before the passphrase is read, so the user is not
asked to type a passphrase for a command which cannot succeed.

Add tests/12-same-file.sh, and document the restriction in scrypt.1.
The previous commit was uploaded through the GitHub API by a helper
which dropped the final newline of every text file it sent.  Rewrite
those files with their trailing newline intact; no other change.
Add the authored static-alias and independent-file regression coverage to the existing test scenario. Preserve the application implementation and existing upstream submission.
@tokenjunkielabs

Copy link
Copy Markdown

Head 14db0e0600340f8254fe602fd07e25a8fe3f59ad adds a deterministic regression for the TOCTOU seam already handled by open_output(): the test starts with a distinct output, gates passphrase reading on a FIFO so the early pathname check has already passed, swaps the output path to a hard link of the input, then resumes and requires exit 1, the same-file diagnostic, and byte-for-byte input preservation. The added shell block parses under sh/bash/dash, and its FIFO/path-swap synchronization was exercised independently. Upstream Compile & test run 35535619823 registered for this exact head but is action_required with no executed jobs, so this is not a hosted-green claim.

@tokenjunkielabs

Copy link
Copy Markdown

Maintainer review / bounty-classification request for #428 (fixes first-reporter report #427). The reported behavior is destructive: scrypt enc f f can truncate the input before it is read, destroying the source data; the current PR carries the same-file/alias guard and race-safe follow-up. Please review and classify #427 under the published Tarsnap bug-bounty schedule, including payment for first reporter woahwhattheheck if accepted. The schedule lists $500 for data corruption/loss; sponsor classification controls and I am not treating that amount as awarded.

The existing same-file guard only runs for a named output path. Append-redirected standard output can therefore write back into the input, for both enc and dec and with a named input or stdin.

Compare the open stdout descriptor when no output path is supplied. Reuse the existing regular/block-device identity policy, retain named-output race protection, and keep character devices such as /dev/null valid. Extend the maintained same-file scenario with four bounded stdout cases and document the limit that a shell may already have truncated a file before scrypt starts.

Actual CLI before/after: https://github.com/woahwhattheheck/scrypt/actions/runs/37202941771/job/111438168445
Standard Ubuntu 24.04.5, GCC 13.3.0, -std=c99 -O2 -Wall -Wextra; compile parent and candidate. Parent 14db0e0 exits 0 and changes the input in all four cases: enc 768 -> 1664 bytes, dec 896 -> 1664 bytes. Candidate exits 1 with the same-file diagnostic and preserves every byte in all four. Independent stdout encryption/decryption roundtrip, named-output alias, named /dev/null, and stdin/stdout /dev/null controls pass. Test script shell syntax passes. No complete suite or platform matrix run.

Executed source 342abef and test 72d49e3 are identical to this commit. The isolated validation workflow/driver remain on validation/scrypt-stdout-alias-20261004 at 120326d and are not included here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

[bug bounty] "scrypt enc f f" silently destroys f: the output file is truncated before the input is read

2 participants