这是indexloc提供的服务,不要输入任何密码
Skip to content

Commit ecad16f

Browse files
committed
Avoid executing ELF files directly
See the README file for a description.
1 parent f3ae554 commit ecad16f

File tree

8 files changed

+930
-224
lines changed

8 files changed

+930
-224
lines changed

.clang-tidy

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Checks: '-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,-clang-analyzer-security.insecureAPI.strcpy,-clang-analyzer-valist.Uninitialized'
2+

.github/workflows/ci.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- '*'
7+
pull_request:
8+
9+
jobs:
10+
check:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: Homebrew/actions/setup-homebrew@master
15+
- run: brew install clang-format
16+
- run: make
17+
- run: make check
18+
- run: make unit-test
19+
20+
actionlint:
21+
runs-on: ubuntu-latest
22+
steps:
23+
- uses: actions/checkout@v4
24+
- name: Download actionlint
25+
id: get_actionlint
26+
run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
27+
- name: Check workflow files
28+
run: ${{ steps.get_actionlint.outputs.executable }} -color

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
*.so
22
*.o
33
*-actual
4+
*.swo
5+
*.swp
6+
test-binary

Makefile

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,35 @@
1-
TERMUX_PREFIX := /data/data/com.termux/files/usr
2-
TERMUX_BASE_DIR := /data/data/com.termux/files
3-
CFLAGS += -Wall -Wextra -Werror -Oz
1+
TERMUX_BASE_DIR ?= /data/data/com.termux/files
2+
CFLAGS += -Wall -Wextra -Werror -Wshadow -O2
3+
C_SOURCE := termux-exec.c exec-variants.c
4+
CLANG_FORMAT := clang-format --sort-includes --style="{ColumnLimit: 120}" $(C_SOURCE)
5+
CLANG_TIDY ?= clang-tidy
46

