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:
- Gradle daemon startup failed with
java.net.SocketException: Operation not permitted.
- A direct Java
ServerSocket.bind(...) probe behaved inconsistently
across loopback address forms.
- 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:
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:
- Should general outbound loopback use
remote ip rather than
local ip?
- Is the literal
"localhost" sufficient here, or does Fence need
explicit coverage for loopback address forms such as:
- 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.
Bug report: macOS loopback networking behaves inconsistently for JVM IPC despite
allowLocalBindingandallowLocalOutboundSummary
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 = truenetwork.allowLocalOutbound = trueThis was observed in three forms:
java.net.SocketException: Operation not permitted.ServerSocket.bind(...)probe behaved inconsistentlyacross loopback address forms.
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
pi-fencedgradleandjshellnetwork.allowLocalBinding = truenetwork.allowLocalOutbound = trueThis investigation was performed inside a Fence session launched via
pi-fenced. The reproductions in this report were not separatelyrepeated under a bare
fence --settings ...invocation during the sameinvestigation.
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
java.net.SocketException: Operation not permitted127.0.0.1EPERM/Operation not permittedlocalhostEPERM/Operation not permitted::10.0.0.0-Djava.net.preferIPv6Addresses=trueSocketTimeoutException: Accept timed outReproduction 1: Gradle daemon startup
Inside the fenced environment, Gradle daemon startup failed with:
The failure propagated through Gradle daemon startup code, including:
Reproduction 2: direct Java bind probe
A minimal Java probe using
ServerSocket.bind(...)was executed insidethat same fenced environment.
Minimal probe:
Observed results in the fenced environment:
bind(127.0.0.1)->EPERM/Operation not permittedbind(localhost)->EPERMbind(::1)-> successbind(0.0.0.0)-> successAdditional observations:
-Djava.net.preferIPv4Stack=truemadebind(127.0.0.1)succeed inthe test process.
-Djava.net.preferIPv6Addresses=truemadebind(localhost)resolveto 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:
It failed with:
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 --stacktraceResult:
BUILD SUCCESSFUL.Java bind probe
Observed compatibility switches:
-Djava.net.preferIPv4Stack=trueallowed the127.0.0.1bind pathused in the direct probe.
-Djava.net.preferIPv6Addresses=trueallowed thelocalhostbindpath 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 = truenetwork.allowLocalOutbound = trueFence 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.1localhost::1Gradle 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:
EPERMfenced 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:The same file uses
remote ipfor proxy-port-specific outbound rules.That makes these questions worth checking:
remote iprather thanlocal ip?"localhost"sufficient here, or does Fence needexplicit coverage for loopback address forms such as:
127.0.0.1::1currently 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=trueandallowLocalOutbound=true. This isconfirmed 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.