Skip to content

Fix GH-21992: ext/standard: Fix symlink trying to resolve dangling links#22024

Open
zaaarf wants to merge 5 commits into
php:PHP-8.4from
zaaarf:gh-21992
Open

Fix GH-21992: ext/standard: Fix symlink trying to resolve dangling links#22024
zaaarf wants to merge 5 commits into
php:PHP-8.4from
zaaarf:gh-21992

Conversation

@zaaarf
Copy link
Copy Markdown

@zaaarf zaaarf commented May 12, 2026

Solves the problem described in #21992 (OP phrased it with more detail than I could come up with).

I didn't add a test because testing this problem kind of requires touching machine state (i.e. creating a dangling link somewhere), but let me know if I should still do it.

@LamentXU123
Copy link
Copy Markdown
Contributor

I didn't add a test because testing this problem kind of requires touching machine state (i.e. creating a dangling link somewhere),

There is a --clean-- section you can fill in to clean your tmp symlinks. See if this test works:

  --TEST--
  GH-21992: symlink() must not resolve the final dangling link component
  --SKIPIF--
  <?php
  if (PHP_OS_FAMILY === 'Windows') {
      die('skip not for Windows');
  }
  ?>
  --FILE--
  <?php
  $dir = __DIR__ . '/gh21992';
  $targetDir = $dir . '/target_dir';
  $deadLink = $dir . '/dead_link.txt';
  $ghost = $targetDir . '/ghost.txt';
  $newTarget = $dir . '/something';

  mkdir($dir);
  mkdir($targetDir);

  var_dump(symlink($ghost, $deadLink));
  var_dump(readlink($deadLink));

  var_dump(symlink($newTarget, $deadLink));
  var_dump(is_link($deadLink));
  var_dump(readlink($deadLink));
  var_dump(is_link($ghost));
  ?>
  --CLEAN--
  <?php
  $dir = __DIR__ . '/gh21992';
  $targetDir = $dir . '/target_dir';
  $deadLink = $dir . '/dead_link.txt';
  $ghost = $targetDir . '/ghost.txt';

  @unlink($ghost);
  @unlink($deadLink);
  @rmdir($targetDir);
  @rmdir($dir);
  ?>
  --EXPECTF--
  bool(true)
  string(%d) "%s/gh21992/target_dir/ghost.txt"

  Warning: symlink(): File exists in %s on line %d
  bool(false)
  bool(true)
  string(%d) "%s/gh21992/target_dir/ghost.txt"
  bool(false)

@zaaarf zaaarf changed the title ext/standard: Fix symlink trying to resolve dangling links GH-21992: ext/standard: Fix symlink trying to resolve dangling links May 12, 2026
@zaaarf
Copy link
Copy Markdown
Author

zaaarf commented May 12, 2026

There is a --clean-- section you can fill in to clean your tmp symlinks. See if this test works:

Oh cool, thanks, good to know. I'll do it in a bit.

@LamentXU123
Copy link
Copy Markdown
Contributor

LamentXU123 commented May 12, 2026

You may also need to add a new entry in NEWS and UPGRADING since this is a (minor) bug fix :)
The implementation makes sense (?) See if the CI is happy after the regression test.

@zaaarf zaaarf marked this pull request as draft May 12, 2026 16:53
@zaaarf
Copy link
Copy Markdown
Author

zaaarf commented May 12, 2026

needs a bit of work, found inconsistent behaviour

Comment thread ext/standard/link.c Outdated
@zaaarf zaaarf requested a review from devnexen May 12, 2026 23:00
@zaaarf zaaarf marked this pull request as ready for review May 12, 2026 23:00
@zaaarf
Copy link
Copy Markdown
Author

zaaarf commented May 12, 2026

Wrote something that actually makes sense this time, and wrote a test.

Sorry about earlier. That's what I get for trying to venture in PHP codebase at 5 in the morning.

I do want to say though, I don't really like that the specific warning we are emitting for this case is "File exists". Could we go with something more specific, like, "Dangling symlink"?

@zaaarf
Copy link
Copy Markdown
Author

zaaarf commented May 12, 2026

Oh, yeah @LamentXU123 want me to mark you as co author of the test?

@zaaarf zaaarf changed the title GH-21992: ext/standard: Fix symlink trying to resolve dangling links Fix GH-21992: ext/standard: Fix symlink trying to resolve dangling links May 12, 2026
Copy link
Copy Markdown
Contributor

@LamentXU123 LamentXU123 left a comment

