Opened 6 months ago
Last modified 5 months ago
#60855 new defect (bug)
wp_mkdir_p can error when simultaneous requests attempt to create the same directory
Reported by: | grantmkin | Owned by: | |
---|---|---|---|
Milestone: | Awaiting Review | Priority: | normal |
Severity: | normal | Version: | |
Component: | General | Keywords: | |
Focuses: | Cc: |
Description
When two requests happen at the same time that both attempt to create a new directory with wp_mkdir_p
, a race condition can occur that results in one request creating the directory and the other triggering an error.
The issue happens when both of the requests reach the file_exists
check at nearly the same time:
The directory does not exist yet, so the check fails and both requests continue execution of the function. Then the requests are in a race to get to the code where the directory is actually created using mkdir
:
The first request to reach that point will successfully create the directory and return true, the second will return false when mkdir
fails because the directory already exists.
This causes an error upstream in the code because wp_mkdir_p
is expected to return true when the directory already exists.
Found when testing the font library on new installs, see https://github.com/WordPress/gutenberg/issues/59023#issuecomment-2022746082
Attachments (2)
Change History (6)
#2
in reply to:
↑ 1
;
follow-up:
↓ 3
@
5 months ago
Replying to peterwilsoncc:
The only technique I've been able to think of to resolve this issue is to use the a collision avoidance technique similar to that used by computer networks.
If calling
mkdir()
fails the first time, back of forusleep( wp_rand( 50, 250000 ) )
and try a second time. As the file system may not actually be writable, I think if it fails the second time it's safe to assume the issue is not a collision. The lower and upper limit would need to be figured out.
I don't love this solution so it would be nice to hear of other suggestions for resolving the race condition.
I'm not sure I understand why it needs to "try a second time"? If the directory has already been created, then nothing more would need to be done, right?
Or maybe you mean it needs to check whether the directory exists a second time? That could work, although I'm not sure why the time sleeping would need to be random in that case. Actually, I'm not sure it would need to sleep at all.
#3
in reply to:
↑ 2
@
5 months ago
Replying to siliconforks:
I'm not sure I understand why it needs to "try a second time"? If the directory has already been created, then nothing more would need to be done, right?
This is to handle a concurrency issue in many of the versions of PHP WordPress supports.
- Thread A needs to create
/path/to/uploads/2024/05
- Thread B needs to create
/path/to/uploads/fonts
- Thread A determines everything from
path
needs to be created - Thread A creates
path
- Thread B determines everything from
to
needs to be created - Thread B creates
to
- Thread A fails as
to
now exists - Thread A rechecks if
05
exists, it does not. - Thread B creates the remainder of the path to
fonts
The result being that one of the requests will still fail.
I'm not sure why the time sleeping would need to be random in that case. Actually, I'm not sure it would need to sleep at all.
The back-off would be random in an attempt to avoid a second collision. If both threads wait the same amount of time, the path creation would collide a second time a few milliseconds later.
Computer networks use Exponential backoff to try again and again, but I think this would be overkill for WordPress's purposes as there is a small risk the request would time out.
#4
@
5 months ago
If the problem is that the recursive version of mkdir()
(with third argument true
) is buggy, an alternative is to simply avoid using recursive mkdir()
and implement the recursion in PHP code.
(I'm not sure if this is actually worth doing, though, given that the PHP bug is said to be fixed in version 8.1, and older versions of PHP will probably not be supported by WordPress for very much longer.)
This is also reproducible via the block editor when uploading media.
Reproduction steps
wp site empty
wp_mkdir_p()
.The only technique I've been able to think of to resolve this issue is to use the a collision avoidance technique similar to that used by computer networks.
If calling
mkdir()
fails the first time, back of forusleep( wp_rand( 50, 250000 ) )
and try a second time. As the file system may not actually be writable, I think if it fails the second time it's safe to assume the issue is not a collision. The lower and upper limit would need to be figured out.I don't love this solution so it would be nice to hear of other suggestions for resolving the race condition.