Skip to content

Commit

Permalink
Avoid executing ELF files directly
Browse files Browse the repository at this point in the history
  • Loading branch information
fornwall committed Oct 2, 2023
1 parent f3ae554 commit fa947f7
Show file tree
Hide file tree
Showing 8 changed files with 844 additions and 221 deletions.
2 changes: 2 additions & 0 deletions .clang-tidy
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Checks: '-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,-clang-analyzer-security.insecureAPI.strcpy,-clang-analyzer-valist.Uninitialized'

28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: CI

on:
push:
branches:
- '*'
pull_request:

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Homebrew/actions/setup-homebrew@master
- run: brew install clang-format
- run: make
- run: make check
- run: make unit-test

actionlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download actionlint
id: get_actionlint
run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
- name: Check workflow files
run: ${{ steps.get_actionlint.outputs.executable }} -color
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
*.so
*.o
*-actual
*.swo
*.swp
test-binary
31 changes: 23 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,20 +1,35 @@
TERMUX_PREFIX := /data/data/com.termux/files/usr
TERMUX_BASE_DIR := /data/data/com.termux/files
CFLAGS += -Wall -Wextra -Werror -Oz
CFLAGS += -Wall -Wextra -Werror -Wshadow -O2
C_SOURCE := termux-exec.c exec-variants.c
CLANG_FORMAT := clang-format --sort-includes --style="{ColumnLimit: 120}" $(C_SOURCE)
CLANG_TIDY := clang-tidy

libtermux-exec.so: termux-exec.c
$(CC) $(CFLAGS) $(LDFLAGS) termux-exec.c -DTERMUX_PREFIX=\"$(TERMUX_PREFIX)\" -DTERMUX_BASE_DIR=\"$(TERMUX_BASE_DIR)\" -shared -fPIC -o libtermux-exec.so
libtermux-exec.so: $(C_SOURCE)
$(CC) $(CFLAGS) $(LDFLAGS) $(C_SOURCE) -DTERMUX_PREFIX=\"$(TERMUX_PREFIX)\" -DTERMUX_BASE_DIR=\"$(TERMUX_BASE_DIR)\" -shared -fPIC -o libtermux-exec.so

