Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
system() Function in Linux: Executing Shell Commands
Run Any Shell Command from C · Return Values · Security Risks · EmbeddedPathashala
system()
Beginner
3 Programs
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()
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: 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: 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: 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 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: 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: 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: 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
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
