Skip to content

macOS loopback networking policy breaks JVM loopback IPC #171

@dpolivaev

Description

@dpolivaev

Bug report: macOS loopback networking behaves inconsistently for JVM IPC despite allowLocalBinding and allowLocalOutbound

Summary

Under Fence on macOS, JVM tools that rely on loopback sockets can fail
or require JVM address-family workarounds even when both of these
settings are enabled:

  • network.allowLocalBinding = true
  • network.allowLocalOutbound = true

This was observed in three forms:

  1. Gradle daemon startup failed with java.net.SocketException: Operation not permitted.
  2. A direct Java ServerSocket.bind(...) probe behaved inconsistently
    across loopback address forms.
  3. jshell's default execution engine also failed under the same fenced
    conditions.

The direct Java bind probe is the strongest evidence in this report. The
jshell failure is included as an additional observed symptom under the
same conditions; it is not presented here as separate proof of the exact
same internal failure path as Gradle.

Environment

  • Host platform: macOS
  • Sandbox runtime: Fence, launched through pi-fenced
  • Java tools involved: JDK 21-based gradle and jshell
  • Effective Fence config included:
    • network.allowLocalBinding = true
    • network.allowLocalOutbound = true

This investigation was performed inside a Fence session launched via
pi-fenced. The reproductions in this report were not separately
repeated under a bare fence --settings ... invocation during the same
investigation.

Effective configuration relevant to this issue

The effective configuration at the time of observation included:

{
  "network": {
    "allowLocalBinding": true,
    "allowLocalOutbound": true
  }
}

So the report is not about missing loopback permissions in user config.
It is about the behavior that remains after those permissions are
already enabled.

Observed behavior

Case Result
Gradle daemon startup failed with java.net.SocketException: Operation not permitted
Java bind probe: 127.0.0.1 failed with EPERM / Operation not permitted
Java bind probe: localhost failed with EPERM / Operation not permitted
Java bind probe: ::1 succeeded
Java bind probe: 0.0.0.0 succeeded
Gradle with -Djava.net.preferIPv6Addresses=true succeeded
jshell default execution engine with IPv6 preference failed with SocketTimeoutException: Accept timed out

Reproduction 1: Gradle daemon startup

Inside the fenced environment, Gradle daemon startup failed with:

java.net.SocketException: Operation not permitted

The failure propagated through Gradle daemon startup code, including:

org.gradle.internal.remote.internal.inet.TcpIncomingConnector.accept(...)

Reproduction 2: direct Java bind probe

A minimal Java probe using ServerSocket.bind(...) was executed inside
that same fenced environment.

Minimal probe:

import java.net.InetSocketAddress;
import java.net.ServerSocket;

public class BindProbe {
    public static void main(String[] args) throws Exception {
        for (String host : args) {
            try (ServerSocket socket = new ServerSocket()) {
                socket.bind(new InetSocketAddress(host, 0));
                System.out.println(host + " OK " + socket.getLocalPort());
            }
            catch (Exception e) {
                System.out.println(host + " FAIL " + e);
            }
        }
    }
}

Observed results in the fenced environment:

  • bind(127.0.0.1) -> EPERM / Operation not permitted
  • bind(localhost) -> EPERM
  • bind(::1) -> success
  • bind(0.0.0.0) -> success

Additional observations:

  • -Djava.net.preferIPv4Stack=true made bind(127.0.0.1) succeed in
    the test process.
  • -Djava.net.preferIPv6Addresses=true made bind(localhost) resolve
    to IPv6 loopback and succeed.

This is the clearest direct evidence that loopback handling inside the
sandbox is inconsistent across address forms.

Reproduction 3: jshell default execution engine

Under the same fenced conditions, jshell's default execution engine was
retried with IPv6 preference enabled:

jshell -J-Djava.net.preferIPv6Addresses=true

It failed with:

FailOverExecutionControlProvider: FAILED: 0:jdi:hostname(0:0:0:0:0:0:0:1) --
Exception: java.net.SocketTimeoutException: Accept timed out

This is included as an additional observed JVM loopback symptom under
Fence. It is consistent with a loopback policy problem, but this report
does not claim that jshell and Gradle were separately proven to fail via
exactly the same internal mechanism.

Observed workarounds

Gradle

This workaround allowed Gradle daemon startup:

JAVA_TOOL_OPTIONS='-Djava.net.preferIPv6Addresses=true' gradle ...

For example:

JAVA_TOOL_OPTIONS='-Djava.net.preferIPv6Addresses=true' gradle :freeplane:compileJava --stacktrace

Result: BUILD SUCCESSFUL.

Java bind probe

Observed compatibility switches:

  • -Djava.net.preferIPv4Stack=true allowed the 127.0.0.1 bind path
    used in the direct probe.
  • -Djava.net.preferIPv6Addresses=true allowed the localhost bind
    path used in the direct probe.

jshell

No reliable Fence-side workaround was confirmed for jshell's default
execution engine during this investigation.

Expected behavior

With both of these enabled:

  • network.allowLocalBinding = true
  • network.allowLocalOutbound = true

Fence on macOS should allow normal JVM loopback IPC without requiring
JVM address-family tuning. At minimum, loopback behavior should be
consistent for the common address forms involved here:

  • 127.0.0.1
  • localhost
  • ::1

Gradle daemon startup should not fail, and direct Java loopback binds
should not depend on JVM flags just to work inside the sandbox.

Actual behavior

Loopback behavior was inconsistent across address forms and JVM modes:

  • some loopback binds failed with EPERM
  • some loopback binds succeeded only after forcing a JVM IP preference
  • an additional JVM tool (jshell default engine) failed under the same
    fenced conditions with a loopback-related timeout symptom

Candidate Fence area to inspect

Fence's macOS Seatbelt profile generator currently emits these general
localhost rules in internal/sandbox/macos.go:

(allow network-bind (local ip "localhost:*"))
(allow network-inbound (local ip "localhost:*"))
(allow network-outbound (local ip "localhost:*"))

The same file uses remote ip for proxy-port-specific outbound rules.

That makes these questions worth checking:

  1. Should general outbound loopback use remote ip rather than
    local ip?
  2. Is the literal "localhost" sufficient here, or does Fence need
    explicit coverage for loopback address forms such as:
    • 127.0.0.1
    • ::1
  3. Does macOS Seatbelt match these address forms differently than Fence
    currently assumes?

This section is a candidate implementation area to inspect, not a
claim that the root cause has already been proven.

Why this report belongs in Fence

The direct Java bind probe demonstrates inconsistent loopback behavior
inside the fenced environment even though the relevant local networking
settings were already enabled. That is sufficient to justify a Fence
investigation independently of Gradle's implementation details.

Gradle and jshell are useful real-world symptoms, but the core report is
not based on Gradle alone.

Minimal statement of the issue

On macOS, Fence appears to handle JVM loopback IPC inconsistently even
when allowLocalBinding=true and allowLocalOutbound=true. This is
confirmed by direct Java loopback bind behavior inside the fenced
environment and is also reflected in real JVM tooling symptoms such as
Gradle daemon startup failure.

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