Skip to content

Two processes creating the cache directory at once make one of them throw "Temporary files directory is not writable" #203

Description

@jakejackson1

When two processes build an Mpdf against the same tempDir while its cache directory does not exist, both see it missing and both mkdir() it. The one that loses gets mkdir(): File exists, and Cache::createBasePath() returns false without looking again, so the constructor throws Temporary files directory "…/mpdf" is not writable for a directory that exists and is writable.

Gravity PDF hits this after its temp-directory cleanup removes tempDir/mpdf and two PDFs are then generated at the same moment (e.g. two form submissions, or a notification and a download). The thrown PDF is dropped from the email. Upstream has the same report open as mpdf#1775; mpdf#534 is the same warning with a permissions cause.

Where

  • src/Cache.php, createBasePath(): if (!file_exists($basePath)) { if (!$this->createDirectory($basePath)) { return false; } }.
  • src/Cache.php, createDirectory(): if (!mkdir($basePath, $permissions, true)) { return false; }.
  • src/ServiceFactory.php constructs two of these per Mpdf (tempDir/mpdf and tempDir/mpdf/ttfontdata), so either can lose.

Measured

PHP 8.5, src/Cache.php from origin/gravitypdf (identical to the copy Gravity PDF 6.17 ships). Each round deletes the directory, then starts four CLI processes that wait on a flag file and each run new Cache($base . '/mpdf'):

<?php
namespace Mpdf; class MpdfException extends \Exception {}
require __DIR__ . '/Cache.php';
[$base, $go] = [$argv[1], $argv[2]];
while (!file_exists($go)) { usleep(50); }
try { new Cache($base . '/mpdf'); echo "ok\n"; }
catch (MpdfException $e) { echo 'FAIL: ' . $e->getMessage() . "\n"; }

170 of 200 rounds had at least one process throw Temporary files directory ".../tmp/mpdf" is not writable, preceded by Warning: mkdir(): File exists in Cache.php on line 47.

Expected

A directory that another process created in the meantime counts as created. mkdir() failing is only a failure if the path is still not a directory afterwards, the usual race-safe form:

if (!mkdir($basePath, $permissions, true) && !is_dir($basePath)) {
	return false;
}

The mkdir() warning should also not reach an error handler that converts warnings to exceptions (as in mpdf#1775's stack trace), so the call wants @ or an is_dir() re-check before the warning is raised. The umask chmod() loop that follows should only run for directories this process created.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions