Skip to content

fix: return error on non-ErrNotExist stat failures in Tar.Sync()#13684

Merged
glours merged 2 commits intodocker:mainfrom
Lidang-Jiang:fix/13654-tar-sync-stat-error
Apr 3, 2026
Merged

fix: return error on non-ErrNotExist stat failures in Tar.Sync()#13684
glours merged 2 commits intodocker:mainfrom
Lidang-Jiang:fix/13654-tar-sync-stat-error

Conversation

@Lidang-Jiang
Copy link
Copy Markdown
Contributor

Summary

  • Fix silent misclassification of non-ErrNotExist stat errors in Tar.Sync() (internal/sync/tar.go)
  • Add unit tests for the three stat outcomes: exists, not-exists, and permission error

The previous two-way branch only checked for fs.ErrNotExist. Any other os.Stat error (e.g. EACCES, EIO) fell through to the else clause, silently treating the path as copyable. This masked real errors and caused failures downstream with the original cause lost.

The fix restructures the condition into a three-way branch, following the pattern already used by entriesForPath() in the same file.

Fixes #13654

Before / After

Before (buggy behavior)

Non-ErrNotExist stat errors are silently treated as "copy":

=== Old (buggy) logic ===
  /tmp/before_test_existing.txt → COPY (err=<nil>)
  /no/such/path → DELETE (not exist)
  /tmp/before_test_noaccess/secret.txt → COPY (err=stat /tmp/before_test_noaccess/secret.txt: permission denied)

The path with a permission error is incorrectly added to the copy list instead of surfacing the error.

After (fixed behavior)

Permission errors are now properly returned:

=== New (fixed) logic ===
  /tmp/before_test_existing.txt → COPY
  /no/such/path → DELETE
  /tmp/before_test_noaccess/secret.txt → ERROR: stat /tmp/before_test_noaccess/secret.txt: permission denied

Unit test output:

=== RUN   TestSync_ExistingPath
--- PASS: TestSync_ExistingPath (0.00s)
=== RUN   TestSync_NonExistentPath
--- PASS: TestSync_NonExistentPath (0.00s)
=== RUN   TestSync_StatPermissionError
--- PASS: TestSync_StatPermissionError (0.00s)
=== RUN   TestSync_MixedPaths
--- PASS: TestSync_MixedPaths (0.00s)
PASS
ok  	github.com/docker/compose/v5/internal/sync	0.005s

Lint:

$ golangci-lint run ./internal/sync/...
0 issues.

Test plan

  • TestSync_ExistingPath — existing host path is correctly added to copy list
  • TestSync_NonExistentPath — missing host path triggers rm -rf delete command
  • TestSync_StatPermissionErrorEACCES error is returned immediately (skipped on root/Windows)
  • TestSync_MixedPaths — mix of existing and missing paths handled correctly in one call
  • golangci-lint run ./internal/sync/... — 0 issues

@Lidang-Jiang Lidang-Jiang requested a review from a team as a code owner March 29, 2026 02:27
@Lidang-Jiang Lidang-Jiang requested review from glours and ndeloof March 29, 2026 02:27
Copy link
Copy Markdown
Contributor

@glours glours left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, unfortunately we just merged the removal of testify library which means you need to update your tests, sorry for that.

Comment on lines +26 to +27
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry but PR #13689 just remove the testify library, can update the test to use gotest.tools/v3 assertions?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, rebased on latest main and migrated all assertions to gotest.tools/v3 in c86e506.

Previously, Sync() only checked for fs.ErrNotExist when classifying
paths into copy vs delete. Non-NotExist stat errors (e.g. EACCES,
EIO) caused the condition to be false, falling through to the else
clause which incorrectly treated the path as copyable. This masked
real errors and led to cryptic failures downstream.

Restructure the condition into a three-way branch:
- err == nil → copy
- ErrNotExist → delete
- other errors → return immediately with context

This follows the pattern already used by entriesForPath() in the
same file.

Fixes docker#13654

Signed-off-by: Lidang Jiang <lidangjiang@gmail.com>
Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
@Lidang-Jiang Lidang-Jiang force-pushed the fix/13654-tar-sync-stat-error branch from 8e9d666 to c86e506 Compare April 1, 2026 12:48
@glours
Copy link
Copy Markdown
Contributor

glours commented Apr 2, 2026

@Lidang-Jiang for legal reason you need to sign-off your last commit otherwise I won't be able to merge your PR

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 2, 2026

Codecov Report

❌ Patch coverage is 75.00000% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/sync/tar.go 75.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
@Lidang-Jiang Lidang-Jiang force-pushed the fix/13654-tar-sync-stat-error branch from c86e506 to 0b25251 Compare April 3, 2026 03:22
@Lidang-Jiang
Copy link
Copy Markdown
Contributor Author

Done, added Signed-off-by to the last commit in 0b25251.

@glours glours enabled auto-merge (rebase) April 3, 2026 10:19
@glours glours merged commit 63601eb into docker:main Apr 3, 2026
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sync: non-ErrNotExist stat errors silently treated as "copy" in Tar.Sync()

2 participants