Skip to content

readlink("/proc/self/exe") and readlink("/proc/self/fd/N") return the Rosetta translator path instead of the guest binary path on x86_64 guests #107

@doanbaotrung

Description

@doanbaotrung

Environment

  • Host: macOS on Apple Silicon (M-series)
  • Guest arch: x86_64 (run via Rosetta translation)
  • Trigger: Running any package management operation that invokes Debian/Ubuntu maintainer scripts (e.g., apt upgrade, dpkg -i)

Problem Description

When an x86_64 guest binary is executed under Rosetta translation, proc_intercept_readlink has two separate code paths that both return the Rosetta interpreter path (/Library/Apple/usr/libexec/oah/RosettaLinux/rosetta, i.e. ROSETTA_PATH) instead of the actual path of the running guest binary. This breaks any guest application that uses /proc/self/exe or /proc/self/fd/N for self-identification.

Code path 1 — readlink("/proc/self/exe")

In src/runtime/procemu.c, proc_intercept_readlink had an early-exit branch at the top of the /proc/self/exe handler:

if (!strcmp(path, "/proc/self/exe")) {
    /* Under rosetta, readlink("/proc/self/exe") points at the rosetta
     * translator (the binfmt_misc interpreter). Matches the behavior Linux
     * exposes when binfmt_misc dispatch is active.
     */
    if (proc_rosetta_active()) {
        size_t len = strlen(ROSETTA_PATH);
        if (len > bufsiz)
            len = bufsiz;
        memcpy(buf, ROSETTA_PATH, len);
        return (int) len;
    }
    /* ... normal path follows */
}

This was written to mimic Linux binfmt_misc behavior, where readlink("/proc/self/exe") in a qemu-static context would return the interpreter (qemu) rather than the guest binary. However, on Linux with native binfmt_misc + Rosetta, /proc/self/exe is supposed to resolve to the real binary, not the interpreter. The behavior that returns the interpreter was specific to older versions of the Rosetta Linux ABI and is not what most guest applications expect.

Code path 2 — readlink("/proc/self/fd/N") when N points to /proc/self/exe

In proc_intercept_open, opening /proc/self/exe under Rosetta opens ROSETTA_PATH on the host:

if (!strcmp(path, "/proc/self/exe")) {
    if (g && g->is_rosetta)
        return open(ROSETTA_PATH, O_RDONLY);   /* <-- returns ROSETTA_PATH fd */
    /* ... */
}

This is intentional: the Rosetta VZ ioctl gate (rosetta_ioctl_target_fd) needs to recognize the fd it is handed as pointing to ROSETTA_PATH, so it accepts the ioctl. However, a side effect is that when the guest subsequently calls readlink("/proc/self/fd/<N>") on that descriptor, fcntl(host_fd, F_GETPATH, fdpath) correctly resolves the host fd to ROSETTA_PATH. The /proc/self/fd/N handler then returns that path verbatim:

/* /proc/self/fd/N -> path of host fd (via fcntl F_GETPATH on macOS) */
if (!strncmp(path, "/proc/self/fd/", 14)) {
    /* ... parse N, get host_fd ... */
    char fdpath[MAXPATHLEN];
    if (fcntl(host_fd, F_GETPATH, fdpath) < 0) { ... }
    /* fdpath is now ROSETTA_PATH -- returned as-is */
    memcpy(buf, fdpath, len);
    return (int) len;
}

Why this matters: GNU coreutils multi-call binary self-identification

Debian/Ubuntu ship a multi-call coreutils binary. Each applet (rm, mv, sed, install, etc.) is a hard-link to the same ELF. At startup, coreutils determines which applet to run by the following sequence:

  1. Open /proc/self/exe → get fd N
  2. Call readlink("/proc/self/fd/N") → expect something like /usr/bin/rm
  3. Match the basename against the applet table

With both bugs above, step 2 returns /Library/Apple/usr/libexec/oah/RosettaLinux/rosetta instead. coreutils fails to match rosetta against any known applet and prints:

coreutils: unknown program 'rosetta'

and exits with status 1.

Observed failure chain during apt upgrade

apt upgrade invokes dpkg, which runs maintainer scripts for every package being upgraded. The maintainer scripts call utilities such as:

  • dpkg-maintscript-helper (a shell wrapper) which calls rm, sed, install, dpkg-trigger
  • base-files.preinst which calls dpkg-query, rm, ln

All of these are hard-linked to the multi-call coreutils binary. Under the Rosetta-path bug, every one of them silently fails with coreutils: unknown program 'rosetta', meaning no package can be configured or installed. dpkg reports them all as failing with exit status 1, and apt upgrade rolls back:

23:45:37 ERROR src/core/elf.c:42: /path/to/rootfs/usr/sbin/dpkg-preconfigure: not an ELF file

(The "not an ELF file" message appears because dpkg-preconfigure is itself a Perl script; when coreutils reports the wrong name, the outer shell tries to execute the coreutils binary thinking it is a new interpreter.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions