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
/* 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. Nothing else can reach a network.
*
* 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: it only affects dynamically-linked programs.
* A statically linked binary — Go's default — never consults the dynamic
* loader and ignores this entirely.
*
* 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.
*/
/* 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