Skip to content

Alpine runtime: apk fails to write its package database on Android, and pip is blocked by PEP 668 #81

Description

@traveler3022

Two bugs in the Alpine runtime (Alpine 3.23 / apk-tools 3)

Tested against AlpineRuntime.kt at commit 54583f2. Both issues are one-line fixes.


Bug 1 — every apk add silently fails to update the package database

Symptom

Every package install ends with:

Executing busybox-1.37.0-r30.trigger
ERROR: System state may be inconsistent: failed to write database: Permission denied

The files are unpacked and the tools run, so the UI reports success — but
lib/apk/db/installed is never updated.

Root cause

Confirmed by reading apk-tools 3.0.8 (the version in Alpine 3.23), not by guesswork:

  1. src/io.c:1160-1168 — __apk_ostream_to_file() opens the destination with
    openat(atfd, path, O_RDWR | O_TMPFILE | O_CLOEXEC, mode) whenever
    /proc/self/fd is present.
  2. src/io.c:1088 — on close it materialises that anonymous inode with
    linkat(AT_FDCWD, "/proc/self/fd/N", fos->atfd, tmpname, AT_SYMLINK_FOLLOW).
  3. On Android app-private storage that linkat() is denied (EACCES), so
    apk_ostream_cancel() propagates the error.
  4. src/database.c:2335 — apk_db_write_layers() fails, and
    src/database.c:2342 prints the message above.

This is new in apk-tools 3. apk-tools 2.14.x has no O_TMPFILE code path at all,
which is why the Alpine 3.21 runtime (scripts/prepare-ios-runtime.sh) never hit it.
Aether ships Alpine 3.23.4 (AlpineRuntime.kt:840), so it is affected.

Impact — not cosmetic

apk_db_write_layers() (src/database.c:2247) is what writes installed,
triggers and scripts.tar:

ld->installed = apk_ostream_to_file(ld->fd, "installed", 0644);
ld->triggers  = apk_ostream_to_file(ld->fd, "triggers", 0644);

When it fails, apk loses its record of what is installed: apk info is incomplete,
subsequent installs redo work, and package triggers are never registered.
apk's own wording — "System state may be inconsistent" — is accurate.

The failure is currently masked in installPackageProfileLocked()
(AlpineRuntime.kt:449):

return if (result.optBoolean("ok") || verifyPackageProfile(profileId)) {

Because the binaries do run, verifyPackageProfile passes and the profile is
reported Ready, so the broken database never surfaces as a failure.

Fix

Pass proot's --link2symlink. Termux's proot carries a handler written for exactly
this call shape — handle_linkat_from_proc_fd() in
src/extension/link2symlink/link2symlink.c:1014:

/** Handler for linkat(..., "/proc/X/fd/Y", ..., AT_SYMLINK_FOLLOW) */

It detects that the source is a " (deleted)" O_TMPFILE inode and, instead of
creating a symlink, copies the contents into a real file via
open(target_path, O_WRONLY|O_CREAT|O_EXCL, ...). The result is an ordinary
regular file, so there is no risk of leaving a dangling symlink where the apk
database should be.

The bundled binary already supports the flag — verified with
strings app/src/main/assets/runtimes/alpine/arm64-v8a/proot.bin | grep -x -- --link2symlink.

Apply in both places that build a proot command line,
buildAlpineProcess() (AlpineRuntime.kt:718) and
buildAlpineInteractiveCommand() (AlpineRuntime.kt:753):

         val prootCommand = listOf(
             AlpineHostLinker,
             prootFile.absolutePath,
+            // apk (apk-tools 3) commits its database with O_TMPFILE + linkat(), which
+            // Android denies in app-private storage. This turns that into a real file.
+            "--link2symlink",
             "-0",
             "-r",
             rootfsDir.absolutePath,

Bug 2 — the agent cannot pip install anything (PEP 668)

Symptom

error: externally-managed-environment
× This environment is externally managed

Any pip install <pkg> inside the Alpine runtime fails, including as root.

Root cause

Alpine ships a PEP 668 marker at /usr/lib/python3.12/EXTERNALLY-MANAGED:

[externally-managed]
Error=
 The system-wide python installation should be maintained using the system
 package manager (apk) only.

The python profile (AlpineRuntime.kt:1294) installs
python3 py3-pip py3-virtualenv, so pip is present but refuses to install
anything into the system interpreter. This is a deliberate distro guard, not a
permission problem — ls -l on the site-packages directory shows root-writable
directories, which makes it easy to misdiagnose.

Fix

The Alpine rootfs here is a private, disposable sandbox rather than a system that
needs protecting, so the guard has no value and only blocks the agent. Set the
override in buildAlpineProcessEnvironment() (AlpineRuntime.kt:775):

     private fun buildAlpineProcessEnvironment(): Map<String, String> =
         buildMap {
             put("HOME", homeDirectory)
             put("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")
+            // Alpine ships a PEP 668 EXTERNALLY-MANAGED marker, so `pip install` fails even
+            // as root. This rootfs is a private sandbox, not a system that needs the guard.
+            put("PIP_BREAK_SYSTEM_PACKAGES", "1")
+            put("UV_BREAK_SYSTEM_PACKAGES", "1")
             put("AETHER_RUNTIME", "alpine")

Verified in a real Alpine 3.23 rootfs under proot:

--- without the variable ---
error: externally-managed-environment
--- with PIP_BREAK_SYSTEM_PACKAGES=1 ---
Would install certifi-2026.7.22 charset-normalizer-3.5.1 idna-3.20 requests-2.34.2 urllib3-2.8.0

UV_BREAK_SYSTEM_PACKAGES is the documented uv equivalent
(uv pip install --help → --break-system-packages ... [env: UV_BREAK_SYSTEM_PACKAGES=]).

A per-project virtualenv remains the better habit for real projects, but it should
be the user's choice, not a hard wall in front of every pip install.


Verification status

Claim How it was checked
apk uses O_TMPFILE + linkat apk-tools 3.0.8 source, io.c:1168 / io.c:1088
Failure loses the installed database database.c:2247 writes installed/triggers through that path
apk-tools 2.x is unaffected no O_TMPFILE in the 2.14.9 tree
--link2symlink handles this case correctly link2symlink.c:1014, copies rather than symlinks
bundled proot accepts the flag strings on proot.bin
PEP 668 fix works run in an Alpine 3.23 rootfs under proot (output above)
Bug 1 fix on a real device not yet verified on-device — needs an install run on Android

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions