Chapter 27.6 — Executing a Shell Command: system()
Run Any Shell Command from C · Return Values · Security Risks · EmbeddedPathashala
📌 Topic
system()
system()
🧠 Level
Beginner
Beginner
💻 Examples
3 Programs
3 Programs
❓ Q&A
8 Questions
8 Questions
What is system()?
The system() function lets you run a shell command from inside a C program — just like typing it in a terminal. Internally it uses fork() + exec("/bin/sh -c ...") + waitpid() so you don’t have to write that yourself.
It’s the easiest way to run a shell command, but also the least efficient and least secure. Use it for quick tasks; avoid it in performance-critical or setUID programs.
Function Signature
#include <stdlib.h>
int system(const char *command);
/* Returns: see table below */
What system() Returns
| Return Value | Meaning | Example |
|---|---|---|
| Non-zero (command=NULL) | A shell is available | system(NULL) → 1 |
| 0 (command=NULL) | No shell available | On minimal/embedded systems |
| -1 | fork() or waitpid() failed | System resource limit hit |
| Wait status with exit code 127 | Shell couldn’t exec the command | Command not found in PATH |
| Normal wait status | Shell ran command, returned its exit status | Use WIFEXITED/WEXITSTATUS to decode |
How system() Works Internally
| Step | What Happens |
|---|---|
| 1 | fork() — create child process |
| 2 | Child: execle(“/bin/sh”, “sh”, “-c”, command, NULL, environ) |
| 3 | Shell parses command, handles pipes/redirects/substitution |
| 4 | Parent: waitpid(child_pid) — waits specifically for this child |
| 5 | Return child’s exit status to caller |
system() — Advantages vs Disadvantages
✅ Advantages
- Simple one-line call
- Full shell power: pipes, redirects, glob, substitution
- No need to handle fork/exec/wait yourself
- Error handling built in
❌ Disadvantages
- Slow: creates 2+ processes (sh + command)
- Dangerous in setUID programs
- Shell env vars can be exploited (IFS, PATH, etc.)
- Can’t distinguish “shell not found” from exit code 127
Example 1: Basic system() Usage
/* example1_system_basic.c
* gcc -o ex1 example1_system_basic.c && ./ex1
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
void run_command(const char *cmd)
{
int ret;
printf("--- Running: %s ---\n", cmd);
ret = system(cmd);
if (ret == -1) {
perror("system() failed");
return;
}
if (WIFEXITED(ret)) {
printf("Exit status: %d\n\n", WEXITSTATUS(ret));
} else if (WIFSIGNALED(ret)) {
printf("Killed by signal: %d\n\n", WTERMSIG(ret));
}
}
int main(void)
{
/* Check if shell is available */
if (system(NULL) == 0) {
printf("No shell available!\n");
return 1;
}
printf("Shell is available.\n\n");
/* Run some shell commands */
run_command("echo Hello from system()!");
run_command("ls /tmp | head -5");
run_command("date '+%Y-%m-%d %H:%M:%S'");
run_command("nonexistent_command_xyz 2>/dev/null"); /* exits 127 */
return 0;
}
/* Expected output:
* Shell is available.
*
* --- Running: echo Hello from system()! ---
* Hello from system()!
* Exit status: 0
*
* --- Running: ls /tmp | head -5 ---
* (5 files from /tmp)
* Exit status: 0
*
* --- Running: date '+%Y-%m-%d %H:%M:%S' ---
* 2025-06-05 10:45:00
* Exit status: 0
*
* --- Running: nonexistent_command_xyz ---
* Exit status: 127 (command not found)
*/
Example 2: Properly Decoding system() Return Value
/* example2_system_retval.c
* gcc -o ex2 example2_system_retval.c && ./ex2
*
* Shows all the different return value scenarios from system()
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string.h>
void analyze_result(const char *cmd, int status)
{
printf("Command: [%s]\n", cmd);
printf("Raw return: 0x%04x\n", (unsigned int)status);
if (status == -1) {
printf("Result: system() FAILED (fork/waitpid error)\n");
} else if (WIFEXITED(status)) {
int code = WEXITSTATUS(status);
if (code == 127)
printf("Result: Exited 127 — shell couldn't exec command\n");
else
printf("Result: Exited normally, code = %d %s\n",
code, code == 0 ? "(SUCCESS)" : "(FAILURE)");
} else if (WIFSIGNALED(status)) {
printf("Result: Killed by signal %d (%s)\n",
WTERMSIG(status), strsignal(WTERMSIG(status)));
} else if (WIFSTOPPED(status)) {
printf("Result: Stopped by signal %d\n", WSTOPSIG(status));
}
printf("\n");
}
int main(void)
{
int ret;
/* Success (exit 0) */
ret = system("true");
analyze_result("true", ret);
/* Failure (exit 1) */
ret = system("false");
analyze_result("false", ret);
/* Command not found (exit 127) */
ret = system("this_cmd_does_not_exist 2>/dev/null");
analyze_result("this_cmd_does_not_exist", ret);
/* Non-zero exit: grep returns 1 if no match */
ret = system("echo hello | grep xyz > /dev/null");
analyze_result("echo hello | grep xyz", ret);
/* Shell features work! Pipes, redirection */
ret = system("ls /etc | wc -l");
analyze_result("ls /etc | wc -l", ret);
return 0;
}
Example 3: Why system() is Dangerous — Shell Injection
/* example3_system_security.c
* gcc -o ex3 example3_system_security.c && ./ex3
*
* DEMONSTRATES: Shell injection attack via system()
* If user input is passed to system() without sanitization,
* an attacker can run arbitrary commands!
*
* NEVER pass user-controlled data to system() unfiltered!
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* BAD: vulnerable to shell injection */
void dangerous_system(const char *user_filename)
{
char cmd[512];
/* If user_filename = "innocent.txt; rm -rf /tmp/test"
* The shell sees: cat innocent.txt; rm -rf /tmp/test
* Two commands! Both run! */
snprintf(cmd, sizeof(cmd), "cat %s", user_filename);
printf("Running: %s\n", cmd);
system(cmd);
}
/* GOOD: use execv family instead — no shell involvement */
void safe_exec(const char *filename)
{
pid_t child;
int status;
char *argv[] = { "cat", (char *)filename, NULL };
printf("Safe exec: cat %s\n", filename);
child = fork();
if (child == 0) {
execvp("cat", argv);
perror("execvp"); exit(1);
}
if (child > 0) waitpid(child, &status, 0);
}
int main(void)
{
/* Legitimate use */
printf("=== Normal filename ===\n");
dangerous_system("/etc/hostname");
printf("\n=== Shell injection attack ===\n");
printf("Attacker provides: /etc/hostname; echo INJECTED\n\n");
dangerous_system("/etc/hostname; echo '!!! SHELL INJECTION WORKED !!!'");
printf("\n=== Safe version (no shell, no injection possible) ===\n");
safe_exec("/etc/hostname");
/* Even with semicolons in filename, execvp passes it as literal arg */
/* No shell = no injection */
return 0;
}
/* Lesson: In security-sensitive code:
* - NEVER use system() with any user-controlled input
* - ALWAYS use fork() + execve()/execvp() directly
* - NEVER use execlp/execvp with untrusted PATH
*/
❓ Interview Questions — system()
Q1. How does system() work internally?
Answer: It calls fork() to create a child, then the child calls execl(“/bin/sh”, “sh”, “-c”, command, NULL) to run the command via the shell. The parent blocks in waitpid(child_pid) until the child finishes. The return value is the shell’s wait status.
Answer: It calls fork() to create a child, then the child calls execl(“/bin/sh”, “sh”, “-c”, command, NULL) to run the command via the shell. The parent blocks in waitpid(child_pid) until the child finishes. The return value is the shell’s wait status.
Q2. Why does system() create at least two processes?
Answer: One process for /bin/sh (the shell), and at least one more for the command the shell runs. If the command is a pipeline like “ls | wc”, there are even more. This makes system() slower than direct fork+exec.
Answer: One process for /bin/sh (the shell), and at least one more for the command the shell runs. If the command is a pipeline like “ls | wc”, there are even more. This makes system() slower than direct fork+exec.
Q3. What does system(NULL) do?
Answer: It checks whether a shell is available. Returns non-zero if /bin/sh exists and can be exec’d, 0 if not. Useful on embedded systems where the shell may not be present.
Answer: It checks whether a shell is available. Returns non-zero if /bin/sh exists and can be exec’d, 0 if not. Useful on embedded systems where the shell may not be present.
Q4. system() returns a value where WEXITSTATUS() gives 127. What does this mean?
Answer: The shell ran, but could not find or execute the command — typically “command not found.” However, you cannot distinguish this from a command that intentionally called exit(127). This ambiguity is a known limitation of system().
Answer: The shell ran, but could not find or execute the command — typically “command not found.” However, you cannot distinguish this from a command that intentionally called exit(127). This ambiguity is a known limitation of system().
Q5. Why should system() never be used in setUID programs?
Answer: The shell that system() invokes inherits and uses environment variables like PATH, IFS, LD_PRELOAD, etc. A malicious user can manipulate these to trick the shell into running a different program or changing behavior — all with the elevated privileges of the setUID program.
Answer: The shell that system() invokes inherits and uses environment variables like PATH, IFS, LD_PRELOAD, etc. A malicious user can manipulate these to trick the shell into running a different program or changing behavior — all with the elevated privileges of the setUID program.
Q6. What is shell injection and how does it affect system()?
Answer: Shell injection is when user-controlled data containing shell metacharacters (;, &, |, $(), etc.) is passed to system() unfiltered. The shell interprets these as command separators and runs attacker-supplied commands. Always use fork()+execve() when dealing with user-supplied arguments — no shell means no injection.
Answer: Shell injection is when user-controlled data containing shell metacharacters (;, &, |, $(), etc.) is passed to system() unfiltered. The shell interprets these as command separators and runs attacker-supplied commands. Always use fork()+execve() when dealing with user-supplied arguments — no shell means no injection.
Q7. What is the difference between system() and popen()?
Answer: Both run shell commands via /bin/sh. The key difference: system() waits for the command to finish and returns its exit status. popen() returns a FILE* pipe, letting you read the command’s output or write to its input. popen() is better when you need the command’s output in your program.
Answer: Both run shell commands via /bin/sh. The key difference: system() waits for the command to finish and returns its exit status. popen() returns a FILE* pipe, letting you read the command’s output or write to its input. popen() is better when you need the command’s output in your program.
Q8. When should you use system() vs fork()+exec()?
Answer: Use system() for: simple scripts in non-sensitive contexts, shell pipeline commands (ls | grep), quick prototyping. Use fork()+exec() for: security-sensitive code, performance-critical applications, setUID programs, when you need precise control over the child’s environment/signals/fds, or when you want to capture stdout/stderr.
Answer: Use system() for: simple scripts in non-sensitive contexts, shell pipeline commands (ls | grep), quick prototyping. Use fork()+exec() for: security-sensitive code, performance-critical applications, setUID programs, when you need precise control over the child’s environment/signals/fds, or when you want to capture stdout/stderr.
Next: Implementing system() from Scratch
How to write your own system() with proper signal handling
