Skip to content

fix: use Lstat in PurgeDirectory to handle symlinks without following them#8254

Merged
stasadev merged 1 commit into
mainfrom
20260327_rfay_intermittent_purge_fail
Mar 28, 2026
Merged

fix: use Lstat in PurgeDirectory to handle symlinks without following them#8254
stasadev merged 1 commit into
mainfrom
20260327_rfay_intermittent_purge_fail

Conversation

@rfay
Copy link
Copy Markdown
Member

@rfay rfay commented Mar 27, 2026

The Issue

A symlink in the TYPO3 v13.4.19 files.tgz (test.txt -> README.txt) caused intermittent failures in TestDdevImportFilesDir on CI: failing run

Error: failed to cleanup .../public/fileadmin before import:
    stat .../public/fileadmin/test.txt: no such file or directory

How This PR Solves The Issue

PurgeDirectory (and PurgeDirectoryExcept) iterated directory entries and called util.Chmod before removing each entry. util.Chmod uses os.Stat, which follows symlinks. If the symlink's target had already been removed earlier in the loop, os.Stat would fail with ENOENT.

This PR switches to os.Lstat to inspect each entry without following symlinks. Symlinks are then removed directly via os.RemoveAll (which removes the symlink entry itself, not its target) without any chmod step — symlinks don't have meaningful permissions of their own.

Relationship to the ZipSlip fix (#8213)

PR #8213 added case tar.TypeSymlink handling to Untar, so symlinks in archives are now properly extracted to the host filesystem. Before that PR, the test.txt symlink entry in TYPO3's files.tgz was silently skipped during extraction and never appeared on the host, so PurgeDirectory never encountered it.

After #8213, test.txt is extracted as a real symlink (test.txt -> README.txt). When PurgeDirectory later iterates fileadmin, Readdirnames returns entries in arbitrary filesystem/inode order. If README.txt is encountered first and removed, test.txt becomes a dangling symlink — and the subsequent util.Chmod call on it fails.

Why it was intermittent

Readdirnames returns entries in inode order, which varies by runner and filesystem. The failure only occurs when README.txt sorts before test.txt in that order, making the bug dependent on the specific runner environment.

Manual Testing Instructions

  • Run ddev import-files against a TYPO3 project that has symlinks in its fileadmin/ directory and confirm the import completes without error
  • Confirm that tarballs containing symlinks are still extracted correctly (the archive.Untar path is unchanged)
  • The intermittent TestDdevImportFilesDir CI failure for TYPO3 should be resolved; watch the next CI runs to confirm

Automated Testing Overview

Existing TestPurgeDirectory and TestPurgeDirectoryExcept tests pass. The intermittent CI failure in TestDdevImportFilesDir for TYPO3 will need to be observed over subsequent CI runs to confirm the fix holds.

Release/Deployment Notes

No impact on behavior for non-symlink files. Symlinks in upload directories are now removed cleanly during ddev import-files pre-import cleanup rather than causing an error.

… them [skip buildkite]

PurgeDirectory and PurgeDirectoryExcept used os.Stat (via util.Chmod)
which follows symlinks. If a symlink's target was already removed earlier
in the loop, the stat would fail with ENOENT.

Use os.Lstat instead and skip chmod for symlinks — symlinks have no
meaningful permissions of their own and os.RemoveAll removes the symlink
entry itself without following it.

This surfaced in TestDdevImportFilesDir for TYPO3: the v13.4.19
files.tgz contains test.txt as a symlink to README.txt. After #8213
added TypeSymlink handling to Untar, symlinks are now extracted to the
host. When README.txt was removed before test.txt in the purge loop,
test.txt became a dangling symlink and the subsequent util.Chmod failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rfay rfay requested a review from a team as a code owner March 27, 2026 20:32
@github-actions
Copy link
Copy Markdown

@rfay rfay requested a review from stasadev March 28, 2026 02:56
Copy link
Copy Markdown
Member

@stasadev stasadev 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 to me.

@stasadev stasadev merged commit a6978aa into main Mar 28, 2026
46 checks passed
@stasadev stasadev deleted the 20260327_rfay_intermittent_purge_fail branch March 28, 2026 07:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants