Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/offline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Install cross tools and QEMU
run: sudo apt-get update && sudo apt-get install -y binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu qemu-system-arm
run: sudo apt-get update && sudo apt-get install -y binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu qemu-system-arm shellcheck
- name: Build and exercise EL2 handoff
run: make check
- name: Upload our generated code and test output
Expand Down
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ OBJDUMP := $(CROSS)objdump
NM := $(CROSS)nm
CC := $(CROSS)gcc

.PHONY: all gateway probes docker-probes test check
.PHONY: all gateway probes docker-probes test check shell-check
all: gateway probes
gateway: $(BUILD)/gateway.bin $(BUILD)/publisher.bin $(BUILD)/marker.bin $(BUILD)/symbols.txt
probes: $(BUILD)/kvm-guest-probe $(BUILD)/kvm-timer-probe $(BUILD)/kvm-timer-guest.bin
Expand Down Expand Up @@ -63,6 +63,9 @@ test: gateway $(BUILD)/test-gateway.bin
python3 tools/uh_image.py check-gateway --build $(BUILD)
python3 -m unittest discover -s tests -p 'test_*.py' -v

check: all docker-probes test
shell-check:
shellcheck -s sh scripts/docker-network.sh scripts/start-docker-test.sh scripts/stop-docker-test.sh

check: all docker-probes test shell-check
python3 -m py_compile tools/*.py
python3 tools/check_docker_config.py
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ Tested on **SM-A515F, Android 11, A515FXXU5EUJ4**, unlocked bootloader, kernel *
An optional [native Docker profile](docs/docker.md) uses kernel
`4.14.113-22755563-docker` and preserves this KVM backend. Hardware checks include
an actual Docker container with overlay2, default seccomp, CPU affinity/quota,
enforced memory/process limits and veth/bridge traffic. See the
enforced memory/process limits and veth/bridge traffic. Containers also have
[verified IPv4 internet access](evidence/docker-internet-summary.json): registry
pulls, DNS, HTTP(S) and package downloads on default and user-created bridges. See the
[Docker evidence](evidence/docker-summary.json) and build/Android compatibility details.

The [sanitized hardware evidence](evidence/hardware-summary.json) records the measurements. This is a research prototype for **one audited firmware layout**. Full Linux guests, SMP guests, long-running workloads, suspend-to-RAM and other A51 variants remain untested. CI exercises a synthetic EL2 harness, separate from the handset results.
Expand Down Expand Up @@ -56,7 +58,7 @@ On Ubuntu or WSL Ubuntu:

```sh
sudo apt-get update
sudo apt-get install -y make python3 git binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu qemu-system-arm
sudo apt-get install -y make python3 git binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu qemu-system-arm shellcheck
git clone https://github.com/SeniorStackOverflow/a51-kvm.git
cd a51-kvm
make check
Expand Down
4 changes: 2 additions & 2 deletions README.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

**Аппаратная виртуализация заработала на настоящем SM-A515F с Exynos 9611.** Здесь сохранены код, патч ядра, точная конфигурация, инструменты сборки и результаты проверок — чтобы следующий человек мог повторить работу и понять, почему она сработала.

Доступен отдельный [профиль ядра для нативного Docker](docs/docker.md): cgroups, namespaces, OverlayFS, veth, bridge, netfilter и seccomp. Проверены настоящий контейнер Docker, ограничения памяти и процессов, CPU quota/affinity и обмен данными через bridge. KVM сохранён; [результаты](evidence/docker-summary.json) включают повторные аппаратные проверки.
Доступен отдельный [профиль ядра для нативного Docker](docs/docker.md): cgroups, namespaces, OverlayFS, veth, bridge, netfilter и seccomp. Проверены настоящий контейнер Docker, ограничения памяти и процессов, CPU quota/affinity и обмен данными через bridge. У контейнеров есть [проверенный выход в интернет по IPv4](evidence/docker-internet-summary.json): загрузка образов, DNS, HTTP(S) и загрузка пакетов в стандартной и отдельно созданной bridge-сети. KVM сохранён; [результаты](evidence/docker-summary.json) включают повторные аппаратные проверки.

## Проверенный результат

Expand Down Expand Up @@ -33,7 +33,7 @@

```sh
sudo apt-get update
sudo apt-get install -y make python3 git binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu qemu-system-arm
sudo apt-get install -y make python3 git binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu qemu-system-arm shellcheck
git clone https://github.com/SeniorStackOverflow/a51-kvm.git
cd a51-kvm
make check
Expand Down
2 changes: 1 addition & 1 deletion docs/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The handset result used Linux 4.14.113, Samsung's A515FXXU5EUJ4 source, Android

```sh
sudo apt-get update
sudo apt-get install -y make python3 git binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu qemu-system-arm \
sudo apt-get install -y make python3 git binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu qemu-system-arm shellcheck \
gcc-11 g++-11 bc bison flex libssl-dev libelf-dev curl xz-utils
make check
```
Expand Down
76 changes: 68 additions & 8 deletions docs/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ compatibility-warning entries. Its reflection-based probe is in
`probes/docker/VintfProbe.java` and can be compiled to dex using JDK and Android
D8, then run as root with `app_process`. SELinux remained `Enforcing`.

## Reproduce the isolated native-container test
## Run native Docker with internet access

The tested userspace is the official static **Docker 27.5.1 aarch64** archive
and official **runc 1.3.6 arm64** binary. Download them from
Expand All @@ -91,6 +91,8 @@ make docker-probes
adb -s "$SERIAL" push docker /data/local/tmp/codex-a51-docker-bin
adb -s "$SERIAL" push build/docker-daemon-launcher /data/local/tmp/docker-daemon-launcher
adb -s "$SERIAL" push scripts/start-docker-test.sh /data/local/tmp/start-docker-test.sh
adb -s "$SERIAL" push scripts/stop-docker-test.sh /data/local/tmp/stop-docker-test.sh
adb -s "$SERIAL" push scripts/docker-network.sh /data/local/tmp/docker-network.sh
adb -s "$SERIAL" shell su -c 'chmod 755 /data/local/tmp/codex-a51-docker-bin/* /data/local/tmp/docker-daemon-launcher'
```

Expand All @@ -110,11 +112,70 @@ kernel; ext4 supplies a supported persistent upperdir while the backing file
stays within encrypted `/data`.

The daemon has private mount and network namespaces. Existing Android cgroup
controllers are bound into its view; Docker adds its own subgroups. Android
tasks and its firewall stay in their existing namespaces. The test network
has a bridge and containers, with **no uplink to Android's internet connection**.
This launcher is a reproducible test setup, not a boot service or a production
network deployment.
controllers are bound into its view; Docker adds its own subgroups. Docker
owns its bridge and firewall in its private network namespace.

The start script waits for the Docker API and connects this namespace to Android
with a veth pair (`a51-dk0` / `a51-dk1`, `10.231.43.0/30`). The network supervisor
adds only two private firewall chains and three scoped policy rules at priorities
9000–9002. It requests forwarding through netd as the `a51-docker` requester,
so shutdown releases that request without disabling another tethering requester.
Android's existing chains, default policies, routes, VPN and application traffic
are preserved. The supervisor refuses collisions with its interface, chains,
rule priorities or subnet.

Every five seconds the supervisor asks Android where root's ordinary IPv4
internet traffic goes and selects that routing table and outgoing interface.
It refreshes the uplink when Android's default network changes; no available
IPv4 route leaves Docker's outgoing traffic blocked. A terminal unreachable rule
prevents accidental routing through a different Android table. This follows
root's internet route; it does not replicate Android's per-application VPN policy
or provide a VPN kill switch for containers. Docker networks are still subject
to their native rules, including `--internal` isolation.

Containers use public DNS servers `1.1.1.1` and `8.8.8.8`. The daemon uses
Android's system CA directories to verify registry HTTPS. Container HTTPS uses
the CA certificates supplied by its image. The ordinary default bridge and
user-created bridge networks have IPv4 internet access. Container IPv6 and
inbound connections from the LAN are outside this uplink's scope.

Use the CLI from Android root:

```sh
adb -s "$SERIAL" shell su -c '/data/local/tmp/codex-a51-docker-bin/docker --host unix:///data/local/tmp/codex-a51-docker/docker.sock pull alpine:3.22'
adb -s "$SERIAL" shell su -c '/data/local/tmp/codex-a51-docker-bin/docker --host unix:///data/local/tmp/codex-a51-docker/docker.sock run --rm alpine:3.22 wget -qO- https://example.com'
adb -s "$SERIAL" shell su -c 'sh /data/local/tmp/docker-network.sh status'
python3 tools/validate_docker_internet.py --serial "$SERIAL"
```

The internet validator exercises a real registry pull, external DNS, HTTP to an
IPv4 address, certificate-verified HTTPS (including rejection of an untrusted
certificate) and `apk update` on both the default
bridge and a user-created bridge. It also checks container-name DNS, ICMP between
containers, lack of internet on an internal bridge, Android connectivity,
unchanged boot ID and SELinux `Enforcing`.

For the original offline test, start with
`sh /data/local/tmp/start-docker-test.sh --isolated`. The launcher is manually
started; it does not install a boot service. The network supervisor automatically
removes its uplink and rules after the daemon exits. Explicit stop also cleans
up the network:

```sh
adb -s "$SERIAL" shell su -c 'sh /data/local/tmp/stop-docker-test.sh'
```

Network logs are in `/data/local/tmp/codex-a51-docker-network.log`. If a supervisor
was forcibly killed, run `docker-network.sh stop` before starting again; it
recovers stale state only when the interface has its expected ownership alias.
No global firewall flushing is used.

The routing and firewall design follows Android's
[netd forwarding API](https://android.googlesource.com/platform/system/netd/+/451debdc6bff2ffda2f4c85f7de244d52b05c806/server/CommandListener.cpp)
and Docker's [bridge networking](https://docs.docker.com/engine/network/drivers/bridge/)
and [iptables behavior](https://docs.docker.com/engine/network/firewall-iptables/).

## Validate native container resources

```sh
python3 tools/validate_docker_device.py --serial "$SERIAL"
Expand All @@ -130,8 +191,7 @@ CPU affinity/quota, 32-task enforcement and a UDP round trip through veth/bridge
Memory and memory+swap are both limited to 64 MiB, so only the over-limit child
is killed and Android's zRAM cannot absorb the test allocation.

Stop the test daemon through the PID in its own `dockerd.pid`, after verifying
the process command line uses this test socket. The launcher has no autostart.
Stop through `stop-docker-test.sh`; it verifies the daemon uses this test socket.
Results and exact reference hashes are in [docker-summary.json](../evidence/docker-summary.json).

Moby 27.5.1's official `contrib/check-config.sh` reported every generally
Expand Down
87 changes: 87 additions & 0 deletions evidence/docker-internet-summary.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"recorded_utc": "2026-10-06T09:22:13.816016+00:00",
"model": "SM-A515F",
"firmware": "A515FXXU5EUJ4",
"kernel": "4.14.113-22755563-docker",
"docker_version": "27.5.1",
"image": "alpine:3.22",
"image_digests": [
"alpine@sha256:5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8"
],
"platform": "arm64",
"uplink": "IPv4 through Android root default route, verified over Wi-Fi",
"registry_pull_verified": true,
"registry_download_digest": "sha256:5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8",
"default_bridge_checks": [
"PASS external DNS",
"PASS IPv4 HTTP without DNS",
"PASS HTTPS with certificate verification",
"PASS untrusted HTTPS certificate rejected",
"PASS package repository downloads"
],
"user_bridge_checks": [
"PASS container-name DNS and bridge round trip",
"PASS external DNS",
"PASS IPv4 HTTP without DNS",
"PASS HTTPS with certificate verification",
"PASS untrusted HTTPS certificate rejected",
"PASS package repository downloads"
],
"internal_bridge_check": "PASS internal bridge has no internet egress",
"lifecycle": {
"passed": true,
"checks": [
"internet survives full daemon restart",
"duplicate start rejected",
"missing Android route blocks container egress",
"internet recovers automatically after route returns",
"killed supervisor recovered without changing Android rules or forwarding state",
"daemon exit automatically removes uplink and restores Android state",
"isolated start retains original offline behavior",
"explicit stop restores exact Android firewall, policy rules and forwarding state",
"final daemon left running with verified HTTPS internet and unchanged Android boot"
],
"boot_id_unchanged": true,
"selinux": "Enforcing",
"baseline_sha256": {
"rules": "a108a1926439488e5bd2d811b78f8360af5852574b2283eae76895143a8ab5a2",
"filter": "67837c037765019a217f2e8134f2d4b46bc3aa8b39cc2e5a911257d6b02b17b2",
"nat": "53b6f8ff12e122069537aa6378555e901c2aced2905baf998463efc4734dae71",
"mangle": "815dd17c50282a984dfdf8ac6facccaefc63450821bcba2603e423dd4fa5d898",
"raw": "1d591ad0e4ce7ddb4c979ab89f32ccec8a5fe40b8cf42163f2babe46710f8662",
"forwarding": "758008efb9c8566d26b4ae6683afde9ae00feb61c1d2dbd7cb8c09bda30d2627"
}
},
"kvm_regression": {
"passed": true,
"boot_id_unchanged": true,
"guest_cycles": 12,
"guest_cpus": [
0,
1,
2,
3,
4,
5,
6,
7
],
"timer_cpus": [
0,
4
],
"idle_migration_passed": true
},
"android_connectivity_verified": true,
"selinux": "Enforcing",
"boot_id_unchanged": true,
"source_sha256": {
"scripts/docker-network.sh": "34685437a66a297091d13ee1a5f3465049c77d3d79b2e2cc9129553f380efd2d",
"scripts/start-docker-test.sh": "737c5825ae70f92f4fdbd65baac94b68b950cd82f170431020905ea7bd51565d",
"scripts/stop-docker-test.sh": "a70ad1e3d6898a93b22be6153661dada1b795b6ac013fb48259e52a47fc6f2fe",
"probes/docker/launcher.c": "9f843066a78d4a681b12c9d7336b3e9576c19c6045c93a7cb0a45b3ae0e29896",
"tools/validate_docker_internet.py": "b35c46cd64867f3ecd1de30633e8c0f1cd276351ae3eec80f7e82e5da01abc3f"
},
"scope": "Real registry download, DNS, HTTP, certificate-verified HTTPS and APK repository downloads; default/user-defined IPv4 bridges and internal-network isolation; recovery after route loss, daemon exit and supervisor kill. Wi-Fi measured; mobile handover, container IPv6, LAN ingress, per-app VPN policy and long-term workloads not measured. Daemon left running with internet access.",
"passed": true
}
2 changes: 1 addition & 1 deletion evidence/docker-summary.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,5 @@
4
]
},
"scope": "Kernel families and native local Docker container verified; isolated test network has no Android uplink; no production workload or Docker Swarm coverage"
"scope": "Original native Docker kernel/container validation used an isolated network without Android uplink; IPv4 internet and cleanup validation are recorded separately in docker-internet-summary.json; no production workload or Docker Swarm coverage"
}
4 changes: 3 additions & 1 deletion probes/docker/launcher.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ int main(int argc,char**argv){
const char*names[]={"cpu","cpuacct","cpuset","memory","freezer","devices","pids","blkio"};
const char*android[]={"/dev/cpuctl","/acct","/dev/cpuset","/dev/memcg","/dev/freezer",NULL,NULL,NULL};
for(int i=0;i<8;i++){char p[256];snprintf(p,sizeof(p),"/sys/fs/cgroup/%s",names[i]);dir(p);if(android[i])ck(mount(android[i],p,NULL,MS_BIND|MS_REC,NULL),names[i]);else ck(mount("cgroup",p,"cgroup",0,names[i]),names[i]);}
// Host namespaces, mounts and Android firewall are untouched. Docker owns a private netns.
// Docker owns a private netns; the host-side supervisor adds a scoped uplink.
setenv("PATH","/data/local/tmp/codex-a51-docker-bin:/system/bin:/system/xbin",1);
setenv("DOCKER_TMPDIR","/data/local/tmp/codex-a51-docker/tmp",1);
// Go's Linux defaults cannot discover Android's CA directory in this layout.
setenv("SSL_CERT_DIR","/system/etc/security/cacerts:/apex/com.android.conscrypt/cacerts",1);
dir("/data/local/tmp/codex-a51-docker");dir("/data/local/tmp/codex-a51-docker/tmp");
if(runsh("/system/bin/ip link set lo up"))return 1;
dir("/data/local/tmp/codex-a51-docker/data");
Expand Down
Loading
Loading