openai / openai/codex

[CLI] allow_local_binding does not cover IPv4-mapped IPv6 loopback connections

Open
#42,925 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug CLI connectivity sandbox
Dominant language
Rust
Stars
125k
Forks
19.4k
PR merge metrics
PR metrics pending

Description

What version of Codex CLI is running?

codex-cli 0.149.0 (bundled with the Codex Desktop app)

What subscription do you have?

Plus

Which model were you using?

Not relevant to this networking-only reproducer.

What platform is your computer?

macOS 26.6.2 (build 25G83), arm64; Codex Desktop 26.825.32147 (build 7303). uname -mprs: Darwin 25.6.0 arm64 arm

What terminal emulator and version are you using (if applicable)?

No response

Codex doctor report

What issue are you seeing?

With allow_local_binding=true, a local loopback connection from the Codex sandbox succeeds for 127.0.0.1 and ::1, but fails when the same IPv4 listener is reached through the IPv4-mapped IPv6 address ::ffff:127.0.0.1.

This is direct loopback traffic and does not pass through the network proxy. The issue is that the Codex sandbox's local-binding permission does not appear to cover this address representation.

What steps can reproduce the bug?
  1. Enable the Codex managed network proxy and allow local binding.

  2. Save this minimal source as /tmp/codex_loopback.py:

    import socket
    import threading
    
    
    def listener(family, host):
        server = socket.socket(family, socket.SOCK_STREAM)
        if family == socket.AF_INET6:
            server.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
        server.bind((host, 0))
        server.listen(1)
        port = server.getsockname()[1]
    
        def accept_one():
            connection, _ = server.accept()
            connection.close()
            server.close()
    
        thread = threading.Thread(target=accept_one, daemon=True)
        thread.start()
        return port, thread
    
    
    def connect(label, family, host, port, dual_stack=False):
        client = socket.socket(family, socket.SOCK_STREAM)
        if dual_stack:
            client.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
        try:
            client.connect((host, port))
            print(f"{label}: CONNECTED")
        except OSError as error:
            print(f"{label}: {type(error).__name__}: {error}")
        finally:
            client.close()
    
    
    ipv4_port, ipv4_thread = listener(socket.AF_INET, "127.0.0.1")
    connect("AF_INET 127.0.0.1", socket.AF_INET, "127.0.0.1", ipv4_port)
    ipv4_thread.join(timeout=1)
    
    ipv6_port, ipv6_thread = listener(socket.AF_INET6, "::1")
    connect("AF_INET6 ::1", socket.AF_INET6, "::1", ipv6_port)
    ipv6_thread.join(timeout=1)
    
    mapped_port, mapped_thread = listener(socket.AF_INET, "127.0.0.1")
    connect(
        "AF_INET6 dual-stack ::ffff:127.0.0.1",
        socket.AF_INET6,
        "::ffff:127.0.0.1",
        mapped_port,
        dual_stack=True,
    )
    mapped_thread.join(timeout=1)
    
  3. Run it in a Codex sandbox:

    codex sandbox --log-denials /bin/sh -c 'python3 /tmp/codex_loopback.py'
    

    The first two connections succeed. The mapped connection fails with PermissionError: [Errno 1] Operation not permitted, and the sandbox log records a network denial.

What is the expected behavior?

When allow_local_binding=true, ::ffff:127.0.0.1 should have the same local loopback permission as 127.0.0.1. A dual-stack client should be able to connect to a local IPv4 listener.

Additional information

The following are separate follow-up diagnostic experiments, not steps in the
Codex reproducer above.

Codex 0.149.0 was observed to generate this Seatbelt exception for local
binding:

(allow network-outbound (remote ip "localhost:*"))

Running an independent sandbox-exec probe with that rule reproduced the same result: 127.0.0.1 and ::1 were allowed, while ::ffff:127.0.0.1 failed with EPERM. Attempts to express a narrower rule with an IP literal were rejected by the parser (host must be * or localhost in network address). Wildcard rules (*:* and *:port) allowed the mapped connection, but broaden egress and are not suitable fixes.

Apple does not document SBPL for third-party use, and sandbox-exec(1) does not define these address-matching semantics. No safe, narrow rule for the mapped loopback address was identified. Please confirm whether this is a limitation of macOS Seatbelt’s localhost matching. If Codex can safely support IPv4-mapped loopback connections without allowing non-loopback egress, please consider doing so. Otherwise, please document the limitation and provide a supported workaround.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the local-binding sandbox policy and the /tmp/codex_loopback.py reproducer. Run codex sandbox --log-denials /bin/sh -c 'python3 /tmp/codex_loopback.py' and inspect how the Seatbelt localhost:* rule handles IPv4-mapped IPv6 addresses. Done means the mapped loopback connection is safely permitted without broadening egress, or the limitation and a supported workaround are documented.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, rust
Domain
cli, operating-systems, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
50/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.