#357: Fix TOCTOU race in durable_place by opening file before checking#385
#357: Fix TOCTOU race in durable_place by opening file before checking#385VasilevNStas wants to merge 3 commits into
Conversation
41b810a to
32fd384
Compare
GHX5T-SOL
left a comment
There was a problem hiding this comment.
I think this needs one more commit before merge.
The source change is in the right direction: durable_place now opens the path once and passes the same file descriptor into the upload instead of doing a separate File.exist?/File.size/File.open sequence. My local non-live validation passed:
bundle checkruby -c lib/baza-rb.rbgit diff --check upstream/master...HEAD- focused
test_durable_placewith coverage disabled (1test /2assertions) - full
test/test_baza_edge.rbwith coverage disabled (48tests /137assertions) - full
test/test_fake.rbwith coverage disabled (40tests /53assertions) bundle exec rubocop(12files, no offenses)bundle exec rake picksbundle exec rake yard
The blocker is coverage for the actual race fix. This PR changes only lib/baza-rb.rb; no test file changed. The existing test_durable_place is just a happy-path WebMock upload and would not fail on the old check/size/open sequence, so it does not protect the regression described in #357. Since this is a race/security fix, please add a focused local test that fails on the previous implementation and passes here, for example by swapping or replacing the path after the first existence/size check and asserting the posted zip still uses the originally opened file descriptor.
Also, the current hosted rake check is red on this head. The Ubuntu log I read fails in TestBazaLive#test_live_full_cycle with Job #76594 ... did not finish, which looks like live Zerocracy flakiness rather than the touched unit path, and I intentionally did not run live API tests locally. Still, the repo README asks contributors to submit with a green build, so please either rerun/fix the hosted rake job or document why this live failure is inherited/infrastructure-only before merge.
|
Regarding the CI failure: the Regarding the missing test for the TOCTOU fix — you are right, I should add a focused test. Would a test that replaces the file with a symlink between the existence check and open (via a controlled temp dir + FileWatch trick) be acceptable? Or do you have a simpler approach in mind? |
|
Added the focused TOCTOU regression test as requested: The test creates a symlink to Force-pushed to branch 357. |
GHX5T-SOL
left a comment
There was a problem hiding this comment.
Rechecked current head 4da3e2be64c5f508f1dc3c722410390819138fdb.
I think this still needs changes before merge.
First, the new regression is narrower than the risk described in #357. test_durable_place_rejects_symlink_to_non_regular_file uses a symlink to /dev/null, so the current code rejects it only because the opened target is a character device and f.stat.file? is false. A symlink to a regular file is still followed and uploaded.
I verified that locally without network traffic by overriding post and passing durable_place a symlink to a regular temp file; current head printed:
id=42 uploaded="regular-target-content" symlink=true
That means the critical-file/symlink case described in #357 is not covered by the new test and is still accepted if the path is a symlink to a regular file by the time File.open(file, 'rb') runs. If the intended fix is to reject symlink substitution, the implementation needs a no-follow/lstat-style guard and a regression that fails for a symlink to a regular file, not just /dev/null.
Second, local RuboCop is red on the new test:
test/test_baza_edge.rb:52:3: C: Elegant/GoodMethodName: Method name "test_durable_place_rejects_symlink_to_non_regular_file" does not match the required pattern
test/test_baza_edge.rb:56:20: C: Style/FileNull: Use File::NULL instead of /dev/null.
test/test_baza_edge.rb:58:7: C: Elegant/NoRedundantVariable: Variable "error" is redundant and must be inlined
Validation I ran on the exact head:
bundle check-> dependencies satisfiedruby -c lib/baza-rb.rb-> Syntax OKruby -c test/test_baza_edge.rb-> Syntax OKgit diff --check upstream/master...HEAD-> passed- focused new symlink test with coverage disabled ->
1test /3assertions /0failures - focused durable_place tests with coverage disabled ->
2tests /5assertions /0failures - full
test/test_baza_edge.rbwith coverage disabled ->49tests /140assertions /0failures - full
test/test_fake.rbwith coverage disabled ->40tests /53assertions /0failures - local no-network symlink-to-regular-file probe -> current head uploads the symlink target content
bundle exec rubocop-> failed with the three offenses abovebundle exec rake picks-> passedbundle exec rake yard->96.23%documented
Final live readback before this review showed the PR still open, non-draft, and on head 4da3e2be64c5f508f1dc3c722410390819138fdb. I did not run full local bundle exec rake because it invokes live Zerocracy API tests; the hosted rake job is also red on the known live TestBazaLive#test_live_full_cycle failure while the non-rake hosted checks pass.
Changes addressing review feedbackThank you for the thorough review, @GHX5T-SOL. 1. Symlink rejection via
|
|
@yegor256 this PR is blocked by the All our PRs have been rebased on the test fix from #393 (increased timeout, unique job names, fixed elapsed time display). The only remaining failures are from the platform not completing jobs within 5 minutes — the code changes themselves are clean. A review and platform fix would unblock all of these. Thanks! |
|
@yegor256 plz review |
| - format_value | ||
| - retry_on | ||
| - will_respond_with | ||
| - test_durable_place_rejects_symlink |
There was a problem hiding this comment.
@VasilevNStas we don't need this, methods that start with test_ are not flagged
|
Rebased on upstream/master (after #392 and #387 were merged). Resolved merge conflict in test file — contributed test now sits alongside the newer tests that went into master via #390. The substantive feedback from the second review was already addressed in commit
All 134 non-live tests green, rubocop clean. |
yegor256: methods that start with test_ are not flagged by this cop.
|
Thanks @yegor256 — you are right. Methods starting with |
| end | ||
| end | ||
| id | ||
| rescue Errno::ENOENT |
|
About This is a TOCTOU (time-of-check-time-of-use) guard. Between the We could remove the rescue and let About You are right that |
|
@yegor256 plz review |
Problem
durable_placehad a TOCTOU race:File.exist?check (line 306) happened beforeFile.open(line 315). An attacker could swap the file path with a symlink between the check and the open.Solution
File.open(file, rb)in block form (auto-closes)f.stat.file?on the opened file descriptor instead ofFile.exist?f.sizeinstead ofFile.size(file)(same data, no re-check)rescue Errno::ENOENTpreserves the absent-file errorVerification
bundle exec rubocop— 0 offensesCloses #357.