Line data Source code
1 : #define _GNU_SOURCE
2 : #include "../../../shared/commands/configure/configure.h"
3 :
4 : #include <errno.h>
5 : #include <stdlib.h> /* strtoul */
6 : #include <unistd.h>
7 : #include <stdio.h>
8 : #include <dirent.h>
9 : #include <signal.h>
10 : #include <fcntl.h>
11 : #include <pthread.h>
12 : #include <sys/stat.h>
13 : #include <sys/types.h>
14 :
15 0 : #define NAME "kill"
16 :
17 : static void
18 : init_perm( fd_cap_chk_t * chk,
19 0 : config_t const * config FD_PARAM_UNUSED ) {
20 0 : fd_cap_chk_root( chk, NAME, "check all open file descriptors in `/proc/`" );
21 0 : }
22 :
23 : static void
24 : cmdline( char * buf,
25 : size_t len,
26 0 : ulong pid ) {
27 0 : char path[ PATH_MAX ];
28 0 : FD_TEST( fd_cstr_printf_check( path, PATH_MAX, NULL, "/proc/%lu/cmdline", pid ) );
29 :
30 0 : FILE * fp = fopen( path, "r" );
31 0 : if( FD_UNLIKELY( !fp && errno==ENOENT ) ) {
32 0 : buf[ 0 ] = '\0';
33 0 : return;
34 0 : }
35 0 : if( FD_UNLIKELY( !fp ) ) FD_LOG_ERR(( "error opening `/proc/%lu/cmdline` (%i-%s)", pid, errno, fd_io_strerror( errno ) ));
36 :
37 0 : ulong read = fread( buf, 1, len - 1, fp );
38 0 : if( FD_UNLIKELY( ferror( fp ) ) ) FD_LOG_ERR(( "error reading `/proc/%lu/cmdline` (%i-%s)", pid, errno, fd_io_strerror( errno ) ));
39 0 : if( FD_UNLIKELY( fclose( fp ) ) ) FD_LOG_ERR(( "error closing `/proc/%lu/cmdline` (%i-%s)", pid, errno, fd_io_strerror( errno ) ));
40 :
41 0 : buf[ read ] = '\0';
42 0 : }
43 :
44 : /* hugetlb_kib returns the process's HugetlbPages: usage from
45 : /proc/<pid>/status (cheap counter read, no page table walk), or
46 : ULONG_MAX if the field is missing. Both hugetlbfs file mappings
47 : (workspaces) and anonymous MAP_HUGETLB pages count toward it, so a
48 : zero here proves the expensive maps/numa_maps scans can be
49 : skipped. */
50 :
51 : static ulong
52 0 : hugetlb_kib( ulong pid ) {
53 0 : char path[ PATH_MAX ];
54 0 : FD_TEST( fd_cstr_printf_check( path, PATH_MAX, NULL, "/proc/%lu/status", pid ) );
55 :
56 0 : int fd = open( path, O_RDONLY|O_CLOEXEC );
57 0 : if( FD_UNLIKELY( -1==fd && errno==ENOENT ) ) return 0UL;
58 0 : if( FD_UNLIKELY( -1==fd ) ) FD_LOG_ERR(( "error opening `%s` (%i-%s)", path, errno, fd_io_strerror( errno ) ));
59 :
60 : /* status is small (~1.5 KiB); one read gets it all */
61 0 : char buf[ 4096 ];
62 0 : long sz;
63 0 : do {
64 0 : sz = read( fd, buf, sizeof(buf)-1UL );
65 0 : }while( FD_UNLIKELY( -1L==sz && errno==EINTR ) );
66 0 : if( FD_UNLIKELY( sz<0L ) ) FD_LOG_ERR(( "error reading `%s` (%i-%s)", path, errno, fd_io_strerror( errno ) ));
67 0 : if( FD_UNLIKELY( -1==close( fd ) ) ) FD_LOG_ERR(( "error closing `%s` (%i-%s)", path, errno, fd_io_strerror( errno ) ));
68 0 : buf[ sz ] = '\0';
69 :
70 0 : char const * line = strstr( buf, "\nHugetlbPages:" );
71 0 : if( FD_UNLIKELY( !line ) ) return ULONG_MAX;
72 0 : return strtoul( line+14UL, NULL, 10 );
73 0 : }
74 :
75 : /* proc_start_time reads /proc/<pid>/stat field 22 (starttime, clock
76 : ticks since boot), 0 if the process is gone. (pid, starttime)
77 : uniquely identifies a process; pids alone recycle. stat is world
78 : readable except under hidepid, where a 0 would silently skip the
79 : process; fail loudly instead (like every other /proc error here).
80 : The comm field can contain spaces and parens, so parse from the
81 : last ')'. */
82 :
83 : static ulong
84 0 : proc_start_time( ulong pid ) {
85 0 : char path[ PATH_MAX ];
86 0 : FD_TEST( fd_cstr_printf_check( path, PATH_MAX, NULL, "/proc/%lu/stat", pid ) );
87 :
88 0 : char buf[ 4096 ];
89 0 : int fd = open( path, O_RDONLY|O_CLOEXEC );
90 0 : if( FD_UNLIKELY( -1==fd && (errno==EACCES || errno==EPERM) ) )
91 0 : FD_LOG_ERR(( "error opening `%s` (%i-%s); /proc is restricted (hidepid?), run as root", path, errno, fd_io_strerror( errno ) ));
92 0 : if( FD_UNLIKELY( -1==fd ) ) return 0UL;
93 0 : long sz;
94 0 : do sz = read( fd, buf, sizeof(buf)-1UL ); while( FD_UNLIKELY( -1L==sz && errno==EINTR ) );
95 0 : if( FD_UNLIKELY( -1==close( fd ) ) ) FD_LOG_ERR(( "error closing `%s` (%i-%s)", path, errno, fd_io_strerror( errno ) ));
96 0 : if( FD_UNLIKELY( sz<=0L ) ) return 0UL;
97 0 : buf[ sz ] = '\0';
98 :
99 0 : char * p = strrchr( buf, ')' );
100 0 : if( FD_UNLIKELY( !p ) ) return 0UL;
101 :
102 0 : ulong start_time = 0UL;
103 0 : if( FD_UNLIKELY( 1!=sscanf( p+1UL, " %*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %*s %lu", &start_time ) ) ) return 0UL;
104 0 : return start_time;
105 0 : }
106 :
107 0 : #define KILL_NO ( 0)
108 0 : #define KILL_IS_FIREDANCER (-3)
109 0 : #define KILL_IS_FIREDANCER_DEV (-4)
110 0 : #define KILL_HAS_WORKSPACE_FD (-5)
111 0 : #define KILL_HAS_HUGEPAGES (-6)
112 0 : #define KILL_CANT_INSPECT (-7) /* check ran unprivileged; init (as root) decides */
113 :
114 : static char const *
115 0 : kill_reason( int err ) {
116 0 : switch( err ) {
117 0 : case KILL_IS_FIREDANCER: return "is firedancer";
118 0 : case KILL_IS_FIREDANCER_DEV: return "is firedancer-dev";
119 0 : case KILL_HAS_WORKSPACE_FD: return "has a workspace file descriptor open";
120 0 : case KILL_HAS_HUGEPAGES: return "has anonymous hugepages mapped";
121 0 : case KILL_CANT_INSPECT: return "cannot be inspected without root";
122 0 : default: FD_LOG_ERR(( "unexpected error code" ));
123 0 : }
124 0 : }
125 :
126 : static int
127 0 : check_binary( ulong pid ) {
128 : /* Match the executable name via one readlink rather than reading
129 : /proc/<pid>/cmdline for every process. */
130 0 : char exe_path[ PATH_MAX ];
131 0 : FD_TEST( fd_cstr_printf_check( exe_path, PATH_MAX, NULL, "/proc/%lu/exe", pid ) );
132 0 : char exe[ PATH_MAX ];
133 0 : long exe_len = readlink( exe_path, exe, PATH_MAX-1UL );
134 0 : if( FD_UNLIKELY( exe_len<0L && errno==EACCES ) ) {
135 : /* Unprivileged check of another user's process: exe is protected
136 : but cmdline is world-readable; match its suffix like the
137 : historical scan. */
138 0 : char proc_cmdline[ PATH_MAX ];
139 0 : cmdline( proc_cmdline, PATH_MAX, pid );
140 0 : ulong cmdline_len = strlen( proc_cmdline );
141 0 : exe_len = (long)fd_ulong_min( cmdline_len, PATH_MAX-1UL );
142 0 : fd_memcpy( exe, proc_cmdline, (ulong)exe_len );
143 0 : } else if( FD_UNLIKELY( exe_len<0L ) ) {
144 0 : return KILL_NO; /* kernel thread or gone */
145 0 : }
146 0 : exe[ exe_len ] = '\0';
147 : /* An overwritten binary reads "/path/binary (deleted)" */
148 0 : if( FD_UNLIKELY( exe_len>=10L && !strcmp( exe+exe_len-10L, " (deleted)" ) ) ) exe[ exe_len-10L ] = '\0';
149 0 : char const * base = strrchr( exe, '/' );
150 0 : base = base ? base+1UL : exe;
151 :
152 0 : static char const * binaries[] = { "firedancer", "firedancer-dev" };
153 0 : static int const errors[] = { KILL_IS_FIREDANCER, KILL_IS_FIREDANCER_DEV };
154 0 : for( ulong i=0UL; i<sizeof(binaries)/sizeof(binaries[0]); i++ ) {
155 0 : if( FD_UNLIKELY( !strcmp( base, binaries[ i ] ) ) ) return errors[ i ];
156 0 : }
157 0 : return KILL_NO;
158 0 : }
159 :
160 : static int
161 : check_hugepages( config_t const * config,
162 0 : ulong pid ) {
163 : /* No hugetlb usage -> cannot hold a workspace mapping or anonymous
164 : hugepages, skip the expensive maps scan. */
165 0 : if( FD_LIKELY( !hugetlb_kib( pid ) ) ) return KILL_NO;
166 :
167 0 : int result = KILL_NO;
168 :
169 0 : char path[ PATH_MAX ];
170 0 : FD_TEST( fd_cstr_printf_check( path, PATH_MAX, NULL, "/proc/%lu/maps", pid ) );
171 0 : FILE * fp = fopen( path, "r" );
172 0 : if( FD_UNLIKELY( !fp && errno==ENOENT ) ) return KILL_NO;
173 0 : else if( FD_UNLIKELY( !fp && errno==EACCES ) ) return KILL_CANT_INSPECT; /* unprivileged check; hugetlb user needs root inspection */
174 0 : else if( FD_UNLIKELY( !fp ) ) FD_LOG_ERR(( "error opening `%s` (%i-%s)", path, errno, fd_io_strerror( errno ) ));
175 :
176 0 : int maybe_huge = 0;
177 0 : char line[ 4096 ];
178 0 : while( FD_LIKELY( fgets( line, 4096, fp ) ) ) {
179 0 : if( FD_UNLIKELY( strlen( line ) == 4095 ) ) FD_LOG_ERR(( "line too long in `%s`", path ));
180 0 : if( FD_UNLIKELY( strstr( line, config->hugetlbfs.gigantic_page_mount_path ) ||
181 0 : strstr( line, config->hugetlbfs.huge_page_mount_path ) ) ) {
182 0 : result = KILL_HAS_WORKSPACE_FD;
183 0 : break;
184 0 : }
185 :
186 0 : if( FD_UNLIKELY( strstr( line, "anon_hugepage" ) || strstr( line, "memfd:" ) ) ) maybe_huge = 1;
187 0 : }
188 0 : if( FD_UNLIKELY( ferror( fp ) ) )
189 0 : FD_LOG_ERR(( "error reading `%s` (%i-%s)", path, errno, fd_io_strerror( errno ) ));
190 0 : if( FD_LIKELY( fclose( fp ) ) )
191 0 : FD_LOG_ERR(( "error closing `%s` (%i-%s)", path, errno, fd_io_strerror( errno ) ));
192 :
193 0 : if( FD_UNLIKELY( result ) ) return result;
194 :
195 : /* No hugepage mappings in maps -> cannot have anonymous hugepages, so
196 : skip the expensive numa_maps read entirely. */
197 0 : if( FD_LIKELY( !maybe_huge ) ) return KILL_NO;
198 :
199 0 : FD_TEST( fd_cstr_printf_check( path, PATH_MAX, NULL, "/proc/%lu/numa_maps", pid ) );
200 0 : fp = fopen( path, "r" );
201 0 : if( FD_UNLIKELY( !fp && errno==ENOENT ) ) return KILL_NO;
202 0 : else if( FD_UNLIKELY( !fp ) ) FD_LOG_ERR(( "error opening `%s` (%i-%s)", path, errno, fd_io_strerror( errno ) ));
203 :
204 0 : while( FD_LIKELY( fgets( line, 4096, fp ) ) ) {
205 0 : if( FD_UNLIKELY( strlen( line ) == 4095 ) ) FD_LOG_ERR(( "line too long in `%s`", path ));
206 0 : if( FD_UNLIKELY( strstr( line, "huge" ) && strstr( line, "anon" ) ) ) {
207 0 : result = KILL_HAS_HUGEPAGES;
208 0 : break;
209 0 : }
210 0 : }
211 0 : if( FD_UNLIKELY( ferror( fp ) ) )
212 0 : FD_LOG_ERR(( "error reading `%s` (%i-%s)", path, errno, fd_io_strerror( errno ) ));
213 0 : if( FD_LIKELY( fclose( fp ) ) )
214 0 : FD_LOG_ERR(( "error closing `%s` (%i-%s)", path, errno, fd_io_strerror( errno ) ));
215 :
216 0 : return result;
217 0 : }
218 :
219 : static int
220 : check_kill( config_t const * config,
221 0 : ulong pid ) {
222 0 : int err = check_binary( pid );
223 0 : if( FD_UNLIKELY( err ) ) return err;
224 0 : return check_hugepages( config, pid );
225 0 : }
226 :
227 : static void
228 : wait_dead( long started,
229 : ulong pid,
230 0 : ulong start_time ) {
231 : /* We need to do this to prevent a race condition, since kill(SIGKILL) returns
232 : before the kernel actually terminates and reclaims the resources from the
233 : process. A task blocked in an uninterruptible syscall (e.g. an
234 : fsync queued behind heavy writeback) does not die until the
235 : syscall completes, which can take seconds. */
236 0 : int notified = 0;
237 0 : while( 1 ) {
238 : /* starttime mismatch means the pid was recycled: ours is dead */
239 0 : if( FD_LIKELY( proc_start_time( pid )!=start_time ) ) return;
240 :
241 0 : long waited = fd_log_wallclock() - started;
242 0 : if( FD_UNLIKELY( waited>=(long)1e9 && !notified ) ) {
243 0 : FD_LOG_WARNING(( "waiting for killed process to exit %s(blocked in uninterruptible disk I/O)%s", fd_log_style_dim(), fd_log_style_normal() ));
244 0 : notified = 1;
245 0 : }
246 0 : if( FD_UNLIKELY( waited >= (long)5e9 ) ) FD_LOG_ERR(( "waited too long for process to exit" ));
247 0 : }
248 0 : }
249 :
250 0 : #define SCAN_THREADS (16UL)
251 : #define SCAN_PIDS_MAX (65536UL)
252 : #define MATCHED_MAX (1024UL)
253 :
254 : struct kill_scan {
255 : config_t const * config;
256 : ulong const * pids;
257 : ulong pid_cnt;
258 : ulong idx; /* this worker's stride offset */
259 : ulong matched [ MATCHED_MAX ];
260 : ulong matched_start[ MATCHED_MAX ];
261 : int match_err [ MATCHED_MAX ];
262 : ulong matched_cnt;
263 : };
264 :
265 : static void *
266 0 : kill_scan_thread( void * arg ) {
267 0 : struct kill_scan * scan = arg;
268 0 : for( ulong i=scan->idx; i<scan->pid_cnt; i+=SCAN_THREADS ) {
269 : /* Bind identity before inspecting: (pid, starttime) is unique,
270 : pids recycle. 0 (gone) drops the match, sidestepping reuse. */
271 0 : ulong start_time = proc_start_time( scan->pids[ i ] );
272 0 : if( FD_UNLIKELY( !start_time ) ) continue;
273 0 : int err = check_kill( scan->config, scan->pids[ i ] );
274 0 : if( FD_UNLIKELY( err ) ) {
275 0 : if( FD_UNLIKELY( scan->matched_cnt==MATCHED_MAX ) ) FD_LOG_ERR(( "too many processes to kill" ));
276 0 : scan->match_err [ scan->matched_cnt ] = err;
277 0 : scan->matched_start[ scan->matched_cnt ] = start_time;
278 0 : scan->matched [ scan->matched_cnt++ ] = scan->pids[ i ];
279 0 : }
280 0 : }
281 0 : return NULL;
282 0 : }
283 :
284 : /* kill_scan runs check_kill over every pid in /proc on SCAN_THREADS
285 : threads (the per-process checks are independent kernel reads).
286 : Writes up to MATCHED_MAX (pid, starttime, reason) triples; returns
287 : the count. */
288 :
289 : static ulong
290 : kill_scan( config_t const * config,
291 : ulong matched[ static MATCHED_MAX ],
292 : ulong matched_start[ static MATCHED_MAX ],
293 0 : int match_err[ static MATCHED_MAX ] ) {
294 0 : DIR * dir = opendir( "/proc" );
295 0 : if( FD_UNLIKELY( !dir ) ) FD_LOG_ERR(( "error opening `/proc` (%i-%s)", errno, fd_io_strerror( errno ) ));
296 :
297 0 : static ulong pids[ SCAN_PIDS_MAX ];
298 0 : ulong matched_cnt = 0UL;
299 :
300 0 : int done = 0;
301 0 : while( !done ) {
302 0 : ulong pid_cnt = 0UL;
303 0 : for(;;) {
304 0 : errno = 0;
305 0 : struct dirent * entry = readdir( dir );
306 0 : if( FD_UNLIKELY( !entry ) ) { done = 1; break; }
307 0 : if( FD_UNLIKELY( entry->d_name[0] == '.' ) ) continue;
308 0 : char * endptr;
309 0 : ulong pid = strtoul( entry->d_name, &endptr, 10 );
310 0 : if( FD_UNLIKELY( *endptr || pid==(ulong)getpid() ) ) continue;
311 0 : pids[ pid_cnt++ ] = pid;
312 0 : if( FD_UNLIKELY( pid_cnt==SCAN_PIDS_MAX ) ) break;
313 0 : }
314 0 : if( FD_UNLIKELY( errno ) ) FD_LOG_ERR(( "readdir() (%i-%s)", errno, fd_io_strerror( errno ) ));
315 0 : if( FD_UNLIKELY( !pid_cnt ) ) break;
316 :
317 0 : static struct kill_scan scans[ SCAN_THREADS ];
318 0 : pthread_t threads[ SCAN_THREADS ];
319 0 : for( ulong t=0UL; t<SCAN_THREADS; t++ ) {
320 : /* Scalars only: a struct assignment would memset the ~20 KiB
321 : match arrays per thread per batch for nothing (matched_cnt
322 : bounds all reads). */
323 0 : scans[ t ].config = config;
324 0 : scans[ t ].pids = pids;
325 0 : scans[ t ].pid_cnt = pid_cnt;
326 0 : scans[ t ].idx = t;
327 0 : scans[ t ].matched_cnt = 0UL;
328 0 : if( FD_UNLIKELY( pthread_create( &threads[ t ], NULL, kill_scan_thread, &scans[ t ] ) ) ) FD_LOG_ERR(( "pthread_create failed" ));
329 0 : }
330 :
331 0 : for( ulong t=0UL; t<SCAN_THREADS; t++ ) {
332 0 : if( FD_UNLIKELY( pthread_join( threads[ t ], NULL ) ) ) FD_LOG_ERR(( "pthread_join failed" ));
333 0 : for( ulong i=0UL; i<scans[ t ].matched_cnt; i++ ) {
334 0 : if( FD_UNLIKELY( matched_cnt==MATCHED_MAX ) ) FD_LOG_ERR(( "too many processes to kill" ));
335 0 : match_err [ matched_cnt ] = scans[ t ].match_err [ i ];
336 0 : matched_start[ matched_cnt ] = scans[ t ].matched_start[ i ];
337 0 : matched [ matched_cnt++ ] = scans[ t ].matched [ i ];
338 0 : }
339 0 : }
340 0 : }
341 :
342 0 : if( FD_UNLIKELY( -1==closedir( dir ) ) ) FD_LOG_ERR(( "closedir (%i-%s)", errno, fd_io_strerror( errno ) ));
343 0 : return matched_cnt;
344 0 : }
345 :
346 : static void
347 0 : init( config_t const * config ) {
348 0 : ulong matched[ MATCHED_MAX ];
349 0 : ulong matched_start[ MATCHED_MAX ];
350 0 : int match_err[ MATCHED_MAX ];
351 0 : ulong matched_cnt = kill_scan( config, matched, matched_start, match_err );
352 :
353 0 : for( ulong i=0UL; i<matched_cnt; i++ ) {
354 : /* Revalidate identity right before the signal: if the pid was
355 : recycled since the scan, the new owner is not our target. */
356 0 : if( FD_UNLIKELY( proc_start_time( matched[ i ] )!=matched_start[ i ] ) ) continue;
357 0 : char proc_cmdline[ PATH_MAX ];
358 0 : cmdline( proc_cmdline, PATH_MAX, matched[ i ] );
359 0 : FD_LOG_NOTICE(( "killing process `%s` (%lu): %s", proc_cmdline, matched[ i ], kill_reason( match_err[ i ] ) ));
360 0 : if( FD_UNLIKELY( -1==kill( (int)matched[ i ], SIGKILL ) && errno!=ESRCH ) ) FD_LOG_ERR(( "kill failed (%i-%s)", errno, fd_io_strerror( errno ) ));
361 0 : }
362 :
363 0 : long started = fd_log_wallclock();
364 0 : for( ulong i=0; i<matched_cnt; i++ ) wait_dead( started, matched[ i ], matched_start[ i ] );
365 0 : }
366 :
367 : static configure_result_t
368 : check( config_t const * config,
369 0 : int check_type FD_PARAM_UNUSED ) {
370 0 : ulong matched[ MATCHED_MAX ];
371 0 : ulong matched_start[ MATCHED_MAX ];
372 0 : int match_err[ MATCHED_MAX ];
373 0 : ulong matched_cnt = kill_scan( config, matched, matched_start, match_err );
374 :
375 0 : if( FD_UNLIKELY( matched_cnt ) ) {
376 0 : char proc_cmdline[ PATH_MAX ];
377 0 : cmdline( proc_cmdline, PATH_MAX, matched[ 0 ] );
378 0 : NOT_CONFIGURED( "process `%s` (%lu) %s", proc_cmdline, matched[ 0 ], kill_reason( match_err[ 0 ] ) );
379 0 : }
380 :
381 0 : CONFIGURE_OK();
382 0 : }
383 :
384 : configure_stage_t fd_cfg_stage_kill = {
385 : .name = NAME,
386 : .always_recreate = 0,
387 : .enabled = NULL,
388 : .init_perm = init_perm,
389 : .fini_perm = NULL,
390 : .init = init,
391 : .fini = NULL,
392 : .check = check,
393 : };
394 :
395 : #undef NAME
|