Skip to content

Fix systemd "status=203/EXEC": Five Causes and How to Tell Them Apart

systemdtroubleshootingservicesexit-codeslinux
6 min read

Quick Answer

Main process exited, code=exited, status=203/EXEC means systemd failed at the exec step: the kernel refused to start the program in ExecStart=, so none of your code ran. systemctl status shows the same 203/EXEC for five different causes, and the journal line just above it tells them apart. Unable to locate executable ... No such file or directory means a relative or wrong path; systemd does not search your shell's PATH. Unable to locate executable ... Permission denied means no execute bit, or a noexec mount. Failed to execute ...: Exec format error means a script with no #! line. Failed to execute ...: No such file or directory on a file that exists means the #! interpreter is missing, or the file has CRLF line endings. Read it with journalctl -u UNIT -n 20. Fix the unit or the file, run systemctl daemon-reload if you edited the unit, then start it again. execstart-check.sh runs all five checks against a unit.

Paste your systemctl status output instead

The systemctl status Explainer reads systemctl status or journalctl -u output in your browser and names the cause line by line, for 203/EXEC and every other systemd failure state. Nothing is uploaded.

A new service fails with status=203/EXEC, and the program's own log is empty because it never ran. The usual advice is "use an absolute path", which fixes one of the five causes. The other four produce the same 203/EXEC in systemctl status, and the journal line that tells them apart is two lines above the one people search for.

What Does status=203/EXEC Look Like?

Reproduced on 2026-10-10 on this machine (Kali, systemd 261) with throwaway user units, so no sudo was needed; system units behave the same and log the same lines. The user name is shown as travis and the home directory as ~. A script with no #! line:

text
$ systemctl --user start bsdemo-noshebang.service $ echo $? 0 $ systemctl --user status bsdemo-noshebang.service × bsdemo-noshebang.service - bashsnippets demo noshebang Loaded: loaded (~/.config/systemd/user/bsdemo-noshebang.service; static) Active: failed (Result: exit-code) since Sat 2026-10-10 18:27:01 CDT; 1min 1s ago Duration: 22ms Invocation: af5694dc42394c61b7064df50ef538af Main PID: 33601 (code=exited, status=203/EXEC) Mem peak: 2M CPU: 5ms Oct 10 18:27:01 laptop systemd[1154]: Started bsdemo-noshebang.service - bashsnippets demo noshebang. Oct 10 18:27:01 laptop (noshebang.sh)[33601]: bsdemo-noshebang.service: Failed to execute ~/demo/svc/noshebang.sh: Exec format error Oct 10 18:27:01 laptop (noshebang.sh)[33601]: bsdemo-noshebang.service: Failed at step EXEC spawning ~/demo/svc/noshebang.sh: Exec format error Oct 10 18:27:01 laptop systemd[1154]: bsdemo-noshebang.service: Main process exited, code=exited, status=203/EXEC Oct 10 18:27:01 laptop systemd[1154]: bsdemo-noshebang.service: Failed with result 'exit-code'.

Note the first two lines: systemctl start exited 0. For Type=simple, the default, start returns once the service is forked, before the exec fails. Every simple unit on this page did the same; only a Type=oneshot unit made start print Job for ... failed. A deploy script that trusts the exit code of systemctl start reports success for a service that never ran.

Which of the Five Causes Is It?

Five units, one fault each, same status=203/EXEC in every systemctl status. The line that differs is the one from the process name in parentheses:

FaultJournal line before status=203/EXEC
ExecStart=noshebang.sh (relative path)Unable to locate executable 'noshebang.sh': No such file or directory
path to a file that does not existUnable to locate executable '~/demo/svc/nope.sh': No such file or directory
file without the execute bitUnable to locate executable '~/demo/svc/noexec.sh': Permission denied
script with no #! lineFailed to execute ~/demo/svc/noshebang.sh: Exec format error
#!/usr/bin/python9 (interpreter missing)Failed to execute ~/demo/svc/badinterp.sh: No such file or directory
#!/bin/bash saved with CRLF endingsFailed to execute ~/demo/svc/crlf.sh: No such file or directory

The last two are the confusing ones. The file exists, and the error says it does not: what is missing is the interpreter, /usr/bin/python9 or /bin/bash\r with an invisible carriage return. That is the same fault as /bin/bash^M: bad interpreter in a shell, without the helpful ^M.

A noexec mount gives the same Permission denied line as a missing execute bit; see Fix Bash "Permission denied" for how to tell those two apart.

Two neighbours fail at a different step and get a different code:

text
(true)[33638]: bsdemo-chdir.service: Changing to the requested working directory failed: No such file or directory (true)[33638]: bsdemo-chdir.service: Failed at step CHDIR spawning /bin/true: No such file or directory ... (true)[34095]: bsdemo-user217.service: Failed to determine credentials for user 'nosuchuser': Unknown user (true)[34095]: bsdemo-user217.service: Failed at step USER spawning /bin/true: Invalid argument

Those show status=200/CHDIR (WorkingDirectory= missing) and status=217/USER (User= does not exist). systemd-analyze exit-status prints the whole table on any box; on this one, systemd 261 lists 46 codes from 200 to 255.

The Script

Save as execstart-check.sh. Point it at a unit (add --user for a user unit) and it checks ExecStart=, WorkingDirectory= and User= the way the kernel and systemd will, using what systemd actually loaded rather than the file on disk.

bash
#!/bin/bash # Script: execstart-check.sh # Purpose: status=203/EXEC has at least five causes and systemctl status shows the same code for all of them; this checks a unit's ExecStart= the way the kernel will, before or after it fails. # Usage: ./execstart-check.sh [--user] UNIT (e.g. ./execstart-check.sh myapp.service) set -euo pipefail CHECK="✓" CROSS="✗" SCOPE=() if [[ "${1:-}" == "--user" ]]; then SCOPE=(--user); shift; fi [[ $# -eq 1 ]] || { echo "usage: $0 [--user] UNIT" >&2; exit 2; } UNIT="$1" PROBLEMS=0 fail() { echo "$CROSS $*"; PROBLEMS=$((PROBLEMS + 1)); } # systemctl show prints what systemd actually loaded, which is not the file on disk until daemon-reload has run. if [[ "$(systemctl "${SCOPE[@]}" show -p NeedDaemonReload --value "$UNIT")" == "yes" ]]; then fail "the unit file changed on disk but systemd still runs the old copy: systemctl ${SCOPE[*]} daemon-reload" fi LOAD=$(systemctl "${SCOPE[@]}" show -p LoadState --value "$UNIT") [[ "$LOAD" == "loaded" ]] || { fail "LoadState=$LOAD (not-found: wrong name or file not in a unit directory; masked: systemctl ${SCOPE[*]} unmask $UNIT)"; exit 1; } EXEC_PROP=$(systemctl "${SCOPE[@]}" show -p ExecStart --value "$UNIT") EXEC_PATH=$(sed -n 's/.*path=\([^ ;]*\).*/\1/p' <<< "$EXEC_PROP" | head -1) # A leading ! is the user manager's own default (home, used only if it exists) and - marks a directory optional. WORKDIR=$(systemctl "${SCOPE[@]}" show -p WorkingDirectory --value "$UNIT") [[ "$WORKDIR" == '!'* ]] && WORKDIR="" RUN_USER=$(systemctl "${SCOPE[@]}" show -p User --value "$UNIT") echo " $UNIT: ExecStart path=$EXEC_PATH${WORKDIR:+, WorkingDirectory=$WORKDIR}${RUN_USER:+, User=$RUN_USER}" [[ -n "$EXEC_PATH" ]] || { fail "no ExecStart= found (a oneshot with only ExecStartPre=, or a typo in the key)"; exit 1; } # 203/EXEC "Unable to locate executable": systemd does not search your shell's PATH for a relative name. if [[ "$EXEC_PATH" != /* ]]; then fail "ExecStart= is not an absolute path; systemd looks it up in its own fixed search path, not yours. Use the full path" elif [[ ! -e "$EXEC_PATH" ]]; then fail "$EXEC_PATH does not exist (typo, a moved venv, or a path inside a mount that is not up yet)" else OPTS=$(findmnt -no OPTIONS --target "$EXEC_PATH" 2>/dev/null || true) # 203/EXEC "Permission denied": the execute bit, or a noexec mount, which beats every mode bit. if [[ ",$OPTS," == *",noexec,"* ]]; then fail "$EXEC_PATH is on a noexec mount ($(findmnt -no TARGET --target "$EXEC_PATH")): ExecStart=/bin/bash $EXEC_PATH, or move it" elif [[ ! -x "$EXEC_PATH" ]]; then fail "$EXEC_PATH is not executable: chmod +x $EXEC_PATH" fi # Text files need a #! line; without one, execve() returns ENOEXEC, which systemd logs as "Exec format error". if head -c 4 "$EXEC_PATH" | grep -q $'^\x7fELF'; then : elif ! IFS= read -r FIRST < "$EXEC_PATH"; then fail "cannot read $EXEC_PATH as $(id -un)" elif [[ "$FIRST" != '#!'* ]]; then fail "$EXEC_PATH has no #! line (Exec format error): add #!/bin/bash as line 1" else # A CRLF line ending makes the interpreter "/bin/bash\r", which does not exist. if [[ "$FIRST" == *$'\r' ]]; then fail "the #! line ends in a carriage return (CRLF file): sed -i 's/\\r\$//' $EXEC_PATH"; fi INTERP=${FIRST#\#!}; INTERP=${INTERP#"${INTERP%%[! ]*}"}; INTERP=${INTERP%%[ $'\r']*} if [[ "$FIRST" != *$'\r' && ! -x "$INTERP" ]]; then fail "interpreter $INTERP from the #! line does not exist or is not executable"; fi fi fi # 200/CHDIR and 217/USER fail before ExecStart is even tried, so they hide behind the same "failed" state. if [[ -n "$WORKDIR" && "$WORKDIR" != -* && ! -d "$WORKDIR" ]]; then fail "WorkingDirectory=$WORKDIR does not exist (status=200/CHDIR); prefix it with - to make it optional" fi if [[ -n "$RUN_USER" ]] && ! getent passwd "$RUN_USER" >/dev/null; then fail "User=$RUN_USER does not exist (status=217/USER): create it, or use DynamicUser=yes" fi if (( PROBLEMS == 0 )); then echo "$CHECK nothing in ExecStart= would stop exec; if it still fails, the program itself exited: journalctl ${SCOPE[*]} -u $UNIT -n 20" exit 0 fi exit 1

Prerequisites

bash 4 or later, systemd (systemctl show), util-linux (findmnt) and getent. No root for user units or for reading system units, but the script must be able to read the executable to check its #! line, so run it as root or as the service's user for system services.

How Does Each Check Work?

  • NeedDaemonReload is yes when the unit file changed after systemd loaded it. Every fix you make to the file does nothing until systemctl daemon-reload, which is the most common reason a "fixed" unit fails the same way.
  • systemctl show -p ExecStart returns the parsed command, with path= holding the program systemd will run. A path that does not start with / is the relative-path cause.
  • findmnt before -x reports noexec first, because on a noexec mount even a rwx file is not executable and chmod +x would be the wrong advice.
  • The #! check skips ELF binaries, flags a text file without #! (Exec format error), then checks the interpreter. A #! line ending in \r is reported as CRLF instead of as a missing interpreter, because that is the fix you need.
  • WorkingDirectory= and User= cover 200/CHDIR and 217/USER. A leading - (optional directory) and the user manager's own ! default are skipped.

What Does the Script Print?

Against the units above, plus a working one:

text
$ ./execstart-check.sh --user bsdemo-relpath.service bsdemo-relpath.service: ExecStart path=noshebang.sh ✗ ExecStart= is not an absolute path; systemd looks it up in its own fixed search path, not yours. Use the full path exit=1 $ ./execstart-check.sh --user bsdemo-noshebang.service bsdemo-noshebang.service: ExecStart path=~/demo/svc/noshebang.sh ✗ ~/demo/svc/noshebang.sh has no #! line (Exec format error): add #!/bin/bash as line 1 exit=1 $ ./execstart-check.sh --user bsdemo-noexec.service bsdemo-noexec.service: ExecStart path=~/demo/svc/noexec.sh ✗ ~/demo/svc/noexec.sh is not executable: chmod +x ~/demo/svc/noexec.sh exit=1 $ ./execstart-check.sh --user bsdemo-missing.service bsdemo-missing.service: ExecStart path=~/demo/svc/nope.sh ✗ ~/demo/svc/nope.sh does not exist (typo, a moved venv, or a path inside a mount that is not up yet) exit=1 $ ./execstart-check.sh --user bsdemo-badinterp.service bsdemo-badinterp.service: ExecStart path=~/demo/svc/badinterp.sh ✗ interpreter /usr/bin/python9 from the #! line does not exist or is not executable exit=1 $ ./execstart-check.sh --user bsdemo-crlf.service bsdemo-crlf.service: ExecStart path=~/demo/svc/crlf.sh ✗ the #! line ends in a carriage return (CRLF file): sed -i 's/\r$//' ~/demo/svc/crlf.sh exit=1 $ ./execstart-check.sh --user bsdemo-chdir.service bsdemo-chdir.service: ExecStart path=/bin/true, WorkingDirectory=~/demo/svc/gone ✗ WorkingDirectory=~/demo/svc/gone does not exist (status=200/CHDIR); prefix it with - to make it optional exit=1 $ ./execstart-check.sh --user bsdemo-user217.service bsdemo-user217.service: ExecStart path=/bin/true, User=nosuchuser ✗ User=nosuchuser does not exist (status=217/USER): create it, or use DynamicUser=yes exit=1 $ ./execstart-check.sh --user bsdemo-good.service bsdemo-good.service: ExecStart path=~/demo/svc/good.sh ✓ nothing in ExecStart= would stop exec; if it still fails, the program itself exited: journalctl --user -u bsdemo-good.service -n 20 exit=0

Run it before systemctl enable --now on a new unit, and the first start is not the test.

Once It Starts, What If It Keeps Dying?

A program that runs and then exits non-zero is no longer 203: it is status=1/FAILURE (or its own code), and with Restart= set it can turn into Start request repeated too quickly. For keeping a service up once it works, Auto-Restart a Linux Service covers Restart=, watchdogs and alerting.

Frequently Asked Questions

What does status=203/EXEC mean in systemd?

203 is systemd's EXEC exit status: it set up the service and called execve() on the ExecStart= program, and the kernel refused. Your program never ran, so its own logs are empty. The cause is in the unit file or the executable: a relative or wrong path, a missing execute bit or noexec mount, a script without #!, or a #! interpreter that does not exist (including a CRLF line ending).

Why does my script run from the shell but fail with 203/EXEC in systemd?

The shell is more forgiving than execve(). bash runs a file with no #! line itself, and finds a relative name through your PATH. systemd does neither: it needs an absolute path in ExecStart=, an executable file, and a #! line naming an interpreter that exists. Add #!/bin/bash as line 1, chmod +x the file and use the full path.

How do I see the real error behind 203/EXEC?

Run journalctl -u UNIT -n 20 (add --user for a user unit). The line just before Main process exited names the cause: Unable to locate executable with No such file or directory or Permission denied, or Failed to execute with Exec format error or No such file or directory. systemctl status shows the same lines at the bottom when they are recent.

Why does systemctl start succeed when the service fails with 203/EXEC?

For Type=simple, the default, systemctl start returns as soon as systemd has forked the service, before the exec happens, so it exits 0 even when the program then fails with 203/EXEC. In the tests for this page every simple unit returned 0; only the Type=oneshot unit printed Job for ... failed. Check systemctl is-active or systemctl status after starting.

What is the difference between 203/EXEC and 200/CHDIR or 217/USER?

They are all systemd setup failures before your program runs, each from a different step. 200/CHDIR means WorkingDirectory= does not exist, 217/USER means User= names an account that does not exist, and 203/EXEC means the final exec failed. The journal names the step in Failed at step EXEC, CHDIR or USER, and the systemctl status Explainer decodes all of them.


Part of the bash snippets collection

Raw script, MIT licensed: scripts/systemd-status-203-exec.sh on GitHub

PAID RESOURCE — $9

The Production Bash Toolkit

An operational script system + a 30-function shared library + a 52-page field guide. The production layer the free snippets don't cover.

Get the Toolkit →
curl -O bashlib-starter.sh

Get the bashlib starter

Ten functions I source into every script on my own boxes — strict-mode setup, an ERR trap that names the failing line, lock and timeout wrappers, and cleanup that runs on every exit path. One email, no sequence.

BashSnippets logo

Written by Travis

Creator of BashSnippets.xyz

bashsnippets.xyz/about

Related Snippets

Frequently Asked Questions

faq — snippet

What does status=203/EXEC mean in systemd?

203 is systemd's EXEC exit status: it set up the service and called execve() on the ExecStart= program, and the kernel refused. Your program never ran, so its own logs are empty. The cause is in the unit file or the executable: a relative or wrong path, a missing execute bit or noexec mount, a script without #!, or a #! interpreter that does not exist (including a CRLF line ending).

faq — snippet

Why does my script run from the shell but fail with 203/EXEC in systemd?

The shell is more forgiving than execve(). bash runs a file with no #! line itself, and finds a relative name through your PATH. systemd does neither: it needs an absolute path in ExecStart=, an executable file, and a #! line naming an interpreter that exists. Add #!/bin/bash as line 1, chmod +x the file and use the full path.

faq — snippet

How do I see the real error behind 203/EXEC?

Run journalctl -u UNIT -n 20 (add --user for a user unit). The line just before Main process exited names the cause: Unable to locate executable with No such file or directory or Permission denied, or Failed to execute with Exec format error or No such file or directory. systemctl status shows the same lines at the bottom when they are recent.

faq — snippet

Why does systemctl start succeed when the service fails with 203/EXEC?

For Type=simple, the default, systemctl start returns as soon as systemd has forked the service, before the exec happens, so it exits 0 even when the program then fails with 203/EXEC. In the tests for this page every simple unit returned 0; only the Type=oneshot unit printed Job for ... failed. Check systemctl is-active or systemctl status after starting.

faq — snippet

What is the difference between 203/EXEC and 200/CHDIR or 217/USER?

They are all systemd setup failures before your program runs, each from a different step. 200/CHDIR means WorkingDirectory= does not exist, 217/USER means User= names an account that does not exist, and 203/EXEC means the final exec failed. The journal names the step in Failed at step EXEC, CHDIR or USER, and the systemctl status Explainer decodes all of them.