From 09b0ebb714e009aaa74b6dad6116a2d2f2adb060 Mon Sep 17 00:00:00 2001 From: Gabriel Pereira do Carmo Date: Tue, 13 Oct 2026 16:55:07 -0300 Subject: [PATCH 1/5] feat: add SELinux status detection Signed-off-by: Gabriel Pereira do Carmo --- README.md | 29 ++++++++++ src/OperatingSystem.php | 37 +++++++++++++ tests/Unit/OperatingSystemTest.php | 87 ++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+) diff --git a/README.md b/README.md index 753d76c..81089bd 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,35 @@ $operatingSystem = new OperatingSystem(); $operatingSystem->getLinuxDistribution(); ``` + +### SELinux status detection + +The `getSELinuxStatus()` method detects the current SELinux enforcement status without requiring administrative privileges. + +Usage example: + +```php +getSELinuxStatus(); + +if ($status === OperatingSystem::SELINUX_ENFORCING) { + // SELinux is enforcing access-control rules. +} +``` + +Possible return values: + +- `enforcing`: SELinux is active and enforcing security policies. +- `permissive`: SELinux is active but not enforcing security policies. +- `unavailable`: The SELinux status file does not exist. +- `unknown`: The SELinux status could not be determined. + +The method reads `/sys/fs/selinux/enforce` and does not modify system settings. + ## How to contribute Feel free to fork, improve and I will be very happy to receive pull requests with your improvements. diff --git a/src/OperatingSystem.php b/src/OperatingSystem.php index c1607e3..0cd24e3 100644 --- a/src/OperatingSystem.php +++ b/src/OperatingSystem.php @@ -10,10 +10,45 @@ class OperatingSystem { + public const SELINUX_ENFORCING = 'enforcing'; + public const SELINUX_PERMISSIVE = 'permissive'; + public const SELINUX_UNAVAILABLE = 'unavailable'; + public const SELINUX_UNKNOWN = 'unknown'; + + public function __construct( private string $rootFolder = '/', ) { } + + /** + * Returns the current SELinux enforcement status. + */ + public function getSELinuxStatus(): string + { + $path = $this->rootFolder . '/sys/fs/selinux/enforce'; + + if (!file_exists($path)) { + return self::SELINUX_UNAVAILABLE; + } + + if (!$this->isInOpenBasedir($path) || !is_readable($path)) { + return self::SELINUX_UNKNOWN; + } + + $value = @file_get_contents($path); + + if ($value === false) { + return self::SELINUX_UNKNOWN; + } + + return match (trim($value)) { + '1' => self::SELINUX_ENFORCING, + '0' => self::SELINUX_PERMISSIVE, + default => self::SELINUX_UNKNOWN, + }; + } + /** * Returns PHP_OS_FAMILY (if defined (which it is on PHP >= 7.2)). * Returns a string (compatible with PHP_OS_FAMILY) derived from PHP_OS otherwise. @@ -23,6 +58,8 @@ public function __construct( * - https://github.com/sebastianbergmann/environment/issues/21 * - https://unix.stackexchange.com/questions/6345/how-can-i-get-distribution-name-and-version-number-in-a-simple-shell-script */ + + public function getFamily(): string { if (defined('PHP_OS_FAMILY')) { diff --git a/tests/Unit/OperatingSystemTest.php b/tests/Unit/OperatingSystemTest.php index 9ce8a5c..9dae6fc 100644 --- a/tests/Unit/OperatingSystemTest.php +++ b/tests/Unit/OperatingSystemTest.php @@ -34,6 +34,93 @@ public function testGetLinuxDistribution(string $expected): void $this->assertEquals(strtolower($expected), strtolower($actual)); } + #[DataProvider('dataSELinuxStatus')] + public function testGetSELinuxStatus( + string $content, + string $expected + ): void { + $tempFolder = sys_get_temp_dir() + . '/whatosami-selinux-' . bin2hex(random_bytes(8)); + + $selinuxFolder = $tempFolder . '/sys/fs/selinux'; + $enforceFile = $selinuxFolder . '/enforce'; + + try { + $this->assertTrue( + mkdir($selinuxFolder, 0755, true) + ); + + file_put_contents($enforceFile, $content); + + $instance = new OperatingSystem($tempFolder); + + $this->assertSame( + $expected, + $instance->getSELinuxStatus() + ); + } finally { + if (file_exists($enforceFile)) { + unlink($enforceFile); + } + + if (is_dir($selinuxFolder)) { + rmdir($selinuxFolder); + rmdir($tempFolder . '/sys/fs'); + rmdir($tempFolder . '/sys'); + rmdir($tempFolder); + } + } + } + + public static function dataSELinuxStatus(): array + { + return [ + 'enforcing' => [ + '1', + OperatingSystem::SELINUX_ENFORCING, + ], + 'enforcing with newline' => [ + "1\n", + OperatingSystem::SELINUX_ENFORCING, + ], + 'permissive' => [ + '0', + OperatingSystem::SELINUX_PERMISSIVE, + ], + 'permissive with newline' => [ + "0\n", + OperatingSystem::SELINUX_PERMISSIVE, + ], + 'empty content' => [ + '', + OperatingSystem::SELINUX_UNKNOWN, + ], + 'invalid content' => [ + 'unexpected', + OperatingSystem::SELINUX_UNKNOWN, + ], + ]; + } + + public function testGetSELinuxStatusUnavailable(): void + { + $tempFolder = sys_get_temp_dir() + . '/whatosami-selinux-' . bin2hex(random_bytes(8)); + + mkdir($tempFolder, 0755, true); + + try { + $instance = new OperatingSystem($tempFolder); + + $this->assertSame( + OperatingSystem::SELINUX_UNAVAILABLE, + $instance->getSELinuxStatus() + ); + } finally { + rmdir($tempFolder); + } + } + public static function dataGetLinuxDistribution(): array { return [ From 07757f33558864b6a908df63d550c714a4036dd6 Mon Sep 17 00:00:00 2001 From: Gabriel Pereira do Carmo Date: Wed, 21 Oct 2026 19:21:27 -0300 Subject: [PATCH 2/5] style: remove extra blank lines Signed-off-by: Gabriel Pereira do Carmo --- src/OperatingSystem.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/OperatingSystem.php b/src/OperatingSystem.php index 0cd24e3..e0ebecc 100644 --- a/src/OperatingSystem.php +++ b/src/OperatingSystem.php @@ -15,7 +15,6 @@ class OperatingSystem public const SELINUX_UNAVAILABLE = 'unavailable'; public const SELINUX_UNKNOWN = 'unknown'; - public function __construct( private string $rootFolder = '/', ) { @@ -58,8 +57,6 @@ public function getSELinuxStatus(): string * - https://github.com/sebastianbergmann/environment/issues/21 * - https://unix.stackexchange.com/questions/6345/how-can-i-get-distribution-name-and-version-number-in-a-simple-shell-script */ - - public function getFamily(): string { if (defined('PHP_OS_FAMILY')) { From cac89f3fa4e1a773c23d450e3d149b0b28de3d82 Mon Sep 17 00:00:00 2001 From: Gabriel Pereira do Carmo Date: Sat, 24 Oct 2026 01:16:02 -0300 Subject: [PATCH 3/5] fix: handle SELinux status with open_basedir restrictions Signed-off-by: Gabriel Pereira do Carmo --- src/OperatingSystem.php | 50 ++++++++++- tests/Unit/OperatingSystemTest.php | 134 +++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+), 2 deletions(-) diff --git a/src/OperatingSystem.php b/src/OperatingSystem.php index e0ebecc..c8932d7 100644 --- a/src/OperatingSystem.php +++ b/src/OperatingSystem.php @@ -27,11 +27,15 @@ public function getSELinuxStatus(): string { $path = $this->rootFolder . '/sys/fs/selinux/enforce'; + if (!$this->isInOpenBasedir($path)) { + return self::SELINUX_UNKNOWN; + } + if (!file_exists($path)) { return self::SELINUX_UNAVAILABLE; } - if (!$this->isInOpenBasedir($path) || !is_readable($path)) { + if (!is_readable($path)) { return self::SELINUX_UNKNOWN; } @@ -168,9 +172,51 @@ private function runSafe(string $command): string private function isInOpenBasedir(string $path): bool { $openBasedir = ini_get('open_basedir'); - if (empty($openBasedir) || strpos($openBasedir, $path) !== false) { + if ($openBasedir === false || $openBasedir === '') { return true; } + + $normalizedPath = str_replace('\\', '/', $path); + if (DIRECTORY_SEPARATOR === '\\') { + $normalizedPath = strtolower($normalizedPath); + } + + foreach (explode(PATH_SEPARATOR, $openBasedir) as $directory) { + // A dot in open_basedir refers to the current working directory. + if ($directory === '.') { + $directory = getcwd(); + if ($directory === false) { + continue; + } + } + + $normalizedDirectory = str_replace('\\', '/', $directory); + if ($normalizedDirectory === '/') { + return true; + } + + $normalizedDirectory = rtrim($normalizedDirectory, '/'); + if ($normalizedDirectory === '') { + continue; + } + if (DIRECTORY_SEPARATOR === '\\') { + $normalizedDirectory = strtolower($normalizedDirectory); + + // Preserve the slash separating a drive root from its children. + if (preg_match('/^[a-z]:$/', $normalizedDirectory) === 1) { + if (str_starts_with($normalizedPath, $normalizedDirectory . '/')) { + return true; + } + continue; + } + } + + if ($normalizedPath === $normalizedDirectory || + str_starts_with($normalizedPath, $normalizedDirectory . '/')) { + return true; + } + } + return false; } } diff --git a/tests/Unit/OperatingSystemTest.php b/tests/Unit/OperatingSystemTest.php index 9dae6fc..1aecac4 100644 --- a/tests/Unit/OperatingSystemTest.php +++ b/tests/Unit/OperatingSystemTest.php @@ -4,6 +4,7 @@ use LibreSign\WhatOSAmI\OperatingSystem; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RunInSeparateProcess; use PHPUnit\Framework\TestCase; /** @@ -72,6 +73,139 @@ public function testGetSELinuxStatus( } } + #[RunInSeparateProcess] + public function testGetSELinuxStatusWithOpenBasedirAllowed(): void + { + $tempFolder = sys_get_temp_dir() + . '/whatosami-openbasedir-' . bin2hex(random_bytes(8)); + $selinuxFolder = $tempFolder . '/sys/fs/selinux'; + $enforceFile = $selinuxFolder . '/enforce'; + + try { + $this->assertTrue(mkdir($selinuxFolder, 0755, true)); + $this->assertNotFalse(file_put_contents($enforceFile, '1')); + + $result = $this->detectSELinuxWithOpenBasedir($tempFolder, $tempFolder); + + $this->assertSame([], $result['warnings']); + $this->assertSame(OperatingSystem::SELINUX_ENFORCING, $result['status']); + } finally { + if (file_exists($enforceFile)) { + unlink($enforceFile); + } + if (is_dir($selinuxFolder)) { + rmdir($selinuxFolder); + rmdir($tempFolder . '/sys/fs'); + rmdir($tempFolder . '/sys'); + rmdir($tempFolder); + } + } + } + + #[RunInSeparateProcess] + public function testGetSELinuxStatusWithOpenBasedirBlocked(): void + { + $tempFolder = sys_get_temp_dir() + . '/whatosami-openbasedir-' . bin2hex(random_bytes(8)); + $selinuxFolder = $tempFolder . '/sys/fs/selinux'; + $enforceFile = $selinuxFolder . '/enforce'; + $allowedFolder = $tempFolder . '/allowed'; + + try { + $this->assertTrue(mkdir($selinuxFolder, 0755, true)); + $this->assertNotFalse(file_put_contents($enforceFile, '1')); + $this->assertTrue(mkdir($allowedFolder)); + + $result = $this->detectSELinuxWithOpenBasedir($tempFolder, $allowedFolder); + + $this->assertSame([], $result['warnings']); + $this->assertSame(OperatingSystem::SELINUX_UNKNOWN, $result['status']); + } finally { + if (file_exists($enforceFile)) { + unlink($enforceFile); + } + if (is_dir($allowedFolder)) { + rmdir($allowedFolder); + } + if (is_dir($selinuxFolder)) { + rmdir($selinuxFolder); + rmdir($tempFolder . '/sys/fs'); + rmdir($tempFolder . '/sys'); + rmdir($tempFolder); + } + } + } + + #[RunInSeparateProcess] + public function testGetSELinuxStatusWithOpenBasedirMissingFile(): void + { + $tempFolder = sys_get_temp_dir() + . '/whatosami-openbasedir-' . bin2hex(random_bytes(8)); + + try { + $this->assertTrue(mkdir($tempFolder, 0755, true)); + + $result = $this->detectSELinuxWithOpenBasedir($tempFolder, $tempFolder); + + $this->assertSame([], $result['warnings']); + $this->assertSame(OperatingSystem::SELINUX_UNAVAILABLE, $result['status']); + } finally { + if (is_dir($tempFolder)) { + rmdir($tempFolder); + } + } + } + + /** + * Run the restriction in a nested PHP process. PHPUnit writes isolation + * results to the OS temp directory, so applying open_basedir inside the + * PHPUnit worker itself would prevent PHPUnit from reporting the test. + * The parent process can also reliably remove the temporary fixtures. + * + * @return array{status: string, warnings: list} + */ + private function detectSELinuxWithOpenBasedir(string $rootFolder, string $allowedFolder): array + { + $sourceFile = dirname(__DIR__, 2) . '/src/OperatingSystem.php'; + $script = <<<'PHP' + require $argv[1]; + + $warnings = []; + set_error_handler(static function (int $severity, string $message) use (&$warnings): bool { + $warnings[] = $message; + return true; + }); + + if (ini_set('open_basedir', $argv[3]) === false) { + fwrite(STDERR, 'Unable to enable open_basedir'); + exit(2); + } + + $status = (new \LibreSign\WhatOSAmI\OperatingSystem($argv[2]))->getSELinuxStatus(); + restore_error_handler(); + echo json_encode(['status' => $status, 'warnings' => $warnings], JSON_THROW_ON_ERROR); + PHP; + + $command = [PHP_BINARY, '-r', $script, '--', $sourceFile, $rootFolder, $allowedFolder]; + $process = proc_open( + $command, + [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], + $pipes + ); + + $this->assertIsResource($process, 'Unable to start isolated PHP process'); + $output = stream_get_contents($pipes[1]); + $errors = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + $exitCode = proc_close($process); + + $this->assertSame(0, $exitCode, $errors); + $this->assertSame('', $errors); + + return json_decode($output, true, 512, JSON_THROW_ON_ERROR); + } + public static function dataSELinuxStatus(): array { return [ From 89a6eebafb6d58853d6880f4ccee34430b4f281c Mon Sep 17 00:00:00 2001 From: Gabriel Pereira do Carmo Date: Sat, 24 Oct 2026 02:40:19 -0300 Subject: [PATCH 4/5] fix: handle SELinux open_basedir path edge cases Signed-off-by: Gabriel Pereira do Carmo --- src/OperatingSystem.php | 99 ++++++--- tests/Unit/OperatingSystemTest.php | 319 ++++++++++++++++++++++++++++- 2 files changed, 386 insertions(+), 32 deletions(-) diff --git a/src/OperatingSystem.php b/src/OperatingSystem.php index c8932d7..54659e2 100644 --- a/src/OperatingSystem.php +++ b/src/OperatingSystem.php @@ -27,11 +27,30 @@ public function getSELinuxStatus(): string { $path = $this->rootFolder . '/sys/fs/selinux/enforce'; - if (!$this->isInOpenBasedir($path)) { + // Let PHP resolve the effective path, including symbolic links. + // A blocked open_basedir check emits a warning; keep it local and + // report UNKNOWN rather than confusing restricted access with absence. + $restrictedAccess = false; + if (ini_get('open_basedir')) { + set_error_handler(static function () use (&$restrictedAccess): bool { + $restrictedAccess = true; + return true; + }, E_WARNING); + + try { + $exists = file_exists($path); + } finally { + restore_error_handler(); + } + } else { + $exists = file_exists($path); + } + + if ($restrictedAccess) { return self::SELINUX_UNKNOWN; } - if (!file_exists($path)) { + if (!$exists) { return self::SELINUX_UNAVAILABLE; } @@ -176,47 +195,67 @@ private function isInOpenBasedir(string $path): bool return true; } - $normalizedPath = str_replace('\\', '/', $path); - if (DIRECTORY_SEPARATOR === '\\') { - $normalizedPath = strtolower($normalizedPath); + $normalizedPath = $this->normalizePathForOpenBasedir($path); + if ($normalizedPath === null) { + return false; } foreach (explode(PATH_SEPARATOR, $openBasedir) as $directory) { - // A dot in open_basedir refers to the current working directory. - if ($directory === '.') { - $directory = getcwd(); - if ($directory === false) { - continue; - } + $normalizedDirectory = $this->normalizePathForOpenBasedir($directory); + if ($normalizedDirectory === null) { + continue; } - $normalizedDirectory = str_replace('\\', '/', $directory); - if ($normalizedDirectory === '/') { + if ($normalizedDirectory === '/' || + $normalizedPath === $normalizedDirectory || + str_starts_with($normalizedPath, rtrim($normalizedDirectory, '/') . '/')) { return true; } + } - $normalizedDirectory = rtrim($normalizedDirectory, '/'); - if ($normalizedDirectory === '') { - continue; + return false; + } + + /** + * Normalize separators, repeated slashes, and dot segments before checking + * directory boundaries. No filesystem access is performed: PHP can reject + * realpath/file_exists calls outside open_basedir with a warning. + */ + private function normalizePathForOpenBasedir(string $path): ?string + { + $path = str_replace('\\', '/', $path); + if ($path === '') { + return null; + } + + $drive = ''; + if (DIRECTORY_SEPARATOR === '\\' && preg_match('/^[a-z]:\//i', $path) === 1) { + $drive = strtolower(substr($path, 0, 2)); + $path = substr($path, 2); + } elseif (!str_starts_with($path, '/')) { + $cwd = getcwd(); + if ($cwd === false) { + return null; } - if (DIRECTORY_SEPARATOR === '\\') { - $normalizedDirectory = strtolower($normalizedDirectory); - - // Preserve the slash separating a drive root from its children. - if (preg_match('/^[a-z]:$/', $normalizedDirectory) === 1) { - if (str_starts_with($normalizedPath, $normalizedDirectory . '/')) { - return true; - } - continue; - } + $path = str_replace('\\', '/', $cwd) . '/' . $path; + if (DIRECTORY_SEPARATOR === '\\' && preg_match('/^[a-z]:\//i', $path) === 1) { + $drive = strtolower(substr($path, 0, 2)); + $path = substr($path, 2); } + } - if ($normalizedPath === $normalizedDirectory || - str_starts_with($normalizedPath, $normalizedDirectory . '/')) { - return true; + $parts = []; + foreach (explode('/', $path) as $part) { + if ($part === '' || $part === '.') { + continue; } + if ($part === '..') { + array_pop($parts); + continue; + } + $parts[] = DIRECTORY_SEPARATOR === '\\' ? strtolower($part) : $part; } - return false; + return $drive . '/' . implode('/', $parts); } } diff --git a/tests/Unit/OperatingSystemTest.php b/tests/Unit/OperatingSystemTest.php index 1aecac4..738c4db 100644 --- a/tests/Unit/OperatingSystemTest.php +++ b/tests/Unit/OperatingSystemTest.php @@ -136,6 +136,303 @@ public function testGetSELinuxStatusWithOpenBasedirBlocked(): void } } + #[RunInSeparateProcess] + public function testGetSELinuxStatusWithOpenBasedirDoubleLeadingSlash(): void + { + if (DIRECTORY_SEPARATOR !== '/') { + $this->markTestSkipped('Double-leading-slash paths are tested on Unix systems.'); + } + + $tempFolder = sys_get_temp_dir() + . '/whatosami-openbasedir-' . bin2hex(random_bytes(8)); + $selinuxFolder = $tempFolder . '/sys/fs/selinux'; + $enforceFile = $selinuxFolder . '/enforce'; + + try { + $this->assertTrue(mkdir($selinuxFolder, 0755, true)); + $this->assertNotFalse(file_put_contents($enforceFile, '1')); + + $regular = $this->detectSELinuxWithOpenBasedir($tempFolder, $tempFolder); + $doubleSlash = $this->detectSELinuxWithOpenBasedir('/' . $tempFolder, $tempFolder); + + $this->assertSame([], $regular['warnings']); + $this->assertSame(OperatingSystem::SELINUX_ENFORCING, $regular['status']); + $this->assertSame([], $doubleSlash['warnings']); + $this->assertSame(OperatingSystem::SELINUX_ENFORCING, $doubleSlash['status']); + } finally { + if (file_exists($enforceFile)) { + unlink($enforceFile); + } + if (is_dir($selinuxFolder)) { + rmdir($selinuxFolder); + rmdir($tempFolder . '/sys/fs'); + rmdir($tempFolder . '/sys'); + rmdir($tempFolder); + } + } + } + + #[RunInSeparateProcess] + public function testGetSELinuxStatusWithOpenBasedirParentTraversal(): void + { + $tempFolder = sys_get_temp_dir() + . '/whatosami-openbasedir-' . bin2hex(random_bytes(8)); + $selinuxFolder = $tempFolder . '/blocked/sys/fs/selinux'; + $enforceFile = $selinuxFolder . '/enforce'; + $allowedFolder = $tempFolder . '/allowed'; + + try { + $this->assertTrue(mkdir($selinuxFolder, 0755, true)); + $this->assertNotFalse(file_put_contents($enforceFile, '1')); + $this->assertTrue(mkdir($allowedFolder)); + + $result = $this->detectSELinuxWithOpenBasedir( + $tempFolder . '/allowed/../blocked', + $allowedFolder + ); + + $this->assertSame([], $result['warnings']); + $this->assertSame(OperatingSystem::SELINUX_UNKNOWN, $result['status']); + } finally { + if (file_exists($enforceFile)) { + unlink($enforceFile); + } + if (is_dir($allowedFolder)) { + rmdir($allowedFolder); + } + if (is_dir($selinuxFolder)) { + rmdir($selinuxFolder); + rmdir($tempFolder . '/blocked/sys/fs'); + rmdir($tempFolder . '/blocked/sys'); + rmdir($tempFolder . '/blocked'); + rmdir($tempFolder); + } + } + } + + #[RunInSeparateProcess] + public function testGetSELinuxStatusWithOpenBasedirSimilarDirectoryPrefix(): void + { + $tempFolder = sys_get_temp_dir() + . '/whatosami-openbasedir-' . bin2hex(random_bytes(8)); + $allowedFolder = $tempFolder . '/allowed'; + $blockedFolder = $tempFolder . '/allowed-extra'; + $selinuxFolder = $blockedFolder . '/sys/fs/selinux'; + $enforceFile = $selinuxFolder . '/enforce'; + + try { + $this->assertTrue(mkdir($allowedFolder, 0755, true)); + $this->assertTrue(mkdir($selinuxFolder, 0755, true)); + $this->assertNotFalse(file_put_contents($enforceFile, '1')); + + $result = $this->detectSELinuxWithOpenBasedir($blockedFolder, $allowedFolder); + + $this->assertSame([], $result['warnings']); + $this->assertSame(OperatingSystem::SELINUX_UNKNOWN, $result['status']); + } finally { + if (file_exists($enforceFile)) { + unlink($enforceFile); + } + if (is_dir($selinuxFolder)) { + rmdir($selinuxFolder); + rmdir($blockedFolder . '/sys/fs'); + rmdir($blockedFolder . '/sys'); + rmdir($blockedFolder); + } + if (is_dir($allowedFolder)) { + rmdir($allowedFolder); + } + if (is_dir($tempFolder)) { + rmdir($tempFolder); + } + } + } + + #[RunInSeparateProcess] + #[DataProvider('dataAllowedOpenBasedirPathVariants')] + public function testGetSELinuxStatusWithOpenBasedirAllowedPathVariants( + string $rootSuffix, + bool $multipleAllowedDirectories, + bool $unixOnly + ): void { + if ($unixOnly && DIRECTORY_SEPARATOR !== '/') { + $this->markTestSkipped('Unix-specific path variation.'); + } + + $tempFolder = sys_get_temp_dir() + . '/whatosami-openbasedir-' . bin2hex(random_bytes(8)); + $selinuxFolder = $tempFolder . '/target/sys/fs/selinux'; + $enforceFile = $selinuxFolder . '/enforce'; + $unrelatedFolder = $tempFolder . '/unrelated'; + + try { + $this->assertTrue(mkdir($selinuxFolder, 0755, true)); + $this->assertNotFalse(file_put_contents($enforceFile, '1')); + $this->assertTrue(mkdir($unrelatedFolder)); + + $allowedPaths = $multipleAllowedDirectories + ? $unrelatedFolder . PATH_SEPARATOR . $tempFolder . '/target/' + : $tempFolder; + $result = $this->detectSELinuxWithOpenBasedir( + $tempFolder . $rootSuffix, + $allowedPaths + ); + + $this->assertSame([], $result['warnings']); + $this->assertSame(OperatingSystem::SELINUX_ENFORCING, $result['status']); + } finally { + if (file_exists($enforceFile)) { + unlink($enforceFile); + } + if (is_dir($selinuxFolder)) { + rmdir($selinuxFolder); + rmdir($tempFolder . '/target/sys/fs'); + rmdir($tempFolder . '/target/sys'); + rmdir($tempFolder . '/target'); + } + if (is_dir($unrelatedFolder)) { + rmdir($unrelatedFolder); + } + if (is_dir($tempFolder)) { + rmdir($tempFolder); + } + } + } + + public static function dataAllowedOpenBasedirPathVariants(): array + { + return [ + 'dot and parent segments staying inside allowed root' => ['/target/../target/.', false, false], + 'multiple allowed directories with trailing separator' => ['/target', true, false], + 'repeated separators inside allowed root' => ['/target///', false, true], + ]; + } + + #[RunInSeparateProcess] + public function testGetSELinuxStatusWithOpenBasedirSymlinkEscapingAllowedDirectory(): void + { + if (DIRECTORY_SEPARATOR !== '/') { + $this->markTestSkipped('Symbolic-link directory tests run on Unix systems.'); + } + + $tempFolder = sys_get_temp_dir() + . '/whatosami-openbasedir-' . bin2hex(random_bytes(8)); + $allowedFolder = $tempFolder . '/allowed'; + $blockedFolder = $tempFolder . '/blocked'; + $link = $allowedFolder . '/linked'; + $enforceFile = $blockedFolder . '/sys/fs/selinux/enforce'; + $selinuxFolder = dirname($enforceFile); + + try { + $this->assertTrue(mkdir($allowedFolder, 0755, true)); + $this->assertTrue(mkdir($selinuxFolder, 0755, true)); + $this->assertNotFalse(file_put_contents($enforceFile, '1')); + $this->assertTrue(symlink($blockedFolder, $link)); + + $result = $this->detectSELinuxWithOpenBasedir($link, $allowedFolder); + + $this->assertSame([], $result['warnings']); + $this->assertSame(OperatingSystem::SELINUX_UNKNOWN, $result['status']); + } finally { + if (is_link($link)) { + unlink($link); + } + if (file_exists($enforceFile)) { + unlink($enforceFile); + } + if (is_dir($selinuxFolder)) { + rmdir($selinuxFolder); + rmdir($blockedFolder . '/sys/fs'); + rmdir($blockedFolder . '/sys'); + rmdir($blockedFolder); + } + if (is_dir($allowedFolder)) { + rmdir($allowedFolder); + } + if (is_dir($tempFolder)) { + rmdir($tempFolder); + } + } + } + + #[RunInSeparateProcess] + public function testGetSELinuxStatusWithOpenBasedirSymlinkResolvingInsideAllowedDirectory(): void + { + if (DIRECTORY_SEPARATOR !== '/') { + $this->markTestSkipped('Symbolic-link directory tests run on Unix systems.'); + } + + $tempFolder = sys_get_temp_dir() + . '/whatosami-openbasedir-' . bin2hex(random_bytes(8)); + $allowedFolder = $tempFolder . '/allowed'; + $outsideFolder = $tempFolder . '/outside'; + $link = $outsideFolder . '/linked'; + $enforceFile = $allowedFolder . '/sys/fs/selinux/enforce'; + $selinuxFolder = dirname($enforceFile); + + try { + $this->assertTrue(mkdir($selinuxFolder, 0755, true)); + $this->assertTrue(mkdir($outsideFolder, 0755, true)); + $this->assertNotFalse(file_put_contents($enforceFile, '1')); + $this->assertTrue(symlink($allowedFolder, $link)); + + // PHP resolves the link to a file inside the allowed directory. + $result = $this->detectSELinuxWithOpenBasedir($link, $allowedFolder); + + $this->assertSame([], $result['warnings']); + $this->assertSame(OperatingSystem::SELINUX_ENFORCING, $result['status']); + } finally { + if (is_link($link)) { + unlink($link); + } + if (file_exists($enforceFile)) { + unlink($enforceFile); + } + if (is_dir($selinuxFolder)) { + rmdir($selinuxFolder); + rmdir($allowedFolder . '/sys/fs'); + rmdir($allowedFolder . '/sys'); + rmdir($allowedFolder); + } + if (is_dir($outsideFolder)) { + rmdir($outsideFolder); + } + if (is_dir($tempFolder)) { + rmdir($tempFolder); + } + } + } + + #[RunInSeparateProcess] + public function testGetSELinuxStatusWithOpenBasedirCurrentDirectory(): void + { + $tempFolder = sys_get_temp_dir() + . '/whatosami-openbasedir-' . bin2hex(random_bytes(8)); + $enforceFile = $tempFolder . '/sys/fs/selinux/enforce'; + $selinuxFolder = dirname($enforceFile); + + try { + $this->assertTrue(mkdir($selinuxFolder, 0755, true)); + $this->assertNotFalse(file_put_contents($enforceFile, '1')); + + // The nested PHP process changes cwd before enabling open_basedir='.'. + $result = $this->detectSELinuxWithOpenBasedir($tempFolder, '.', $tempFolder); + + $this->assertSame([], $result['warnings']); + $this->assertSame(OperatingSystem::SELINUX_ENFORCING, $result['status']); + } finally { + if (file_exists($enforceFile)) { + unlink($enforceFile); + } + if (is_dir($selinuxFolder)) { + rmdir($selinuxFolder); + rmdir($tempFolder . '/sys/fs'); + rmdir($tempFolder . '/sys'); + rmdir($tempFolder); + } + } + } + #[RunInSeparateProcess] public function testGetSELinuxStatusWithOpenBasedirMissingFile(): void { @@ -164,7 +461,11 @@ public function testGetSELinuxStatusWithOpenBasedirMissingFile(): void * * @return array{status: string, warnings: list} */ - private function detectSELinuxWithOpenBasedir(string $rootFolder, string $allowedFolder): array + private function detectSELinuxWithOpenBasedir( + string $rootFolder, + string $allowedFolder, + ?string $workingDirectory = null + ): array { $sourceFile = dirname(__DIR__, 2) . '/src/OperatingSystem.php'; $script = <<<'PHP' @@ -176,6 +477,11 @@ private function detectSELinuxWithOpenBasedir(string $rootFolder, string $allowe return true; }); + if (isset($argv[4]) && $argv[4] !== '' && !chdir($argv[4])) { + fwrite(STDERR, 'Unable to change working directory'); + exit(2); + } + if (ini_set('open_basedir', $argv[3]) === false) { fwrite(STDERR, 'Unable to enable open_basedir'); exit(2); @@ -186,7 +492,16 @@ private function detectSELinuxWithOpenBasedir(string $rootFolder, string $allowe echo json_encode(['status' => $status, 'warnings' => $warnings], JSON_THROW_ON_ERROR); PHP; - $command = [PHP_BINARY, '-r', $script, '--', $sourceFile, $rootFolder, $allowedFolder]; + $command = [ + PHP_BINARY, + '-r', + $script, + '--', + $sourceFile, + $rootFolder, + $allowedFolder, + $workingDirectory ?? '', + ]; $process = proc_open( $command, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], From 406faed887f331c728e1b64eefd116670fb72583 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 27 Oct 2026 04:05:56 +0000 Subject: [PATCH 5/5] style: align SELinux test helper with PHP-CS rules Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/Unit/OperatingSystemTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Unit/OperatingSystemTest.php b/tests/Unit/OperatingSystemTest.php index 738c4db..d184fcd 100644 --- a/tests/Unit/OperatingSystemTest.php +++ b/tests/Unit/OperatingSystemTest.php @@ -465,8 +465,7 @@ private function detectSELinuxWithOpenBasedir( string $rootFolder, string $allowedFolder, ?string $workingDirectory = null - ): array - { + ): array { $sourceFile = dirname(__DIR__, 2) . '/src/OperatingSystem.php'; $script = <<<'PHP' require $argv[1];