5-
libtermux-exec.so: termux-exec.c
6-
$(CC) $(CFLAGS) $(LDFLAGS) termux-exec.c -DTERMUX_PREFIX=\"$(TERMUX_PREFIX)\" -DTERMUX_BASE_DIR=\"$(TERMUX_BASE_DIR)\" -shared -fPIC -o libtermux-exec.so
7+
libtermux-exec.so: $(C_SOURCE)
8+
$(CC) $(CFLAGS) $(LDFLAGS) $(C_SOURCE) -DTERMUX_PREFIX=\"$(TERMUX_PREFIX)\" -DTERMUX_BASE_DIR=\"$(TERMUX_BASE_DIR)\" -shared -fPIC -o libtermux-exec.so
9+
10+
clean:
11+
rm -f libtermux-exec.so tests/*-actual test-binary
712

813
install: libtermux-exec.so
914
install libtermux-exec.so $(DESTDIR)$(PREFIX)/lib/libtermux-exec.so
1015

1116
uninstall:
1217
rm -f $(DESTDIR)$(PREFIX)/lib/libtermux-exec.so
1318

14-
test: libtermux-exec.so
19+
on-device-tests: libtermux-exec.so
1520
@LD_PRELOAD=${CURDIR}/libtermux-exec.so ./run-tests.sh
1621

17-
clean:
18-
rm -f libtermux-exec.so tests/*-actual
22+
format:
23+
$(CLANG_FORMAT) -i $(C_SOURCE)
24+
25+
check:
26+
$(CLANG_FORMAT) --dry-run $(C_SOURCE)
27+
$(CLANG_TIDY) -warnings-as-errors='*' $(C_SOURCE) -- -DTERMUX_BASE_DIR=\"$(TERMUX_BASE_DIR)\"
28+
29+
test-binary: $(C_SOURCE)
30+
$(CC) $(CFLAGS) $(LDFLAGS) $(C_SOURCE) -g -fsanitize=address -fno-omit-frame-pointer -DUNIT_TEST=1 -DTERMUX_BASE_DIR=\"$(TERMUX_BASE_DIR)\" -o test-binary
31+
32+
unit-test: test-binary
33+
./test-binary
1934

20-
.PHONY: clean install test uninstall
35+
.PHONY: clean install uninstall test format check-format test

README.md

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,81 @@
11
# termux-exec
2-
A `execve()` wrapper to fix problem with shebangs when running in Termux.
2+
A `execve()` wrapper to fix two problems with exec-ing files in Termux.
33

4-
# Problem
4+
# Problem 1: Cannot execute files not part of the APK
5+
Android 10 started blocking executing files under the app data directory, as
6+
that is a [W^X](https://en.wikipedia.org/wiki/W%5EX) violation - files should be either
7+
writeable or executable, but not both. Resources:
8+
9+
- [Google Android issue](https://issuetracker.google.com/issues/128554619)
10+
- [Termux: No more exec from data folder on targetAPI >= Android Q](https://github.com/termux/termux-app/issues/1072)
11+
- [Termux: Revisit the Android W^X problem](https://github.com/termux/termux-app/issues/2155)
12+
13+
While there is merit in that general principle, this prevents using Termux and Android
14+
as a general computing device, where it should be possible for users to create executable
15+
scripts and binaries.
16+
17+
# Solution 1: Cannot execute files not part of the APK
18+
Create an `exec` interceptor using [LD_PRELOAD](https://en.wikipedia.org/wiki/DLL_injection#Approaches_on_Unix-like_systems),
19+
that instead of executing an ELF file directly, executes `/system/bin/linker64 /path/to/elf`.
20+
Explanation follows below.
21+
22+
On Linux, the kernel is normally responsible for loading both the executable and the
23+
[dynamic linker](https://en.wikipedia.org/wiki/Dynamic_linker). The executable is invoked
24+
by file path with the [execve system call](https://en.wikipedia.org/wiki/Exec_(system_call)).
25+
The kernel loads the executable into the process, and looks for a `PT_INTERP` entry in
26+
its [ELF program header table](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#Program_header)
27+
of the file - this specifies the path to the dynamic linker (`/system/bin/linker64` for 64-bit Android).
28+
29+
There is another way to load the two ELF objects:
30+
[since 2018](https://android.googlesource.com/platform/bionic/+/8f639a40966c630c64166d2657da3ee641303194)
31+
the dynamic linker can be invoked directly with `exec`.
32+
If passed the filename of an executable, the dynamic linker will load and run the executable itself.
33+
So, instead of executing `path/to/mybinary`, it's possible to execute
34+
`/system/bin/linker64 /absolute/path/to/mybinary` (the linker needs an absolute path).
35+
36+
This is what `termux-exec` does to circumvent the block on executing files in the data
37+
directory - the kernel sees only `/system/bin/linker64` being executed.
38+
39+
This also means that we need to extract [shebangs](https://en.wikipedia.org/wiki/Shebang_(Unix)). So for example, a call to execute:
40+
41+
```sh
42+
./path/to/myscript.sh <args>
43+
```
44+
45+
where the script has a `#!/path/to/interpreter` shebang, is replaced with:
46+
47+
```sh
48+
/system/bin/linker64 /path/to/interpreter ./path/to/myscript.sh <args>
49+
```
50+
51+
Implications:
52+
53+
- It's important that `LD_PRELOAD` is kept - see e.g. [this change in sshd](https://github.com/termux/termux-packages/pull/18069).
54+
We could also consider patching this exec interception into the build process of termux packages, so `LD_PRELOAD` would not be necessary for packages built by the termux-packages repository.
55+
56+
- The executable will be `/system/bin/linker64`. So some programs that inspects the executable name (on itself or other programs) using `/proc/${PID}/exec` or `/proc/${PID}/comm` (where `$(PID}` could be `self`, for the current process) needs to be changed to instead inspect `argv0` of the process instead. See [this llvm driver change](https://github.com/termux/termux-packages/pull/18074) and [this pgrep/pkill change](https://github.com/termux/termux-packages/pull/18075).
57+
58+
- Statically linked binaries will not work. These are rare in Android and Termux, but zig currently produces statically linked binaries against musl libc.
59+
60+
- The interception using `LD_PRELOAD` will only work for programs using the [C library wrappers](https://linux.die.net/man/3/execve) for executing a new process, not when using the `execve` system call directly. Luckily most programs do use this. Programs using raw system calls needs to be patched or be run under [proot](https://wiki.termux.com/wiki/PRoot).
61+
62+
**NOTE**: The above example used `/system/bin/linker64` - on 32-bit systems, the corresponding
63+
path is `/system/bin/linker`.
64+
65+
**NOTE**: While this circumvents the technical restriction, it still might be considered
66+
violating [Google Play policy](https://support.google.com/googleplay/android-developer/answer/9888379).
67+
So this workaround is not guaranteed to enable Play store distribution of Termux - but it's
68+
worth an attempt, and regardless of Play store distribution, updating the targetSdk is necessary.
69+
70+
# Problem 2: Shebang paths
571
A lot of Linux software is written with the assumption that `/bin/sh`, `/usr/bin/env`
672
and similar file exists. This is not the case on Android where neither `/bin/` nor `/usr/`
773
exists.
874

975
When building packages for Termux those hard-coded assumptions are patched away - but this
1076
does not help with installing scripts and programs from other sources than Termux packages.
1177

12-
# Solution
78+
# Solution 2: Shebang paths
1379
Create an `execve()` wrapper that rewrites calls to execute files under `/bin/` and `/usr/bin`
1480
into the matching Termux executables under `$PREFIX/bin/` and inject that into processes
1581
using `LD_PRELOAD`.
@@ -22,3 +88,6 @@ using `LD_PRELOAD`.
2288
# Where is LD_PRELOAD set?
2389
The `$PREFIX/bin/login` program which is used to create new Termux sessions checks for
2490
`$PREFIX/lib/libtermux-exec.so` and if so sets up `LD_PRELOAD` before launching the login shell.
91+
92+
Soon, when making a switch to target Android 10+, this will be setup by the Termux app even before
93+
launching any process, as `LD_PRELOAD` will be necessary for anything non-system to execute.

exec-variants.c

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// These exec variants, which ends up calling execve(), comes from bionic:
2+
// https://android.googlesource.com/platform/bionic/+/refs/heads/main/libc/bionic/exec.cpp
3+
//
4+
// For some reason these are only necessary starting with Android 14 - before
5+
// that intercepting execve() is enough.
6+
//
7+
// See the test-program.c for how to test the different variants.
8+
9+
#define _GNU_SOURCE
10+
#include <errno.h>
11+
#include <paths.h>
12+
#include <stdarg.h>
13+
#include <stdbool.h>
14+
#include <stdio.h>
15+
#include <stdlib.h>
16+
#include <string.h>
17+
#include <unistd.h>
18+
19+
enum { ExecL, ExecLE, ExecLP };
20+
21+
static int __exec_as_script(const char *buf, char *const *argv, char *const *envp) {
22+
size_t arg_count = 1;
23+
while (argv[arg_count] != NULL)
24+
++arg_count;
25+
26+
const char *script_argv[arg_count + 2];
27+
script_argv[0] = "sh";
28+
script_argv[1] = buf;
29+
memcpy(script_argv + 2, argv + 1, arg_count * sizeof(char *));
30+
return execve(_PATH_BSHELL, (char **const)script_argv, envp);
31+
}
32+
33+
int execv(const char *name, char *const *argv) { return execve(name, argv, environ); }
34+
35+
int execvp(const char *name, char *const *argv) { return execvpe(name, argv, environ); }
36+
37+
int execvpe(const char *name, char *const *argv, char *const *envp) {
38+
// if (name == NULL || *name == '\0') { errno = ENOENT; return -1; }
39+
40+
// If it's an absolute or relative path name, it's easy.
41+
if (strchr(name, '/') && execve(name, argv, envp) == -1) {
42+
if (errno == ENOEXEC)
43+
return __exec_as_script(name, argv, envp);
44+
return -1;
45+
}
46+
47+
// Get the path we're searching.
48+
const char *path = getenv("PATH");
49+
if (path == NULL)
50+
path = _PATH_DEFPATH;
51+
52+
// Make a writable copy.
53+
size_t len = strlen(path) + 1;
54+
char writable_path[len];
55+
memcpy(writable_path, path, len);
56+
57+
bool saw_EACCES = false;
58+
59+
// Try each element of $PATH in turn...
60+
char *strsep_buf = writable_path;
61+
const char *dir;
62+
while ((dir = strsep(&strsep_buf, ":"))) {
63+
// It's a shell path: double, leading and trailing colons
64+
// mean the current directory.
65+
if (*dir == '\0')
66+
dir = ".";
67+
68+
size_t dir_len = strlen(dir);
69+
size_t name_len = strlen(name);
70+
71+
char buf[dir_len + 1 + name_len + 1];
72+
mempcpy(mempcpy(mempcpy(buf, dir, dir_len), "/", 1), name, name_len + 1);
73+
74+
execve(buf, argv, envp);
75+
switch (errno) {
76+
case EISDIR:
77+
case ELOOP:
78+
case ENAMETOOLONG:
79+
case ENOENT:
80+
case ENOTDIR:
81+
break;
82+
case ENOEXEC:
83+
return __exec_as_script(buf, argv, envp);
84+
return -1;
85+
case EACCES:
86+
saw_EACCES = true;
87+
break;
88+
default:
89+
return -1;
90+
}
91+
}
92+
if (saw_EACCES)
93+
errno = EACCES;
94+
return -1;
95+
}
96+
97+
static int __execl(int variant, const char *name, const char *argv0, va_list ap) {
98+
// Count the arguments.
99+
va_list count_ap;
100+
va_copy(count_ap, ap);
101+
size_t n = 1;
102+
while (va_arg(count_ap, char *) != NULL) {
103+
++n;
104+
}
105+
va_end(count_ap);
106+
107+
// Construct the new argv.
108+
char *argv[n + 1];
109+
argv[0] = (char *)argv0;
110+
n = 1;
111+
while ((argv[n] = va_arg(ap, char *)) != NULL) {
112+
++n;
113+
}
114+
115+
// Collect the argp too.
116+
char **argp = (variant == ExecLE) ? va_arg(ap, char **) : environ;
117+
118+
va_end(ap);
119+
120+
return (variant == ExecLP) ? execvp(name, argv) : execve(name, argv, argp);
121+
}
122+
123+
int execl(const char *name, const char *arg, ...) {
124+
va_list ap;
125+
va_start(ap, arg);
126+
int result = __execl(ExecL, name, arg, ap);
127+
va_end(ap);
128+
return result;
129+
}
130+
131+
int execle(const char *name, const char *arg, ...) {
132+
va_list ap;
133+
va_start(ap, arg);
134+
int result = __execl(ExecLE, name, arg, ap);
135+
va_end(ap);
136+
return result;
137+
}
138+
139+
int execlp(const char *name, const char *arg, ...) {
140+
va_list ap;
141+
va_start(ap, arg);
142+
int result = __execl(ExecLP, name, arg, ap);
143+
va_end(ap);
144+
return result;
145+
}
146+
147+
int fexecve(int fd, char *const *argv, char *const *envp) {
148+
char buf[40];
149+
snprintf(buf, sizeof(buf), "/proc/self/fd/%d", fd);
150+
execve(buf, argv, envp);
151+
if (errno == ENOENT)
152+
errno = EBADF;
153+
return -1;
154+
}

0 commit comments

Comments
 (0)