Choose a reason for hiding this comment

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

I don't run the suggested test locally so it seems like I forgot to check the logic to deal with Windows

Oh, yeah @LamentXU123 want me to mark you as co author of the test?

You can if you want to, I don't have write access. Weilin Du 108666168+LamentXU123@users.noreply.github.com No worries about that I am happy to help :)

Oh, seems like You've fixed the test already :) Hmmmm, I think the test fail just because it didn't start with skip. Or maybe I am forgetting something. Thanks!

Comment on lines +4 to +8
<?php
if (PHP_OS_FAMILY === 'Windows') {
die('not for Windows');
}
?>
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.

Suggested change
<?php
if (PHP_OS_FAMILY === 'Windows') {
die('not for Windows');
}
?>
<?php
if (PHP_OS_FAMILY === 'Windows') {
die('skip not for Windows');
}
?>

SKIPIF section must have a output starting from skip. Now this may fix the CI on Windows.

Copy link
Copy Markdown
Author

@zaaarf zaaarf May 13, 2026

Choose a reason for hiding this comment

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

That's on me I guess. For now I tried pasting the skipif from another symlink-related test, let's see if that works.

Comment thread ext/standard/link.c
php_error_docref(NULL, E_WARNING, "File exists in %s", frompath);
RETURN_FALSE;
}

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.

Now, I think there is a edge case,

You see, your check happens before we resolve the path from expand_filepath and since this is a file operation, we must consider race condition. A file can pass your check and suddenly be changed into dangling links to cause a bug.

I hope this make sense to you.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I suppose, but I don't think there's a way to make the check + symlink operation atomic, is there?

Copy link
Copy Markdown
Contributor

@LamentXU123 LamentXU123 May 13, 2026

Choose a reason for hiding this comment

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

I don't think there's a way to make the check + symlink operation atomic, is there?

AFAIK there may be, but only by letting the kernel do it, ideally via symlink() on the unmodified final path or symlinkat() on an opened parent directory.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

or symlinkat() on an opened parent directory.

Would that check for the symlink being dangling though? If so, that's pretty convenient. We have zend_dirname to get tofd and its length, at least.

Copy link
Copy Markdown
Contributor

@LamentXU123 LamentXU123 May 13, 2026

Choose a reason for hiding this comment

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

or symlinkat() on an opened parent directory.

Would that check for the symlink being dangling though? If so, that's pretty convenient. We have zend_dirname to get tofd and its length, at least.

Yes.

  • if the final name does not exist, it creates the new symlink
  • if the final name already exists as anything at all, including a dangling symlink, it fails with EEXIST

Copy link
Copy Markdown
Author

@zaaarf zaaarf May 13, 2026

Choose a reason for hiding this comment

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

Okay well, are you sure? I tried with symlinkat just now and I'm getting the same buggy behaviour described by OP (at least, I think so? The test is failing because symlink now returns true on PHP). I removed my dangling check, and I'm replacing the php_sys_symlink call with this:

const char *basename = source_p + len + 1; // +1 to remove the slash
int dirfd = open(dirname, O_RDONLY | O_DIRECTORY);
ret = symlinkat(topath, dirfd, basename);
close(dirfd);

I'm going to sleep for tonight now, so don't worry if I don't reply for a while.

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.

This seems fine. Let's wait for David's opinion :)

@LamentXU123
Copy link
Copy Markdown
Contributor

I do want to say though, I don't really like that the specific warning we are emitting for this case is "File exists". Could we go with something more specific, like, "Dangling symlink"?

I don't like this either. This is actually misleading and frankly, wrong. The doc reads:

"The function fails, and issues E_WARNING, if link already exists."

So, it will surely be better to specify the error message. But lets wait for @devnexen 's opinion on this.

@zaaarf
Copy link
Copy Markdown
Author

zaaarf commented May 13, 2026

The doc reads

Fair enough, I took OP's word on that. As long as we respect the warning level, we should be respecting the docs, then, no matter what the message is.

@LamentXU123
Copy link
Copy Markdown
Contributor

LamentXU123 commented May 13, 2026

@zaaarf The CI is failing again but this is a known issue #21919. You can force push this again to rerun CI. The test now seems to be successfully passed except for the unreleated one. Thank you.

Also, you may need to remove the compile_commands.json you (accidentally) uploaded.

@zaaarf
Copy link
Copy Markdown
Author

zaaarf commented May 13, 2026

Lol, yeah. Will do. I'm usually more careful, not sure why I'm messing up so much in these.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants