Skip to content

Commit d2eed94

Browse files
committed
Initial commit
1 parent 8b09741 commit d2eed94

13 files changed

+171
-1
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.so
2+
*.o
3+
*-actual

Makefile

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
libtermux-exec.so: termux-exec.c
2+
$(CC) $(CFLAGS) -Wall -Wextra -Oz termux-exec.c -shared -fPIC -o libtermux-exec.so
3+
4+
install: libtermux-exec.so
5+
install libtermux-exec.so $(PREFIX)/lib/libtermux-exec.so
6+
7+
uninstall:
8+
rm -f $(PREFIX)/lib/libtermux-exec.so
9+
10+
test: libtermux-exec.so
11+
@LD_PRELOAD=${CURDIR}/libtermux-exec.so ./run-tests.sh
12+
13+
clean:
14+
rm -f libtermux-exec.so tests/*-actual
15+
16+
.PHONY: clean install test uninstall

README.md

+23-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,24 @@
11
# termux-exec
2-
A execve() wrapper to fix problem with shebangs.
2+
A `execve()` wrapper to fix problem with shebangs when running in Termux
3+
4+
# Problem
5+
A lot of Linux software is written with the assumption that `/bin/sh`, `/usr/bin/env`
6+
and similar file exists. This is not the case on Android where neither `/bin/` or `/usr/`
7+
exists.
8+
9+
When building packages for Termux those hard-coded assumptions are patched away, but this
10+
does not help with installing scripts and programs during runtime.
11+
12+
# Solution
13+
Create an `execve()` wrapper that rewrites calls to execute files under `/bin/` and `/usr/bin`
14+
into the matching Termux executables under `$PREFIX/bin/` and injecat that into processes
15+
using `LD_PRELOAD`.
16+
17+
# How to install
18+
1. Install with `pkg install termux-exec`.
19+
2. Exit your current session and start a new one.
20+
3. From now on shebangs such as `/bin/sh` and `/usr/bin/env python` should work.
21+
22+
# Where is LD_PRELOAD set?
23+
The `$PREFIX/bin/login` program which is used to create new Termux sessions checks for
24+
`$PREFIX/lib/libtermux-exec.so` and if so sets up `LD_PRELOAD` before launching the login shell.

run-tests.sh

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/data/data/com.termux/files/usr/bin/bash
2+
3+
set -u
4+
5+
for f in tests/*.sh; do
6+
printf "Running $f..."
7+
8+
EXPECTED_FILE=$f-expected
9+
ACTUAL_FILE=$f-actual
10+
11+
rm -f $ACTUAL_FILE
12+
$f $ACTUAL_FILE > $ACTUAL_FILE
13+
14+
if cmp --silent $ACTUAL_FILE $EXPECTED_FILE; then
15+
printf " OK\n"
16+
else
17+
printf " FAILED - compare expected $EXPECTED_FILE with ${ACTUAL_FILE}\n"
18+
fi
19+
done
20+

termux-exec.c

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#include <dlfcn.h>
2+
#include <libgen.h>
3+
#include <stdio.h>
4+
#include <stdlib.h>
5+
#include <string.h>
6+
#include <unistd.h>
7+
8+
const char* termux_rewrite_executable(const char* filename, char* buffer, int buffer_len)
9+
{
10+
strcpy(buffer, "/data/data/com.termux/files/usr/bin/");
11+
char* bin_match = strstr(filename, "/bin/");
12+
if (bin_match == filename || bin_match == (filename + 4)) {
13+
// We have either found "/bin/" at the start of the string or at
14+
// "/xxx/bin/". Take the path after that.
15+
strncpy(buffer + 36, bin_match + 5, buffer_len - 37);
16+
filename = buffer;
17+
}
18+
return filename;
19+
}
20+
21+
// Examples:
22+
// [1] "#!/bin/sh" should exec "$PREFIX" without argument.
23+
// [2] "#! /bin/sh" should exec "$PREFIX" without argument.
24+
// [3] "#!/bin/sh a" should exec "$PREFIX" with single argument "a"
25+
// [4] "#! /bin/sh a " should exec "$PREFIX" with single argument "a"
26+
// [5] "#! /bin/sh a b " should exec "$PREFIX" with single argument "a b"
27+
int execve(const char* filename, char* const* argv, char *const envp[])
28+
{
29+
int fd = -1;
30+
31+
char filename_buffer[512];
32+
filename = termux_rewrite_executable(filename, filename_buffer, sizeof(filename_buffer));
33+
34+
fd = open(filename, O_RDONLY);
35+
if (fd == -1) goto final;
36+
37+
// execve(2): "A maximum line length of 127 characters is allowed
38+
// for the first line in a #! executable shell script."
39+
char shebang[128];
40+
ssize_t read_bytes = read(fd, shebang, sizeof(shebang) - 1);
41+
if (read_bytes < 5 || !(shebang[0] == '#' && shebang[1] == '!')) goto final;
42+
43+
shebang[read_bytes] = 0;
44+
char* newline_location = strchr(shebang, '\n');
45+
if (newline_location == NULL) goto final;
46+
47+
// Strip whitespace at end of shebang:
48+
while (*(newline_location - 1) == ' ') newline_location--;
49+
50+
// Null-terminate the shebang line:
51+
*newline_location = 0;
52+
53+
// Skip whitespace to find interpreter start:
54+
char* interpreter = shebang + 2;
55+
while (*interpreter == ' ') interpreter++;
56+
if (interpreter == newline_location) goto final;
57+
58+
char* arg = NULL;
59+
char* whitespace_pos = strchr(interpreter, ' ');
60+
if (whitespace_pos != NULL) {
61+
// Null-terminate the interpreter string.
62+
*whitespace_pos = 0;
63+
64+
// Find start of argument:
65+
arg = whitespace_pos + 1;;
66+
while (*arg != 0 && *arg == ' ') arg++;
67+
if (arg == newline_location) {
68+
// Only whitespace after interpreter.
69+
arg = NULL;
70+
}
71+
}
72+
73+
const char* new_argv[4];
74+
new_argv[0] = basename(interpreter);
75+
76+
char interp_buf[512];
77+
const char* new_interpreter = termux_rewrite_executable(interpreter, interp_buf, sizeof(interp_buf));
78+
79+
if (arg) {
80+
new_argv[1] = arg;
81+
new_argv[2] = filename;
82+
new_argv[3] = NULL;
83+
} else {
84+
new_argv[1] = filename;
85+
new_argv[2] = NULL;
86+
}
87+
88+
filename = new_interpreter;
89+
argv = (char**) new_argv;
90+
91+
final:
92+
if (fd != -1) close(fd);
93+
int (*real_execve)(const char*, char *const[], char *const[]) = dlsym(RTLD_NEXT, "execve");
94+
return real_execve(filename, argv, envp);
95+
}

tests/args-with-spaces.sh

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
#!/bin/echo hello world bye

tests/args-with-spaces.sh-expected

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hello world bye tests/args-with-spaces.sh

tests/initial-whitespace.sh

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# !/bin/sh
2+
3+
echo hi2

tests/initial-whitespace.sh-expected

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hi2

tests/simple.sh

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/sh
2+
3+
echo hi

tests/simple.sh-expected

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hi

tests/usr-bin-env.sh

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env sh
2+
3+
echo hello-user-bin-env-sh

tests/usr-bin-env.sh-expected

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hello-user-bin-env-sh

0 commit comments

Comments
 (0)