diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 6dd511f..508c136 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -39,6 +39,11 @@ jobs: # Pinned: GitHub moves "ubuntu-latest" to Ubuntu 26.04 from 2026-10-19, and this job installs # Ubuntu-specific cross-compilers. Move to a newer image deliberately, after testing it. runs-on: ubuntu-24.04 + # id-token and attestations: the build provenance of a release's APKs (see "Attest the APKs") + permissions: + contents: write + id-token: write + attestations: write steps: - name: Checkout Source Code @@ -573,6 +578,23 @@ jobs: if-no-files-found: error retention-days: 3 + # Anyone can check that a release's APK was built here from this repository's source, by this + # workflow, at this commit: gh attestation verify FILE --repo ssfuisu/cortex (signing/README.md) + - name: Checksums of the APKs + if: github.event_name != 'pull_request' && github.event.inputs.dry_run != 'true' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') + working-directory: release-apks + run: | + sha256sum Cortex-arm64-v8a-signed.apk Cortex-armeabi-v7a-signed.apk > SHA256SUMS.txt + cat SHA256SUMS.txt + + - name: Attest the APKs + if: github.event_name != 'pull_request' && github.event.inputs.dry_run != 'true' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: | + release-apks/Cortex-arm64-v8a-signed.apk + release-apks/Cortex-armeabi-v7a-signed.apk + - name: Create GitHub Release if: github.event_name != 'pull_request' && github.event.inputs.dry_run != 'true' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') env: @@ -626,13 +648,15 @@ jobs: if grep -q '[^[:space:]]' changes.md; then cat changes.md else - echo "Cortex runs **Ubuntu 24.04 LTS (Noble Numbat)** with **glibc 2.39** natively on Android: no root, no PRoot, no containers, no virtual machine." + echo "Cortex runs Ubuntu 24.04 LTS (glibc 2.39) on Android through an \`LD_PRELOAD\` library: no root, no PRoot, no virtual machine." fi echo echo "#### Downloads" echo "- \`Cortex-arm64-v8a-signed.apk\`: 64-bit ARM (almost all current phones)" echo "- \`Cortex-armeabi-v7a-signed.apk\`: 32-bit ARM" echo + echo "\`SHA256SUMS.txt\` has the checksums. Each APK also has a build attestation: \`gh attestation verify Cortex-arm64-v8a-signed.apk --repo ${GITHUB_REPOSITORY}\` shows it was built by this repository's workflow from the tagged source." + echo echo "How to verify a download: [signing/README.md](https://github.com/${GITHUB_REPOSITORY}/blob/main/signing/README.md). All versions: [CHANGELOG.md](https://github.com/${GITHUB_REPOSITORY}/blob/main/CHANGELOG.md)." } > release-notes.md echo "Release notes:" @@ -641,6 +665,8 @@ jobs: gh release create "$VERSION" \ release-apks/Cortex-arm64-v8a-signed.apk \ release-apks/Cortex-armeabi-v7a-signed.apk \ + release-apks/SHA256SUMS.txt \ + --target "$(git rev-parse HEAD)" \ --title "Cortex Release $VERSION" \ --notes-file release-notes.md \ $PRERELEASE_FLAG diff --git a/.github/workflows/compat-nightly.yml b/.github/workflows/compat-nightly.yml index 2f430f5..873f8f7 100644 --- a/.github/workflows/compat-nightly.yml +++ b/.github/workflows/compat-nightly.yml @@ -26,6 +26,9 @@ permissions: contents: read actions: read +# The nightly run of the latest release also updates docs/compat.json, which the website's +# compatibility page shows (see "Publish the results on the website"). + concurrency: group: package-compatibility cancel-in-progress: false @@ -35,6 +38,9 @@ jobs: name: Packages on the emulator (API 34, arm64-v8a) runs-on: ubuntu-24.04 timeout-minutes: 180 + permissions: + contents: write + actions: read steps: - name: Checkout Source Code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -67,6 +73,7 @@ jobs: sudo udevadm trigger --name-match=kvm - name: Install and try the packages on the emulator + id: emulator uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0 env: E2E_COMPAT_ONLY: ${{ inputs.only }} @@ -92,3 +99,33 @@ jobs: name: package-compatibility path: e2e-out retention-days: 30 + + # Only the scheduled run of the latest release: a run with a chosen release, some packages + # or a dry run's APKs is not what the page describes. The commit only touches docs/ and + # skips CI. + - name: Publish the results on the website + # A run that timed out leaves part of the list: only a finished one replaces the page's + if: always() && steps.emulator.outcome == 'success' && github.event_name == 'schedule' && github.ref == 'refs/heads/main' + run: | + set -euo pipefail + RESULTS=e2e-out/cortex-e2e/results.txt + if [ ! -e e2e-out/cortex-e2e/done ] || [ ! -s "$RESULTS" ] || ! grep -q '^\(PASS\|FAIL\) ' "$RESULTS"; then + echo "No complete results to publish." + exit 0 + fi + python3 tests/e2e/compat-json.py "$RESULTS" "$(cat tag.txt)" "$(date -u +%F)" > docs/compat.json + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add docs/compat.json + # Not a commit a day: only when a result or the release changed (the date alone does not) + if ! git diff --cached -U0 | grep '^[-+] ' | grep -vq '"date"'; then + echo "The results did not change." + exit 0 + fi + git commit -m "Package compatibility of $(cat tag.txt) [skip ci]" + for i in 1 2 3; do + git pull --rebase --quiet origin main && git push origin HEAD:main && exit 0 + sleep $((i * 10)) + done + echo "Could not push the results." >&2 + exit 1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 70b3c8d..286541b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,40 @@ What changed in each version of Cortex Terminal, newest first. The release workf of a GitHub release from the section of its version here. Older versions are described on the [GitHub releases](https://github.com/ssfuisu/cortex/releases) page. +## 1.27.7 (2026-11-13) + +### Fixed + +- **Podman and other container tools found no cgroups in root mode** ([#72](https://github.com/ssfuisu/cortex/issues/72)). + The chroot got a fresh `/sys` with nothing mounted on `/sys/fs/cgroup`, and Android keeps its + own hierarchies somewhere else (`/dev/cpuset`, `/dev/cpuctl`, `/dev/memcg`, `/acct`), so + `podman info` listed no controllers and dropped the PIDs limit. Root mode now lays the phone's + cgroups out the way Ubuntu does: the cgroup2 tree when the phone has it at `/sys/fs/cgroup` + (Android 12 and later), otherwise one directory per v1 hierarchy, with Android's own hierarchies + bound in and the enabled controllers that have none, often `pids`, mounted. All of it stays in the + session's own mount namespace. + +### Added + +- **Proof of where an APK comes from.** Releases are now built on GitHub from the tagged source with + a build attestation for each APK, so `gh attestation verify Cortex-arm64-v8a-signed.apk --repo + ssfuisu/cortex` shows the workflow and commit it came from, and every release has + `SHA256SUMS.txt`. How to check both is in `signing/README.md`. +- **A benchmark you can run next to proot-distro.** `tests/bench/compare.sh` runs the same tests + (starting programs, file operations, `tar`, pure CPU, Python, `gcc`, `apt-cache`) in whatever it + runs in and prints a table, so Cortex and proot-distro can be compared on the same phone. +- **The nightly package test results on the website**, failures included, with what is known about + each failure: [ssfuisu.github.io/cortex/compat.html](https://ssfuisu.github.io/cortex/compat.html). +- **`SECURITY.md`**: what the app can and cannot do to a phone, every permission and why, and how to + report a problem. **`docs/containers.md`**: Podman and Docker in root mode, including the + iptables setting kernels without nftables NAT need. + +### Changed + +- The website, the README and the release notes say plainly how Cortex works: an `LD_PRELOAD` + library, the technique fakechroot and fakeroot use, rewritten for Android, with no `ptrace` and no + virtual machine. The old "natively, no containers" wording made it sound like more than that. + ## 1.27.6 (2026-11-12) ### Fixed diff --git a/README.md b/README.md index 7130c0c..f9b338e 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ Download · How it works · Real vs emulated · + Package tests · + Security · Changelog · Issues · Telegram @@ -97,7 +99,7 @@ The library is about 10,000 lines of C in [`app/src/main/cpp/hook`](app/src/main ## Install 1. Download from [Releases](https://github.com/ssfuisu/cortex/releases/latest): `Cortex-arm64-v8a-signed.apk` for nearly every phone, `Cortex-armeabi-v7a-signed.apk` for 32-bit ARM. -2. Optional: check the signature as described in [`signing/README.md`](signing/README.md). +2. Optional: check where it comes from. From 1.27.7 on every APK is built on GitHub from the tagged source, `gh attestation verify Cortex-arm64-v8a-signed.apk --repo ssfuisu/cortex` proves it, and `SHA256SUMS.txt` is next to it. The signing key is in [`signing/README.md`](signing/README.md). 3. Install and open it. Ubuntu is unpacked on the first start, then you get a shell. Android 7.0 or newer. Like Termux, the app targets API level 28, the last one that lets an app run programs from its own data directory. @@ -169,14 +171,43 @@ Programs run at native speed. The library only costs something on calls that tak Run [`tests/bench/run-micro.sh`](tests/bench/run-micro.sh) inside Cortex to measure your own phone. +**Comparing with proot-distro yourself.** [`tests/bench/compare.sh`](tests/bench/compare.sh) runs the same tests anywhere (starting programs, creating, listing and reading files, `tar`, `find`, pure CPU, Python start-up, a compile, `apt-cache`) and prints a table. Run it in Cortex and in proot-distro on the same phone: + +```bash +curl -fsSL https://raw.githubusercontent.com/ssfuisu/cortex/main/tests/bench/compare.sh | bash +``` + +It also says what it detected it runs in, so the two tables can't get mixed up. + ## Root mode On a rooted phone (Magisk, KernelSU, APatch), *Settings > Root mode* runs new sessions as real root in a chroot of the same Ubuntu files. The library is not loaded there. Mounts, real owners, hard links, set-user-ID programs and low ports work as on a PC. Each session gets its own mount namespace, so nothing is mounted for Android or other apps. Turning it off gives the files back to the app. +Containers need root mode. Since 1.27.7 it also mounts the phone's cgroups at `/sys/fs/cgroup`, and [docs/containers.md](docs/containers.md) has the steps for Podman and Docker, including the iptables setting many Android kernels need. + +## Is it safe for the phone? + +In normal mode Cortex is an ordinary app: everything you run in it runs as the app's own Android user, in the app sandbox. It can't change Android, the system or other apps' private data, it can't brick anything, and uninstalling removes everything in the app's own storage. Programs only *think* they are root. The one thing to keep in mind: once you allow storage access, commands can change and delete your files in `/sdcard`, and uninstalling doesn't undo that. [SECURITY.md](SECURITY.md) explains every permission the app asks for and what the app itself connects to (Ubuntu's mirrors and GitHub's release API, nothing else). + ## FAQ +**What do people actually use it for?** +Programming on the phone or tablet (Python, Node.js, C, Rust, Go, git), small servers and bots (nginx, Redis, MariaDB, a Telegram bot), SSH into Cortex from a computer with `cortex-sshd`, scripts that use the phone through `cortex-api`, and the occasional desktop program on the built-in X11 desktop. + **Will every Ubuntu package work?** -Most command-line software does, and a nightly job installs and runs a set of common packages on an emulator. What needs the things in the list above (systemd services, other users, raw devices) does not work in normal mode. +Most command-line software does. Every night a job installs a set of common packages on an emulator and tests each one, the [latest results](https://ssfuisu.github.io/cortex/compat.html) are public, failures included. What needs the things in the list above (systemd services, other users, raw devices) does not work in normal mode. + +**Can I run Docker or Podman?** +Only in root mode, see [docs/containers.md](docs/containers.md). Containers need namespaces, mounts and cgroups, and Android never gives those to an app. + +**Is there a GUI?** +Yes, as an add-on: *Desktop* in the menu installs Openbox or XFCE (and Firefox if you want) and shows it inside the app. It's drawn by the CPU, no GPU, so it suits editors and browsing more than games. + +**Does it work on 32-bit phones?** +Yes, `Cortex-armeabi-v7a-signed.apk` is Ubuntu's armhf. Every change builds it and installs and sets it up on an emulator, but the emulator can't run 32-bit Ubuntu programs, so the in-terminal tests only run for 64-bit. Reports from 32-bit phones help. + +**Why does Go need special code?** +Go programs don't use the C library, they make their system calls themselves, so a preloaded library never sees them. Before a Go 1.19+ program starts for the first time, Cortex adds a small piece of code to it that does the same path mapping. **Do single-file downloads work?** Static C and C++ programs do, and Go programs built with Go 1.19 or newer. Older or 32-bit Go programs run but see Android's files instead of Ubuntu's. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..1a3915c --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,33 @@ +# Security + +## Reporting a problem + +If you find a security problem in Cortex, please don't open a public issue. Write on [Telegram](https://t.me/ratzgn), or open an issue that only says you found something and we'll take it private from there. I'll answer within a few days and credit you in the changelog if you want. + +## What Cortex can and cannot do to your phone + +**In normal mode Cortex is an ordinary Android app.** Everything it runs, `apt`, `gcc`, a server you start, runs as the app's own Android user, inside the app sandbox, with the same rights the terminal app itself has. Programs think they are root because the library answers "uid 0", but the kernel still sees the app's user. That means: + +- It cannot change Android, the system partition, the bootloader or other apps' private data. It cannot brick the phone. A broken package can break Cortex's own Ubuntu, and clearing the app's data (or reinstalling it) gives you a fresh one. +- Uninstalling the app removes everything in its own storage. Nothing is left in the system. +- **Shared storage is the exception.** If you allow storage access, programs in Cortex can read, change and delete your files in `/sdcard` (photos, downloads, documents) just like a file manager can. A bad `rm` there is not undone by clearing data or uninstalling, and files programs create there stay after uninstalling. +- Ubuntu's files live in the app's private storage (`/data/data/org.cortex.terminal`), which other apps cannot read. Your phone's shared storage (`/sdcard`) is only reachable after you allow it. +- Packages come from Ubuntu's own mirrors and `apt` checks Ubuntu's signatures, the same as on a PC. Cortex ships Ubuntu's keyring and does not add any other package source. + +**Root mode is different.** It only exists on phones you rooted yourself, only runs after you turn it on and your root manager (Magisk, KernelSU, APatch) asks you, and then sessions really are root. A root session can do anything root can do on Android. Each session gets its own mount namespace so its mounts don't leak into Android, but a root shell is a root shell: use it like you would `su`. + +## Permissions + +| Permission | Why | +|---|---| +| `INTERNET`, `ACCESS_NETWORK_STATE` | `apt`, `git`, `curl` and every other network program. The app itself only talks to Ubuntu's mirrors (package lists and the fastest-mirror check) and to `api.github.com` (is there a new Cortex release). No analytics, no ads, no tracking. | +| `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_SPECIAL_USE`, `WAKE_LOCK` | Keeps your sessions running when the app is in the background, with the notification you see. The wake lock is only taken when you turn it on in that notification. | +| `POST_NOTIFICATIONS`, `VIBRATE` | The session notification, `notify-send` and `cortex-api` notifications, and the bell. | +| `READ_EXTERNAL_STORAGE`, `WRITE_EXTERNAL_STORAGE`, `MANAGE_EXTERNAL_STORAGE` | `/sdcard` in the terminal. The app asks once at the start; say *Later* and everything else works, only `/sdcard` stays closed. | +| `REQUEST_INSTALL_PACKAGES` | The in-app updater installs a newer Cortex APK, after you tap it. | + +Cortex has no accessibility service, reads no contacts, SMS, location, camera or microphone, and the Files app entry only shows the Cortex home folder to the system file picker. + +## Checking a download + +From 1.27.7 on, every release is built on GitHub's servers from the tagged source and comes with a build attestation and `SHA256SUMS.txt`. How to check both, and the signing key fingerprints, is in [signing/README.md](signing/README.md). The code is all here: the library that does the path mapping is [`app/src/main/cpp/hook`](app/src/main/cpp/hook), the root mode helper is [`app/src/main/cpp/root/cortex-chroot.c`](app/src/main/cpp/root/cortex-chroot.c). diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 96eff64..5b5af31 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,8 +16,8 @@ android { // from the app's writable data directory (filesDir). Android 10+ (targetSdk >= 29) // enforces SELinux W^X restrictions preventing execution from writable app data storage. targetSdk = 28 - versionCode = 12706 - versionName = "1.27.6" + versionCode = 12707 + versionName = "1.27.7" // UbuntuOnDeviceTest (app/src/androidTest), on Firebase Test Lab testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/cpp/root/cortex-chroot.c b/app/src/main/cpp/root/cortex-chroot.c index e20bac3..a1b1cbc 100644 --- a/app/src/main/cpp/root/cortex-chroot.c +++ b/app/src/main/cpp/root/cortex-chroot.c @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -239,6 +240,192 @@ static int setup_dev(const char *root) { return 0; } +// --------------------------------------------------------------------------------------------- +// /sys/fs/cgroup + +#define CGROUP_NAMES_MAX 32 +#define CGROUP_NAME_LEN 32 + +static int path_has_prefix(const char *path, const char *prefix) { + size_t n = strlen(prefix); + return strncmp(path, prefix, n) == 0 && (path[n] == '\0' || path[n] == '/'); +} + +// The enabled controllers the kernel has (/proc/cgroups) and the v1 hierarchy each is in (0 for +// none). +static int read_controllers(char names[][CGROUP_NAME_LEN], int *hierarchies, int max) { + FILE *f = fopen("/proc/cgroups", "re"); + if (!f) return 0; + char line[256]; + int count = 0; + while (count < max && fgets(line, sizeof(line), f)) { + char name[CGROUP_NAME_LEN]; + int hierarchy, cgroups, enabled; + if (line[0] == '#' || sscanf(line, "%31s %d %d %d", name, &hierarchy, &cgroups, &enabled) != 4) continue; + if (!enabled) continue; + memcpy(names[count], name, sizeof(name)); + hierarchies[count] = hierarchy; + count++; + } + fclose(f); + return count; +} + +static int is_controller(const char *opt, size_t len, char names[][CGROUP_NAME_LEN], int count) { + for (int i = 0; i < count; i++) { + if (strlen(names[i]) == len && strncmp(names[i], opt, len) == 0) return 1; + } + return 0; +} + +// The directory a v1 hierarchy gets, named like Ubuntu's: its controllers joined by commas +// ("cpu,cpuacct") or the name= of a named hierarchy ("systemd"). +static int hierarchy_dir_name(const char *opts, char names[][CGROUP_NAME_LEN], int count, char *out, size_t size) { + out[0] = '\0'; + size_t used = 0; + for (const char *p = opts; *p;) { + const char *end = strchr(p, ','); + size_t len = end ? (size_t)(end - p) : strlen(p); + const char *word = p; + size_t wlen = len; + if (len > 5 && strncmp(p, "name=", 5) == 0) { + word = p + 5; + wlen = len - 5; + } else if (!is_controller(p, len, names, count)) { + word = NULL; + } + if (word && wlen > 0) { + if (used + wlen + 2 > size) return -1; + if (used) out[used++] = ','; + memcpy(out + used, word, wlen); + used += wlen; + out[used] = '\0'; + } + if (!end) break; + p = end + 1; + } + return used ? 0 : -1; +} + +static int is_mount_point(const char *path) { + char parent[PATH_MAX]; + struct stat st, pst; + if (join_path(parent, sizeof(parent), path, "..") != 0 || stat(path, &st) != 0 || stat(parent, &pst) != 0) return 0; + return st.st_dev != pst.st_dev; +} + +// Container tools (Podman, Docker, LXC, systemd-nspawn) look for cgroups in /sys/fs/cgroup. The +// fresh sysfs has nothing mounted there and Android keeps its hierarchies elsewhere (/dev/cpuset, +// /dev/cpuctl, /dev/memcg, /acct, cgroup2 at /dev/cg2_bpf or at /sys/fs/cgroup), so they are +// bound in the layout Ubuntu uses. Binding keeps Android's own hierarchies and their options; the +// enabled controllers in no hierarchy at all (often pids) get one of their own. Everything here +// stays in the session's mount namespace and is best effort. +static void setup_cgroups(const char *root) { + char base[PATH_MAX]; + struct stat st; + if (join_path(base, sizeof(base), root, "sys/fs/cgroup") != 0 || lstat(base, &st) != 0 || !S_ISDIR(st.st_mode)) { + return; + } + // sysfs was bound from the host with whatever is mounted below it. + if (is_mount_point(base)) return; + + char names[CGROUP_NAMES_MAX][CGROUP_NAME_LEN]; + int hierarchies[CGROUP_NAMES_MAX]; + int count = read_controllers(names, hierarchies, CGROUP_NAMES_MAX); + + FILE *m = setmntent("/proc/self/mounts", "re"); + if (!m) return; + char v2[PATH_MAX] = ""; + int v2_is_root = 0; + struct mntent *e; + while ((e = getmntent(m)) != NULL) { + if (strcmp(e->mnt_type, "cgroup2") != 0 || path_has_prefix(e->mnt_dir, root)) continue; + if (strcmp(e->mnt_dir, "/sys/fs/cgroup") == 0) { + snprintf(v2, sizeof(v2), "%s", e->mnt_dir); + v2_is_root = 1; + break; + } + if (!v2[0]) snprintf(v2, sizeof(v2), "%s", e->mnt_dir); + } + // cgroup v2 at /sys/fs/cgroup on the host (Android 12 and later, most Linux systems): the same + // unified layout in the chroot. + if (v2_is_root) { + endmntent(m); + if (bind_mount(v2, base, 0) != 0) warn_msg("binding %s: %s", v2, strerror(errno)); + return; + } + if (mount("tmpfs", base, "tmpfs", MS_NOSUID | MS_NODEV | MS_NOEXEC, "mode=755") != 0) { + warn_msg("mounting tmpfs on /sys/fs/cgroup: %s", strerror(errno)); + endmntent(m); + return; + } + rewind(m); + while ((e = getmntent(m)) != NULL) { + if (strcmp(e->mnt_type, "cgroup") != 0 || path_has_prefix(e->mnt_dir, root)) continue; + char name[PATH_MAX], dir[PATH_MAX]; + if (hierarchy_dir_name(e->mnt_opts, names, count, name, sizeof(name)) != 0) continue; + if (join_path(dir, sizeof(dir), base, name) != 0) continue; + // The same hierarchy mounted twice on the host. + if (lstat(dir, &st) == 0) continue; + if (mkdir(dir, 0755) != 0) continue; + if (bind_mount(e->mnt_dir, dir, 0) != 0) { + warn_msg("binding %s: %s", e->mnt_dir, strerror(errno)); + rmdir(dir); + continue; + } + // cpu and cpuacct of a "cpu,cpuacct" hierarchy, as on Ubuntu. + if (strchr(name, ',')) { + char copy[PATH_MAX]; + snprintf(copy, sizeof(copy), "%s", name); + for (char *save = NULL, *tok = strtok_r(copy, ",", &save); tok; tok = strtok_r(NULL, ",", &save)) { + char link[PATH_MAX]; + if (join_path(link, sizeof(link), base, tok) == 0) best_effort(symlink(name, link)); + } + } + } + endmntent(m); + // The hierarchies not mounted where this namespace can see them: the controllers of one + // hierarchy are mounted together (the kernel gives back the existing one), a controller in none + // gets its own. + int done[CGROUP_NAMES_MAX] = {0}; + for (int i = 0; i < count; i++) { + if (done[i]) continue; + char opts[PATH_MAX], dir[PATH_MAX]; + snprintf(opts, sizeof(opts), "%s", names[i]); + done[i] = 1; + for (int j = i + 1; hierarchies[i] != 0 && j < count; j++) { + if (hierarchies[j] != hierarchies[i]) continue; + size_t used = strlen(opts); + snprintf(opts + used, sizeof(opts) - used, ",%s", names[j]); + done[j] = 1; + } + if (join_path(dir, sizeof(dir), base, names[i]) != 0 || lstat(dir, &st) == 0) continue; + char group[PATH_MAX]; + if (join_path(group, sizeof(group), base, opts) != 0 || lstat(group, &st) == 0 || mkdir(group, 0755) != 0) { + continue; + } + // EBUSY when the controller is in use in the v2 hierarchy: it stays there. + if (mount("cgroup", group, "cgroup", MS_NOSUID | MS_NODEV | MS_NOEXEC, opts) != 0) { + rmdir(group); + continue; + } + if (strchr(opts, ',')) { + for (char *save = NULL, *tok = strtok_r(opts, ",", &save); tok; tok = strtok_r(NULL, ",", &save)) { + char link[PATH_MAX]; + if (join_path(link, sizeof(link), base, tok) == 0) best_effort(symlink(group + strlen(base) + 1, link)); + } + } + } + if (v2[0]) { + char dir[PATH_MAX]; + if (join_path(dir, sizeof(dir), base, "unified") == 0 && mkdir(dir, 0755) == 0 && bind_mount(v2, dir, 0) != 0) { + rmdir(dir); + } + } + // Read-only like Ubuntu's: the hierarchies below stay writable. + best_effort(mount(NULL, base, NULL, MS_REMOUNT | MS_NOSUID | MS_NODEV | MS_NOEXEC | MS_RDONLY, "mode=755")); +} + // --------------------------------------------------------------------------------------------- // The session @@ -342,7 +529,7 @@ static int setup_session(const char *root) { } if (mount_kernel_fs(root, "proc", "proc") != 0) return EXIT_MOUNT; - mount_kernel_fs(root, "sys", "sysfs"); + if (mount_kernel_fs(root, "sys", "sysfs") == 0) setup_cgroups(root); if (setup_dev(root) != 0) return EXIT_MOUNT; setup_sdcard(root); setup_data_alias(root); diff --git a/docs/compat.html b/docs/compat.html new file mode 100644 index 0000000..6330631 --- /dev/null +++ b/docs/compat.html @@ -0,0 +1,188 @@ + + + + + +Package compatibility · Cortex Terminal + + + + + + + + + + + +
+

Which packages work

+

Every night a job takes the latest release, installs common packages with apt on an Android emulator, one at a time, and runs a short test of what each one is for: a Python script, a compile, a server answering a request. These are the latest results, failures included. The page changes when a result does.

+ +
+
…packages tested
+
…work
+
…don't
+
…release tested
+
+ +
+ + + +
+ +

Loading the latest results…

+
    + + +
    +

    How to read this

    +

    The emulator is an x86_64 machine running the arm64 APK through Android's translator, because GitHub has no ARM machines with an emulator. Some failures look like they come from that translator and not from Cortex, they're marked, and stay listed as failures until a phone shows otherwise. The packages are installed one after another into the same Ubuntu with --no-install-recommends, so a later test can find what an earlier one installed, and each test gets 5 minutes.

    +

    Something you use isn't here?

    +

    The list lives in tests/e2e/compat.sh, one line per package. Open an issue or a pull request and it gets tested every night from then on. The logs of every run are in the workflow runs.

    +
    +
    + + + + + + diff --git a/docs/compat.json b/docs/compat.json new file mode 100644 index 0000000..d6336a8 --- /dev/null +++ b/docs/compat.json @@ -0,0 +1,211 @@ +{ + "version": "v1.27.6", + "date": "2026-11-13", + "where": "Android 14 emulator (API 34), arm64-v8a APK", + "packages": [ + { + "name": "python3", + "ok": true, + "seconds": 37, + "why": "" + }, + { + "name": "python3-venv-pip", + "ok": true, + "seconds": 129, + "why": "" + }, + { + "name": "nodejs-npm", + "ok": true, + "seconds": 269, + "why": "" + }, + { + "name": "c-gcc", + "ok": true, + "seconds": 49, + "why": "" + }, + { + "name": "cpp-g++", + "ok": true, + "seconds": 4, + "why": "" + }, + { + "name": "cmake", + "ok": true, + "seconds": 29, + "why": "" + }, + { + "name": "rust", + "ok": true, + "seconds": 26, + "why": "" + }, + { + "name": "go", + "ok": false, + "seconds": 16, + "why": "the test failed: $ printf \"package main\\nimport \\\"fmt\\\"\\nfunc main() { fmt.Println(\\\"hello from Go\\\") }\\n\" > h.go && go run h.go Error running /data/user/0/org.cortex.terminal/files/cortex/bin/go: Unable to get realpath of go exit status 255" + }, + { + "name": "java", + "ok": false, + "seconds": 30, + "why": "the test failed: # bash: line 1: 13043 Aborted javac H.java exit status 134" + }, + { + "name": "php", + "ok": true, + "seconds": 274, + "why": "" + }, + { + "name": "ruby", + "ok": true, + "seconds": 15, + "why": "" + }, + { + "name": "perl", + "ok": true, + "seconds": 2, + "why": "" + }, + { + "name": "lua", + "ok": true, + "seconds": 6, + "why": "" + }, + { + "name": "git", + "ok": true, + "seconds": 9, + "why": "" + }, + { + "name": "curl-https", + "ok": true, + "seconds": 6, + "why": "" + }, + { + "name": "wget", + "ok": true, + "seconds": 7, + "why": "" + }, + { + "name": "jq", + "ok": true, + "seconds": 7, + "why": "" + }, + { + "name": "sqlite3", + "ok": true, + "seconds": 6, + "why": "" + }, + { + "name": "zip-unzip", + "ok": true, + "seconds": 7, + "why": "" + }, + { + "name": "xz-zstd", + "ok": true, + "seconds": 6, + "why": "" + }, + { + "name": "rsync", + "ok": true, + "seconds": 8, + "why": "" + }, + { + "name": "openssh-client", + "ok": true, + "seconds": 10, + "why": "" + }, + { + "name": "htop", + "ok": true, + "seconds": 7, + "why": "" + }, + { + "name": "ripgrep", + "ok": true, + "seconds": 6, + "why": "" + }, + { + "name": "vim", + "ok": true, + "seconds": 12, + "why": "" + }, + { + "name": "neovim", + "ok": true, + "seconds": 13, + "why": "" + }, + { + "name": "tmux", + "ok": true, + "seconds": 11, + "why": "" + }, + { + "name": "zsh", + "ok": true, + "seconds": 15, + "why": "" + }, + { + "name": "fish", + "ok": true, + "seconds": 16, + "why": "" + }, + { + "name": "redis", + "ok": true, + "seconds": 18, + "why": "" + }, + { + "name": "nginx", + "ok": true, + "seconds": 20, + "why": "" + }, + { + "name": "mariadb", + "ok": true, + "seconds": 44, + "why": "" + }, + { + "name": "postgresql-client", + "ok": true, + "seconds": 31, + "why": "" + }, + { + "name": "postgresql", + "ok": false, + "seconds": 86, + "why": "the test failed: psql: error: connection to server on socket \"/var/run/postgresql/.s.PGSQL.5432\" failed: No such file or directory Is the server running locally and accepting connections on that socket? exit status 2" + } + ] +} diff --git a/docs/containers.md b/docs/containers.md new file mode 100644 index 0000000..44bb8ee --- /dev/null +++ b/docs/containers.md @@ -0,0 +1,80 @@ +# Containers (Podman, Docker) on Cortex + +Short version: **containers need root mode.** In normal mode Cortex is an ordinary app, and Android doesn't let an app create namespaces, mount file systems or touch cgroups, which is exactly what a container runtime does. No amount of library tricks changes that, so `podman run` or `dockerd` won't work there. If you just want to run a program, install it with `apt` and run it directly, it's the same Ubuntu. + +On a rooted phone, root mode (*Settings > Root mode*) gives you a real chroot with real root, and from 1.27.7 on it also sets up `/sys/fs/cgroup` the way Ubuntu does. That's enough for Podman on many kernels. Android kernels differ a lot between phones though, so treat this as "should work, here's what to try" and not as a promise. + +## 1. Does your kernel have what containers need? + +```bash +zcat /proc/config.gz | grep -E 'CONFIG_(NAMESPACES|PID_NS|NET_NS|USER_NS|CGROUPS|CGROUP_PIDS|OVERLAY_FS|VETH|BRIDGE|NF_NAT|IP_NF_NAT|NFT_NAT|NF_TABLES_IPV4)=' +``` + +You want `NAMESPACES`, `PID_NS`, `NET_NS` and `CGROUPS` at `=y`. `OVERLAY_FS` makes images fast, without it use the `vfs` driver below. `VETH` and `BRIDGE` are needed for container networking, without them use `--network=host`. If `/proc/config.gz` doesn't exist your kernel doesn't expose its config, just try. + +## 2. Podman + +```bash +apt install podman +``` + +Tell it there is no systemd: + +```bash +mkdir -p /etc/containers +cat > /etc/containers/containers.conf <<'EOF' +[engine] +cgroup_manager = "cgroupfs" +events_logger = "file" +EOF +``` + +**Networking.** Many Android kernels have the old iptables NAT (`CONFIG_IP_NF_NAT=y`) but not the nftables one (`CONFIG_NFT_NAT` not set). Ubuntu's `iptables` uses nftables by default, and then Netavark fails with `TABLE_ADD failed (Address family not supported by protocol): table nat`. Switch to the legacy backend: + +```bash +update-alternatives --set iptables /usr/sbin/iptables-legacy +update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy +``` + +Then: + +```bash +podman run --rm docker.io/library/hello-world +``` + +If networking still fails, `--network=host` skips it entirely and the container uses the phone's network directly: + +```bash +podman run --rm --network=host docker.io/library/alpine wget -qO- https://example.com +``` + +**Storage.** If pulling an image fails with an overlay error, use the slower but always working `vfs` driver. Changing the driver needs a reset first, and **`podman system reset` deletes every container, image, network and volume Podman has**, so only do this on a fresh setup or after saving what you need. Reset with the old setting, then switch: + +```bash +podman system reset # asks before it deletes anything +cat > /etc/containers/storage.conf <<'EOF' +[storage] +driver = "vfs" +EOF +``` + +## 3. Docker + +Docker works the same way, it's just heavier. There's no systemd to start it, so start the daemon yourself in one tab and use it in another: + +```bash +apt install docker.io +update-alternatives --set iptables /usr/sbin/iptables-legacy +update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy +dockerd > /var/log/dockerd.log 2>&1 & +docker run --rm hello-world +``` + +If `dockerd` stops right away, the end of `/var/log/dockerd.log` says which kernel feature it misses. + +## About the warnings + +- **"PIDs limit discarded"** means the `pids` cgroup controller isn't there. Before 1.27.7 root mode had no cgroups at all. Now it mounts every controller the kernel has, so if you still see this, your kernel was built without `CONFIG_CGROUP_PIDS`. +- **Capabilities.** `podman info` lists the capabilities a *container* gets by default (`CAP_CHOWN`, `CAP_KILL`, `CAP_NET_BIND_SERVICE` and so on). `CAP_NET_ADMIN` and `CAP_SYS_ADMIN` aren't on that list on any Linux. It's Podman's default, not a Cortex limit. Add them per container with `--cap-add` when you need them. + +If something here doesn't match what you see on your phone, open an issue with your phone model, kernel version (`uname -r`) and the error. That's how this page gets better. See [#72](https://github.com/ssfuisu/cortex/issues/72) for the first report. diff --git a/docs/index.html b/docs/index.html index 1faa741..480ec86 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,7 +4,7 @@ Cortex Terminal: Ubuntu 24.04 on Android without root or PRoot - +