1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
/* blocknet.c — preloaded shim that blocks network egress for sandboxed code.
*
* Fails socket() and connect() with EACCES for NETWORK address families only
* — AF_INET and AF_INET6 — for any call that resolves socket()/connect()
* through the symbol this shim replaces. That is not every way to reach the
* network; see "Best-effort by construction" below for the two classes of
* call that never touch this symbol at all.
*
* It used to block socket() unconditionally, which also killed AF_UNIX. A unix
* socket is local IPC and cannot leave the machine, so blocking it bought no
* security and broke things that had nothing to do with egress: multiprocessing,
* runtime event loops, and anything talking to a local socket file. Verified —
* with the old shim, AF_UNIX returned EACCES. That is the same mistake as
* blocking socketpair(), which the next paragraph already knew not to make.
*
* Deliberately does NOT touch socketpair(): runtimes (tokio, libuv, glibc) use
* it for internal signal and event plumbing, and blocking it crashes the
* runtime rather than its network call.
*
* Best-effort by construction, in two independent ways. It only affects
* dynamically-linked programs: a statically linked binary — Go's default —
* never consults the dynamic loader and ignores this entirely. And even a
* dynamically-linked program can route AROUND the interposed symbol rather
* than through it: `ctypes`/`dlsym` pulling `socket()` straight out of libc,
* or a raw `syscall(SYS_socket, ...)`, never resolves the name this shim
* replaces, so neither is seen here at all. Verified live: with this shim
* loaded and `no_net=True`, `ctypes.CDLL(find_library("c")).socket(2, 1, 0)`
* returns a working fd (E-1) — the executor's `unenforced`
* now discloses this whenever the shim is what satisfied `no_net`, rather
* than reporting it as fully applied. Containers or a real kernel-level
* egress block (gVisor, network namespaces) are the fix for either gap; this
* shim is a speed bump against the ordinary case, not a boundary against an
* adversarial one.
*
* Build:
* Built automatically by executor/build.rs as part of `cargo build`, into the
* same directory as the executable (which is where it is looked up at run
* time). Do NOT build it by hand: a shim that lags this source enforces the
* OLD policy while every "is the shim present?" check still passes, which is
* how an AF_UNIX fix here stayed inert for an entire test run.
*
* Linux cc -shared -fPIC -O2 -o blocknet.so blocknet.c (LD_PRELOAD)
* macOS cc -shared -fPIC -O2 -o blocknet.dylib blocknet.c (DYLD_INSERT_LIBRARIES)
*
* The two platforms need DIFFERENT mechanisms, and this is not a detail that
* can be skipped. Linux resolves symbols in load order, so simply DEFINING
* socket() in a preloaded object shadows libc's. macOS uses a two-level
* namespace: each call site is bound to the specific library it was linked
* against, so an identically-named function in an inserted dylib is never
* consulted. The supported mechanism there is a __DATA,__interpose section,
* which dyld reads and applies to every image it loads.
*
* That difference was found by CI, not by reading: the macOS job built the
* dylib, preloaded it, and watched the probe connect anyway.
*
* Two macOS caveats remain regardless of the mechanism, and both make --no-net
* weaker there than on Linux:
*
* 1. SIP and the hardened runtime strip DYLD_INSERT_LIBRARIES for protected
* and hardened-signed binaries, which includes most signed interpreters.
* AMFI can refuse interposing outright.
* 2. Interposing applies between the process and the images dyld loads. It
* does NOT reach calls made INSIDE the dyld shared cache, where libSystem
* lives — so a program's own socket()/connect() is intercepted, but a
* system framework that opens a connection internally is not.
*
* The executor reports --no-net as unenforced when no shim is applied, but it
* cannot detect either of these at runtime. Treat macOS --no-net as a
* best-effort speed bump, never as isolation. Containers are the answer for
* anything stronger, on every platform.
*
* On Linux this shim is no longer the enforcement mechanism: the executor
* installs a seccomp-bpf filter in the child (see executor/src/platform/unix.rs)
* that refuses the socket(AF_INET/AF_INET6) SYSCALL in-kernel, closing the
* ctypes/dlsym/raw-syscall bypass described above. This shim remains the
* no_net path on macOS, and the Linux fallback when the kernel refuses a
* seccomp filter — everything above about it being best-effort is still true
* for those two cases.
*/
/* Only these can reach a network. AF_UNIX/AF_NETLINK/AF_ALG are local. */
static int
static int
/* dyld applies each {replacement, replacee} pair it finds in this section. */
/* On macOS a call made from inside this library is not itself interposed, so
* the real implementation is reachable by name. */
static int
static int
;
;
/* ELF: our definition shadows libc's, so reaching the real one needs
* dlsym(RTLD_NEXT). Calling socket() here would recurse into ourselves. */
int
int