Ein Auftrag ruft ein Konvertierungswerkzeug auf, und heraus kommt eine leere Zeichenkette. Keine Ausnahme, kein Eintrag im Protokoll, einfach nichts. Solche stummen Fehlschläge haben oft eine banale Ursache: Der Datei fehlt das Ausführungsrecht. PHP is_executable() stellt genau diese Frage, bevor der Aufruf überhaupt stattfindet. Lesen und Schreiben sind mit is_readable() und is_writable() jeweils eigene Themen. Hier geht es um das dritte Recht und um den Moment, in dem es zählt.
Zwei Dateien mit derselben Aufgabe, ein Zeichen Unterschied in der Rechteanzeige. Welche von beiden startet, entscheidet allein dieses Zeichen.
Was PHP is_executable() beantwortet und was nicht
Die Antwort ist wahr oder falsch. Wahr bedeutet: Der Pfad existiert, und der Systembenutzer, unter dem PHP gerade arbeitet, darf ihn starten. Dieser Zusatz ist der wichtigste Satz des ganzen Tutorials. Ausführbarkeit an sich gibt es nicht, immer nur eine für einen bestimmten Benutzer.
Im Webserver ist das je nach Aufbau www-data oder der Benutzer des FPM-Pools, auf der Kommandozeile der gerade angemeldete Benutzer. Dieselbe Datei kann deshalb im Browser falsch und im Terminal wahr ergeben. Wer PHP is_executable() aufruft, misst also die Sicht des laufenden Prozesses und nicht die eigene.
<?php
var_dump(is_executable('/bin/sh'));
/* bool(true) */
var_dump(is_executable('/etc/hosts'));
/* bool(false) */
Ein falsches Ergebnis sagt allerdings nicht, woran es liegt. Fehlende Datei, fehlendes Leserecht oder fehlendes x-Bit: PHP is_executable() antwortet in allen drei Fällen gleich. Die Unterscheidung muss der aufrufende Code selbst treffen, und genau dazu dient die gestufte Prüfung weiter unten.
| Funktion | Antwortet auf | Rückgabe |
is_readable() | darf der laufende Benutzer lesen | wahr oder falsch |
is_writable() | darf der laufende Benutzer schreiben | wahr oder falsch |
is_executable() | darf der laufende Benutzer starten oder betreten | wahr oder falsch |
fileperms() | welche Rechte stehen gesetzt | Zahl, die selbst ausgewertet werden muss |
Syntax: die Signatur im Original
Ein Parameter, ein Rückgabewert, keine Schalter. Zusammengefasst sieht die Funktion so aus:
is_executable(string $filename): bool
/* filename - path to the file to check
return value - true if the file exists and is
executable, false otherwise
This function tells whether the filename is a
file the current process may run. A file is
executable if the executable bit of the file
permissions is set for the user the PHP
process runs as; it returns false otherwise.
On POSIX systems a directory is executable
when it may be entered. On Windows there is
no executable bit at all, so the extension
decides instead.
Unlike the other filesystem functions it does
not read from the stat cache. Url wrappers
are a different matter: a phar:// path returns
false even where file_exists() says true. */
Der Rückgabetyp ist schlicht bool, es gibt also keinen dritten Zustand für unbekannt. Eine nicht vorhandene Datei ergibt dasselbe wie eine vorhandene ohne Recht. Die beiden letzten Sätze des Kommentars nehmen zwei Abschnitte dieses Tutorials vorweg: den Benutzer, aus dessen Sicht PHP is_executable() antwortet, und die abweichende Regel unter Windows.
File Permissions: das x-Bit und die drei Rechteklassen
Unter Linux und macOS hängen Rechte an drei Klassen: am Eigentümer der Datei, an ihrer Gruppe und an allen übrigen. Jede Klasse hat ihr eigenes Lese-, Schreib- und Ausführungsrecht. In der Oktalschreibweise steht 0755 für Lesen, Schreiben und Ausführen beim Eigentümer sowie Lesen und Ausführen für Gruppe und Rest.
Die Funktion beantwortet nicht, ob irgendwo ein x-Bit gesetzt ist. Sie beantwortet, ob das für den laufenden Benutzer zuständige Bit gesetzt ist. Eine Datei mit 0700 und einem fremden Eigentümer trägt ein x-Bit und ergibt trotzdem falsch. Fehlt das Recht wirklich, wird es mit chmod gesetzt, nicht hier geprüft.
flowchart TD
A[Pfad pruefen] --> B{existiert?}
B -->|nein| C[Pfad falsch]
B -->|ja| D{Datei oder Ordner?}
D -->|Ordner| E[x heisst Betreten]
D -->|Datei| F{is_executable?}
F -->|nein| G[x-Bit fehlt]
F -->|ja| H[exec moeglich]
Das Diagramm zeigt zugleich die beiden Bedeutungen des x-Bits. Bei einer Datei heißt es starten, bei einem Verzeichnis betreten. Beide Fälle laufen über dieselbe Funktion, und das sorgt für einen Teil der Verwirrung.
Praxis: die gestufte Prüfung vor exec() und proc_open()
Eine einzelne Abfrage reicht für die Entscheidung, aber nicht für eine brauchbare Fehlermeldung. Wer stattdessen vier Stufen nacheinander abfragt, bekommt aus dem stummen Fehlschlag eine Aussage, die sich ins Protokoll schreiben und einem Kunden zeigen lässt.
<?php
function programmPruefen(string $pfad): ?string
{
if (!file_exists($pfad)) {
return "Nicht gefunden: $pfad";
}
if (is_dir($pfad)) {
return "Ist ein Verzeichnis: $pfad";
}
if (!is_readable($pfad)) {
return "Nicht lesbar: $pfad";
}
if (!is_executable($pfad)) {
return "Kein x-Bit: $pfad";
}
return null;
}
$fehler = programmPruefen('/usr/local/bin/conv');
if ($fehler !== null) {
error_log($fehler);
}
Die Reihenfolge ist nicht beliebig. file_exists() zuerst, weil ein Tippfehler im Pfad der häufigste Fall ist. Dann is_dir(), weil PHP is_executable() bei einem Verzeichnis aus einem anderen Grund wahr sagt. Danach das Leserecht, und erst zum Schluss das Ausführungsrecht.
<?php
$prog = '/usr/local/bin/conv';
$datei = '/srv/upload/bild.tif';
if (!is_executable($prog)) {
$meldung = "Nicht startbar: $prog";
throw new RuntimeException($meldung);
}
$spec = [
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$p = proc_open([$prog, $datei], $spec, $pipes);
if (!is_resource($p)) {
error_log('Start fehlgeschlagen');
}
Bei einem Dienst, der immer dasselbe Werkzeug aufruft, gehört diese Prüfung in die Startphase und nicht in jeden einzelnen Auftrag. Dann fällt eine falsche Konfiguration beim Einrichten auf und nicht beim ersten Kunden.
<?php
final class Konverter
{
public function __construct(private string $bin)
{
if (!is_executable($bin)) {
throw new LogicException("x fehlt: $bin");
}
}
}
Shebang und x-Bit bei CLI-Skripten
Ein Shebang in der ersten Zeile legt fest, welcher Interpreter ein Skript startet. Er wirkt aber nur, wenn die Datei ausführbar ist. Ohne das Recht meldet die Shell schlicht, dass die Berechtigung fehlt, und die erste Zeile wird nie gelesen.
#!/usr/bin/env php
<?php
/* Ohne x-Bit startet diese Datei nicht,
auch wenn die erste Zeile stimmt.
Gesetzt wird das Recht mit chmod +x. */
echo "Wartung laeuft\n";
Das Recht geht öfter verloren, als man denkt. Ein Entpacken aus einem ZIP-Archiv, eine Übertragung per FTP und manches Deployment legen Dateien ohne x-Bit ab. Ein Installer, der die mitgelieferten Skripte mit PHP is_executable() durchgeht und fehlende Rechte benennt, erspart eine ganze Klasse von Supportfällen.
Bei einem Verzeichnis bedeutet x etwas anderes
Bei einem Verzeichnis steht das x-Bit für das Betretungsrecht, also für die Erlaubnis, auf die Einträge darin zuzugreifen. Ohne dieses Bit nützt ein gesetztes Leserecht wenig: Die Namensliste lässt sich dann zwar abrufen, aber keine Datei darin öffnen.
<?php
var_dump(is_executable('/tmp'));
/* bool(true) - x heisst hier Betreten */
if (is_executable('/tmp') && !is_dir('/tmp')) {
echo 'echtes Programm';
}
/* gibt nichts aus: /tmp ist ein Verzeichnis */
Ein wahres Ergebnis für /tmp ist deshalb kein Fehlverhalten, sondern die richtige Antwort auf eine Frage, die anders gemeint war. Wer sicher zwischen Programm und Verzeichnis unterscheiden will, kombiniert PHP is_executable() mit is_dir(). Bei einem symbolischen Link folgt die Prüfung dem Link und bewertet das Ziel, nicht den Eintrag. Ein Link auf ein gelöschtes Programm ergibt deshalb falsch, obwohl der Eintrag im Verzeichnis steht. Wer den Eintrag selbst meint, fragt vorher mit is_link() nach.
Unter Windows entscheidet die Dateiendung
Das POSIX-Rechtemodell gibt es dort nicht, also kann auch kein x-Bit abgefragt werden. Stattdessen entscheidet die Endung, und zwar enger als erwartet. Nachgemessen mit PHP 8.4 unter Windows: .exe und .com ergeben wahr, .bat und .cmd dagegen falsch, ebenso jede andere Endung und jede Datei ganz ohne. Eine reine Textdatei, die auf .exe endet, ergibt wahr; eine echte Stapelverarbeitung als .bat ergibt falsch.
Zwei naheliegende Vermutungen stimmen nicht. Die Liste hängt nicht an PATHEXT: Auch wenn dort nur .BAT und .CMD stehen, bleibt die Antwort für eine Stapeldatei falsch. Und fileperms() taugt nicht als zweite Meinung, denn es meldet für .bat und .cmd sehr wohl 0777 und damit ein gesetztes x-Bit, während PHP is_executable() nein sagt. Auch der Verzeichnisfall dreht sich um: Unter Windows ergibt ein Ordner falsch, unter Linux wahr.
<?php
$prog = PHP_OS_FAMILY === 'Windows'
? 'C:/tools/conv.exe'
: '/usr/local/bin/conv';
if (!is_executable($prog)) {
$hinweis = PHP_OS_FAMILY === 'Windows'
? 'Endung pruefen, etwa .exe oder .com'
: 'x-Bit fehlt, chmod +x setzen';
error_log($hinweis);
}
Die Regel ist dort nicht schwächer, sie ist eine andere. Für plattformübergreifenden Code folgt daraus zweierlei: PHP is_executable() taugt unter Windows als Hinweis, nicht als Zusage, und die Fehlermeldung sollte je nach Plattform etwas anderes vorschlagen. Eine Aufforderung zu chmod hilft auf einem Windows-Rechner niemandem weiter.
Zwei Grenzen gelten unabhängig von der Plattform. Die Prüfung ist auf echte Dateipfade ausgelegt, und an einem Stream-Wrapper ist die Antwort falsch, auch wenn an den Rechten nichts fehlt. Ein Phar-Archiv zeigt das deutlich: Für phar://archiv.phar/bin/tool.sh sagen file_exists() und is_readable() wahr, PHP is_executable() sagt falsch. Bei http:// und bei selbst angemeldeten Wrappern ist es dasselbe. Und ein open_basedir, das den Pfad aussperrt, führt ebenfalls zu falsch. Wer eine überraschende Antwort bekommt, prüft deshalb zuerst, ob der Pfad überhaupt im Zugriff des Prozesses liegt.
Statcache: warum access() hier am Cache vorbeigeht
PHP merkt sich Dateiinformationen je Anfrage, und in vielen Anleitungen steht deshalb, nach einem chmod() müsse erst clearstatcache() folgen. Für diese Funktion stimmt das nicht. Sie fragt über den Systemaufruf access() jedes Mal direkt beim Kernel nach und sieht eine Rechteänderung sofort. Nachgemessen mit PHP 8.4 unter Linux:
<?php
$pfad = '/srv/app/bin/import.sh';
chmod($pfad, 0755);
var_dump(is_executable($pfad));
/* bool(true) - ohne clearstatcache() */
printf("%o\n", fileperms($pfad) & 0777);
/* 755 - chmod() leert den Cache fuer
diesen Pfad gleich mit */
Der Zwischenspeicher ist trotzdem real. Er fällt nur auf, wenn die Änderung von außerhalb des eigenen Prozesses kommt, etwa durch einen parallel laufenden Dienst oder einen Upload während der Anfrage. Dann antworten fileperms(), filesize(), filemtime() und stat() weiter mit dem alten Stand, während PHP is_executable() längst den neuen meldet.
<?php
$pfad = '/srv/app/bin/import.sh';
printf("%o\n", fileperms($pfad) & 0777);
/* 644 - fuellt den Cache */
/* jetzt setzt ein Fremdprozess 0755 */
var_dump(is_executable($pfad));
/* bool(true) */
printf("%o\n", fileperms($pfad) & 0777);
/* 644 - noch der alte Stand */
clearstatcache(true, $pfad);
printf("%o\n", fileperms($pfad) & 0777);
/* 755 */
Der zweite Parameter von clearstatcache() begrenzt das Aufräumen auf einen Pfad, der erste schaltet den Realpath-Zwischenspeicher mit ab. Zu räumen gibt es bei fileperms(), filemtime(), filesize() und stat(). Die Prüffunktionen is_executable(), is_readable(), is_writable() und file_exists() laufen dagegen über access() und brauchen den Aufruf nicht. Ein Installer, der Rechte setzt und danach die gesetzten Werte protokollieren will, braucht ihn also für die Protokollzeile, nicht für die Entscheidung.
Dürfen ist nicht sollen: die Sicherheitsseite
Die Funktion beantwortet, ob die Rechte eine Ausführung zulassen. Sie beantwortet nicht, ob dieser Pfad ausgeführt werden soll. Ein Pfad aus einer Formulareingabe bleibt gefährlich, auch wenn die Prüfung ihn durchwinkt, denn ausführbar ist auf einem Server sehr vieles.
Wer ein Programm anhand einer Benutzerangabe auswählen lässt, arbeitet mit einer festen Liste erlaubter Werte und schlägt den zugehörigen Pfad darin nach. Der eingegebene Text wird nie selbst zum Pfad. Dazu kommt ein zweiter Punkt: Zwischen der Prüfung und dem Aufruf liegt ein Zeitfenster, in dem sich die Lage ändern kann. PHP is_executable() ist eine Diagnosehilfe, die verständliche Fehlermeldungen möglich macht, und kein Schutzmechanismus.
<?php
function diagnose(string $pfad): string
{
if (!file_exists($pfad)) {
return "$pfad: fehlt";
}
$modus = fileperms($pfad);
$rechte = substr(sprintf('%o', $modus), -4);
$owner = fileowner($pfad);
$x = is_executable($pfad) ? 'ja' : 'nein';
return "$pfad rechte=$rechte uid=$owner x=$x";
}
echo diagnose('/usr/local/bin/conv');
/* /usr/local/bin/conv rechte=0755 uid=0 x=ja */
Diese Zeile gehört in jede Supportantwort, bei der ein Aufruf nicht startet. Sie nennt die gesetzten Rechte, den Eigentümer und das Ergebnis der Prüfung nebeneinander, und meist ist der Widerspruch damit schon sichtbar.
Fazit
PHP is_executable() beantwortet eine schmale Frage sehr genau: Darf der Benutzer, unter dem dieses Skript gerade läuft, diesen Pfad starten? Ohne die Angabe des Benutzers ist die Aussage unvollständig, und mit ihr lösen sich die meisten Widersprüche zwischen Browser und Kommandozeile auf.
Unter Linux ergibt ein Verzeichnis wahr, weil das x-Bit dort Betreten bedeutet, weshalb is_dir() dazugehört. Unter Windows entscheidet stattdessen die Endung, und dort zählen nur .exe und .com. Der Stat-Cache wiederum trifft fileperms() und seine Nachbarn, nicht diese Prüfung: Sie geht über access() und ist nach einem chmod() sofort aktuell. Eingebaut in eine gestufte Prüfung wird aus einer leeren Rückgabe damit ein Satz, der die Ursache benennt.