clean:
rm -f libtermux-exec.so tests/*-actual test-binary

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

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

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

clean:
rm -f libtermux-exec.so tests/*-actual
format:
$(CLANG_FORMAT) -i $(C_SOURCE)

check:
$(CLANG_FORMAT) --dry-run $(C_SOURCE)
$(CLANG_TIDY) -warnings-as-errors='*' $(C_SOURCE) --

test-binary: $(C_SOURCE)
$(CC) $(CFLAGS) $(LDFLAGS) $(C_SOURCE) -g -fsanitize=address -fno-omit-frame-pointer -DUNIT_TEST=1 -o test-binary

unit-test: test-binary
./test-binary

.PHONY: clean install test uninstall
.PHONY: clean install uninstall test format check-format test
68 changes: 65 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,74 @@
# termux-exec
A `execve()` wrapper to fix problem with shebangs when running in Termux.
A `execve()` wrapper to fix two problems with exec-ing files in Termux.

# Problem
# Problem 1: Cannot execute files not part of the APK
Android 10 started blocking executing files under the app data directory, as
that is a [W^X](https://en.wikipedia.org/wiki/W%5EX) violation - files should be either
writeable or executable, but not both. Resources:

- [Google Android issue](https://issuetracker.google.com/issues/128554619)
- [Termux: No more exec from data folder on targetAPI >= Android Q](https://github.com/termux/termux-app/issues/1072)
- [Termux: Revisit the Android W^X problem](https://github.com/termux/termux-app/issues/2155)

While there is merit in that general principle, this prevents using Termux and Android
as a general computing device, where it should be possible for users to create executable
scripts and binaries.

# Solution 1: Cannot execute files not part of the APK
Create an `execve()` wrapper that instead of exec-ing an ELF file directly, executes
`/system/bin/linker64 /path/to/elf`. Explanation follows below.

On Linux, the kernel is normally responsible for loading both the executable and the
[dynamic linker](https://en.wikipedia.org/wiki/Dynamic_linker). The executable is invoked
by filename with `execve()`. The kernel loads the executable into the process, and looks
for a `PT_INTERP` entry in its ELF Program Headers; this specifies the filename of the
dynamic linker (`/system/bin/linker64` for 64-bit Android). This entry exists for
dynamically linked executables.

There is another way to load the two ELF objects: the dynamic linker can be invoked directly
with `execve()`. If passed the filename of an executable, the dynamic linker will load the
executable itself. So, instead of executing `path/to/mybinary`, it's possible to execute
`/system/bin/linker64 /absolute/path/to/mybinary` (the linker needs an absolute path).

This is what `termux-exec` does to circumvent the block on executing files in the data
directory - the kernel sees only `/system/bin/linker64` being executed.

This also means that we need to extract shebangs. So for example, a call to execute:

```sh
./path/to/myscript.sh <args>
```

where the script has a `#!/path/to/interpreter` shebang, is replaced with:

```sh
/system/bin/linker64 /path/to/interpreter ./path/to/myscript.sh <args>
```

Implications:

- It's important that `LD_PRELOAD` is kept - see e.g. [this change in sshd](https://github.com/termux/termux-packages/pull/18069).
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.

- The executable will be `/system/bin/linker64`. So some programs that inspects the executable name (on itself or other programs) needs to be changed. 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).

**NOTE**: The above example used `/system/bin/linker64` - on 32-bit systems, the corresponding
path is `/system/bin/linker`.

**NOTE**: While this circumvents the technical restriction, it still might be considered
violating [Google Play policy](https://support.google.com/googleplay/android-developer/answer/9888379).
So this workaround is not guaranteed to enable Play store distribution of Termux - but it's
worth an attempt, and regardless of Play store distribution, updating the targetSdk is necessary.

# Problem 2: Shebang paths
A lot of Linux software is written with the assumption that `/bin/sh`, `/usr/bin/env`
and similar file exists. This is not the case on Android where neither `/bin/` nor `/usr/`
exists.

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

# Solution
# Solution 2: Shebang paths
Create an `execve()` wrapper that rewrites calls to execute files under `/bin/` and `/usr/bin`
into the matching Termux executables under `$PREFIX/bin/` and inject that into processes
using `LD_PRELOAD`.
Expand All @@ -22,3 +81,6 @@ using `LD_PRELOAD`.
# Where is LD_PRELOAD set?
The `$PREFIX/bin/login` program which is used to create new Termux sessions checks for
`$PREFIX/lib/libtermux-exec.so` and if so sets up `LD_PRELOAD` before launching the login shell.

Soon, when making a switch to target Android 10+, this will be setup by the Termux app even before
launching any process, as `LD_PRELOAD` will be necessary for anything non-system to execute.
154 changes: 154 additions & 0 deletions exec-variants.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// These exec variants, which ends up calling execve(), comes from bionic:
// https://android.googlesource.com/platform/bionic/+/refs/heads/main/libc/bionic/exec.cpp
//
// For some reason these are only necessary starting with Android 14 - before
// that intercepting execve() is enough.
//
// See the test-program.c for how to test the different variants.

#define _GNU_SOURCE
#include <errno.h>
#include <paths.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

enum { ExecL, ExecLE, ExecLP };

static int __exec_as_script(const char *buf, char *const *argv, char *const *envp) {
size_t arg_count = 1;
while (argv[arg_count] != NULL)
++arg_count;

const char *script_argv[arg_count + 2];
script_argv[0] = "sh";
script_argv[1] = buf;
memcpy(script_argv + 2, argv + 1, arg_count * sizeof(char *));
return execve("/data/data/com.termux/files/usr/bin/sh", (char **const)script_argv, envp);
}

int execv(const char *name, char *const *argv) { return execve(name, argv, environ); }

int execvp(const char *name, char *const *argv) { return execvpe(name, argv, environ); }

int execvpe(const char *name, char *const *argv, char *const *envp) {
// if (name == NULL || *name == '\0') { errno = ENOENT; return -1; }

// If it's an absolute or relative path name, it's easy.
if (strchr(name, '/') && execve(name, argv, envp) == -1) {
if (errno == ENOEXEC)
return __exec_as_script(name, argv, envp);
return -1;
}

// Get the path we're searching.
const char *path = getenv("PATH");
if (path == NULL)
path = _PATH_DEFPATH;

// Make a writable copy.
size_t len = strlen(path) + 1;
char writable_path[len];
memcpy(writable_path, path, len);

bool saw_EACCES = false;

// Try each element of $PATH in turn...
char *strsep_buf = writable_path;
const char *dir;
while ((dir = strsep(&strsep_buf, ":"))) {
// It's a shell path: double, leading and trailing colons
// mean the current directory.
if (*dir == '\0')
dir = ".";

size_t dir_len = strlen(dir);
size_t name_len = strlen(name);

char buf[dir_len + 1 + name_len + 1];
mempcpy(mempcpy(mempcpy(buf, dir, dir_len), "/", 1), name, name_len + 1);

execve(buf, argv, envp);
switch (errno) {
case EISDIR:
case ELOOP:
case ENAMETOOLONG:
case ENOENT:
case ENOTDIR:
break;
case ENOEXEC:
return __exec_as_script(buf, argv, envp);
return -1;
case EACCES:
saw_EACCES = true;
break;
default:
return -1;
}
}
if (saw_EACCES)
errno = EACCES;
return -1;
}

static int __execl(int variant, const char *name, const char *argv0, va_list ap) {
// Count the arguments.
va_list count_ap;
va_copy(count_ap, ap);
size_t n = 1;
while (va_arg(count_ap, char *) != NULL) {
++n;
}
va_end(count_ap);

// Construct the new argv.
char *argv[n + 1];
argv[0] = (char *)argv0;
n = 1;
while ((argv[n] = va_arg(ap, char *)) != NULL) {
++n;
}

// Collect the argp too.
char **argp = (variant == ExecLE) ? va_arg(ap, char **) : environ;

va_end(ap);

return (variant == ExecLP) ? execvp(name, argv) : execve(name, argv, argp);
}

int execl(const char *name, const char *arg, ...) {
va_list ap;
va_start(ap, arg);
int result = __execl(ExecL, name, arg, ap);
va_end(ap);
return result;
}

int execle(const char *name, const char *arg, ...) {
va_list ap;
va_start(ap, arg);
int result = __execl(ExecLE, name, arg, ap);
va_end(ap);
return result;
}

int execlp(const char *name, const char *arg, ...) {
va_list ap;
va_start(ap, arg);
int result = __execl(ExecLP, name, arg, ap);
va_end(ap);
return result;
}

int fexecve(int fd, char *const *argv, char *const *envp) {
char buf[40];
snprintf(buf, sizeof(buf), "/proc/self/fd/%d", fd);
execve(buf, argv, envp);
if (errno == ENOENT)
errno = EBADF;
return -1;
}
Loading

0 comments on commit fa947f7

Please sign in to comment.