Skip to content

amd64 guests run a stale Rosetta (367.4) instead of the host's fixed 367.9: MULX clobbers the carry flag, breaking crypto/TLS #2683

Description

@gthoppae

Describe the bug

Update (edited): after filing I found the actual cause. Apple already fixed the MULX bug; OrbStack is loading an older Rosetta for Linux runtime than the one macOS ships, so amd64 guests still have it.

OrbStack 2.2.3 on macOS 26.6.2 maps Rosetta 367.4 into x86 processes (/mnt/rv/[rosetta]), while the host's /Library/Apple/usr/libexec/oah/RosettaLinux/rosetta is 367.9. Apple's container CLI (1.2.2) on the same Mac maps the host file (/run/rosetta/rosetta, identical hash) and the test below is correct there; under OrbStack it is wrong.

host file:        /Library/Apple/usr/libexec/oah/RosettaLinux/rosetta -> 367.9, md5(first 64KiB) f3c478849d70e4e98244393a57361216
apple/container:  /run/rosetta/rosetta (mapped in guest)               -> md5(first 64KiB) f3c478849d70e4e98244393a57361216  (same as host)
OrbStack:         /mnt/rv/[rosetta]   (mapped in guest)               -> 367.4, md5(first 64KiB) c5e224d10a7177399aff92ad31085375  (older build)

The hashes come from rosetta_fp.py (inline at the bottom), which reads the mapped Rosetta runtime out of /proc/self/mem and prints a hash of its first 64 KiB plus version-like strings. Rosetta AOT is not involved: freshly compiled binaries fail and ROSETTA_DISABLE_AOT=1 changes nothing.

Why it matters: Rosetta 367.4 still has Apple's MULX bug (rdar://171843590, diagnosed by Russ Cox in FiloSottile/age#633). MULX must leave flags untouched, but the old runtime clobbers the carry flag, so any carry-chain arithmetic after it is wrong. Since the guest CPUID advertises BMI2, anything doing runtime CPU detection (Go, Zig -mcpu=native, OpenSSL, ...) or built with -mbmi2 takes the broken path, and the failures look like application bugs.

Real-world impact I hit: a Zig program built inside the container fails every TLS handshake to hosts whose certificate chain has an ECDSA P-384 signer (x.ai, Google, GitHub, example.com) with CertificateSignatureInvalid, while RSA chains work. age users in the linked thread saw undecryptable files. It took a full day to trace back to the emulator.

To Reproduce

  1. Apple Silicon Mac, macOS 26.6.2, OrbStack 2.2.3.
  2. Start an amd64 container, install gcc, and run the attached mulxcheck.c (sets CF with an overflowing ADD, executes MULX, reads CF back with ADC):
docker run --rm -it --platform linux/amd64 ubuntu:24.04 bash
apt-get update && apt-get install -y gcc
gcc -O0 -mbmi2 -o mulxcheck mulxcheck.c && ./mulxcheck

OrbStack output:

cpuid advertises BMI2: yes
carry flag after MULX: 0 -> WRONG (MULX clobbered CF)

Same binary, same Mac, under container run --arch amd64 ... (Apple container CLI 1.2.2):

cpuid advertises BMI2: yes
carry flag after MULX: 1 -> CORRECT
  1. Run python3 rosetta_fp.py inside each guest to see which runtime is mapped (output above).

Guest environment: kernel 7.0.14-orbstack-00380-ga7e0a2dc9535, /proc/cpuinfo model VirtualApple @ 2.50GHz, flags include bmi1 bmi2.

Expected behavior

OrbStack should load the Rosetta runtime currently installed on the host, or refresh its copy whenever the host's changes (e.g. after a macOS update), so amd64 guests get Apple's fixes. With 367.9 the test prints carry flag after MULX: 1 -> CORRECT.

Diagnostic report (REQUIRED)

OrbStack info:
Version: 2.2.3
Commit: c83556b0ef8f1ba9a33abbb194622b6b7a1c0307 (v2.2.3)

System info:
macOS: 26.6.2 (25G83)
CPU: arm64, 12 cores
CPU model: Apple M4 Pro
Model: Mac16,8
Memory: 48 GiB

Full report: https://orbstack.dev/_admin/diag/orbstack-diagreport_2026-09-02T12-42-33.901573Z.zip

mulxcheck.c

Screenshots and additional context (optional)

rosetta_fp.py
import re, hashlib, os
maps = open("/proc/self/maps").read().splitlines()
mem = open("/proc/self/mem", "rb")
chunks = []
for line in maps:
    if "rosetta" not in line.lower(): continue
    parts = line.split()
    start, end = (int(x, 16) for x in parts[0].split("-"))
    perms, off = parts[1], int(parts[2], 16)
    try:
        mem.seek(start); data = mem.read(end - start)
    except OSError as e:
        print(f"{parts[0]} {perms} off={off:#x} unreadable: {e}"); continue
    chunks.append((off, perms, data))
    print(f"{parts[0]} {perms} off={off:#x} size={end-start:#x} {parts[5] if len(parts)>5 else ''}")
chunks.sort()
if chunks:
    first = chunks[0][2]
    print("ELF magic:", first[:4], "class/machine bytes:", first[4], first[18:20].hex())
    print("md5 of first 64KiB at file offset", hex(chunks[0][0]), ":", hashlib.md5(first[:65536]).hexdigest())
    blob = b"".join(c[2] for c in chunks)
    print("total mapped bytes:", len(blob))
    seen = set()
    for m in re.finditer(rb"[\x20-\x7e]{6,120}", blob):
        s = m.group().decode()
        if re.search(r"(?i)rosetta|version|build|libexec/oah|advertise|AOT|cache", s) and s not in seen:
            seen.add(s); print("  str:", s)
        if len(seen) > 40: break

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    t/bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions