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_FDDEV (-1)
109 0 : #define KILL_IS_FDCTL (-2)
110 0 : #define KILL_IS_FIREDANCER (-3)
111 0 : #define KILL_IS_FIREDANCER_DEV (-4)
112 0 : #define KILL_HAS_WORKSPACE_FD (-5)
113 0 : #define KILL_HAS_HUGEPAGES (-6)
114 0 : #define KILL_CANT_INSPECT (-7) /* check ran unprivileged; init (as root) decides */
115 :
116 : static char const *
117 0 : kill_reason( int err ) {
118 0 : switch( err ) {
119 0 : case KILL_IS_FDDEV: return "is fddev";
120 0 : case KILL_IS_FDCTL: return "is fdctl";
121 0 : case KILL_IS_FIREDANCER: return "is firedancer";
122 0 : case KILL_IS_FIREDANCER_DEV: return "is firedancer-dev";
123 0 : case KILL_HAS_WORKSPACE_FD: return "has a workspace file descriptor open";
124 0 : case KILL_HAS_HUGEPAGES: return "has anonymous hugepages mapped";
125 0 : case KILL_CANT_INSPECT: return "cannot be inspected without root";
126 0 : default: FD_LOG_ERR(( "unexpected error code" ));
127 0 : }
128 0 : }
129 :
130 : static int
131 0 : check_binary( ulong pid ) {
132 : /* Match the executable name via one readlink rather than reading
133 : /proc/<pid>/cmdline for every process. */
134 0 : char exe_path[ PATH_MAX ];
135 0 : FD_TEST( fd_cstr_printf_check( exe_path, PATH_MAX, NULL, "/proc/%lu/exe", pid ) );
136 0 : char exe[ PATH_MAX ];
137 0 : long exe_len = readlink( exe_path, exe, PATH_MAX-1UL );
138 0 : if( FD_UNLIKELY( exe_len<0L && errno==EACCES ) ) {
139 : /* Unprivileged check of another user's process: exe is protected
140 : but cmdline is world-readable; match its suffix like the
141 : historical scan. */
142 0 : char proc_cmdline[ PATH_MAX ];
143 0 : cmdline( proc_cmdline, PATH_MAX, pid );
144 0 : ulong cmdline_len = strlen( proc_cmdline );
145 0 : exe_len = (long)fd_ulong_min( cmdline_len, PATH_MAX-1UL );
146 0 : fd_memcpy( exe, proc_cmdline, (ulong)exe_len );
147 0 : } else if( FD_UNLIKELY( exe_len<0L ) ) {
148 0 : return KILL_NO; /* kernel thread or gone */
149 0 : }
150 0 : exe[ exe_len ] = '\0';
151 : /* An overwritten binary reads "/path/binary (deleted)" */
152 0 : if( FD_UNLIKELY( exe_len>=10L && !strcmp( exe+exe_len-10L, " (deleted)" ) ) ) exe[ exe_len-10L ] = '\0';
153 0 : char const * base = strrchr( exe, '/' );
154 0 : base = base ? base+1UL : exe;
155 :
156 0 : static char const * binaries[] = { "fddev", "fdctl", "firedancer", "firedancer-dev" };
157 0 : static int const errors[] = { KILL_IS_FDDEV, KILL_IS_FDCTL, KILL_IS_FIREDANCER, KILL_IS_FIREDANCER_DEV };
158 0 : for( ulong i=0UL; i<sizeof(binaries)/sizeof(binaries[0]); i++ ) {
159 0 : if( FD_UNLIKELY( !strcmp( base, binaries[ i ] ) ) ) return errors[ i ];
160 0 : }
161 0 : return KILL_NO;
162 0 : }
163 :
164 : static int
165 : check_hugepages( config_t const * config,
166 0 : ulong pid ) {
167 : /* No hugetlb usage -> cannot hold a workspace mapping or anonymous
168 : hugepages, skip the expensive maps scan. */
169 0 : if( FD_LIKELY( !hugetlb_kib( pid ) ) ) return KILL_NO;
170 :
171 0 : int result = KILL_NO;
172 :
173 0 : char path[ PATH_MAX ];
174 0 : FD_TEST( fd_cstr_printf_check( path, PATH_MAX, NULL, "/proc/%lu/maps", pid ) );
175 0 : FILE * fp = fopen( path, "r" );
176 0 : if( FD_UNLIKELY( !fp && errno==ENOENT ) ) return KILL_NO;
177 0 : else if( FD_UNLIKELY( !fp && errno==EACCES ) ) return KILL_CANT_INSPECT; /* unprivileged check; hugetlb user needs root inspection */
178 0 : else if( FD_UNLIKELY( !fp ) ) FD_LOG_ERR(( "error opening `%s` (%i-%s)", path, errno, fd_io_strerror( errno ) ));
179 :
180 0 : int maybe_huge = 0;
181 0 : char line[ 4096 ];
182 0 : while( FD_LIKELY( fgets( line, 4096, fp ) ) ) {
183 0 : if( FD_UNLIKELY( strlen( line ) == 4095 ) ) FD_LOG_ERR(( "line too long in `%s`", path ));
184 0 : if( FD_UNLIKELY( strstr( line, config->hugetlbfs.gigantic_page_mount_path ) ||
185 0 : strstr( line, config->hugetlbfs.huge_page_mount_path ) ) ) {
186 0 : result = KILL_HAS_WORKSPACE_FD;
187 0 : break;
188 0 : }
189 :
190 0 : if( FD_UNLIKELY( strstr( line, "anon_hugepage" ) || strstr( line, "memfd:" ) ) ) maybe_huge = 1;
191 0 : }
192 0 : if( FD_UNLIKELY( ferror( fp ) ) )
193 0 : FD_LOG_ERR(( "error reading `%s` (%i-%s)", path, errno, fd_io_strerror( errno ) ));
194 0 : if( FD_LIKELY( fclose( fp ) ) )
195 0 : FD_LOG_ERR(( "error closing `%s` (%i-%s)", path, errno, fd_io_strerror( errno ) ));
196 :
197 0 : if( FD_UNLIKELY( result ) ) return result;
198 :
199 : /* No hugepage mappings in maps -> cannot have anonymous hugepages, so
200 : skip the expensive numa_maps read entirely. */
201 0 : if( FD_LIKELY( !maybe_huge ) ) return KILL_NO;
202 :
203 0 : FD_TEST( fd_cstr_printf_check( path, PATH_MAX, NULL, "/proc/%lu/numa_maps", pid ) );
204 0 : fp = fopen( path, "r" );
205 0 : if( FD_UNLIKELY( !fp && errno==ENOENT ) ) return KILL_NO;
206 0 : else if( FD_UNLIKELY( !fp ) ) FD_LOG_ERR(( "error opening `%s` (%i-%s)", path, errno, fd_io_strerror( errno ) ));
207 :
208 0 : while( FD_LIKELY( fgets( line, 4096, fp ) ) ) {
209 0 : if( FD_UNLIKELY( strlen( line ) == 4095 ) ) FD_LOG_ERR(( "line too long in `%s`", path ));
210 0 : if( FD_UNLIKELY( strstr( line, "huge" ) && strstr( line, "anon" ) ) ) {
211 0 : result = KILL_HAS_HUGEPAGES;
212 0 : break;
213 0 : }
214 0 : }
215 0 : if( FD_UNLIKELY( ferror( fp ) ) )
216 0 : FD_LOG_ERR(( "error reading `%s` (%i-%s)", path, errno, fd_io_strerror( errno ) ));
217 0 : if( FD_LIKELY( fclose( fp ) ) )
218 0 : FD_LOG_ERR(( "error closing `%s` (%i-%s)", path, errno, fd_io_strerror( errno ) ));
219 :
220 0 : return result;
221 0 : }
222 :
223 : static int
224 : check_kill( config_t const * config,
225 0 : ulong pid ) {
226 0 : int err = check_binary( pid );
227 0 : if( FD_UNLIKELY( err ) ) return err;
228 0 : return check_hugepages( config, pid );
229 0 : }
230 :
231 : static void
232 : wait_dead( long started,
233 : ulong pid,
234 0 : ulong start_time ) {
235 : /* We need to do this to prevent a race condition, since kill(SIGKILL) returns
236 : before the kernel actually terminates and reclaims the resources from the
237 : process. A task blocked in an uninterruptible syscall (e.g. an
238 : fsync queued behind heavy writeback) does not die until the
239 : syscall completes, which can take seconds. */
240 0 : int notified = 0;
241 0 : while( 1 ) {
242 : /* starttime mismatch means the pid was recycled: ours is dead */
243 0 : if( FD_LIKELY( proc_start_time( pid )!=start_time ) ) return;
244 :
245 0 : long waited = fd_log_wallclock() - started;
246 0 : if( FD_UNLIKELY( waited>=(long)1e9 && !notified ) ) {
247 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() ));
248 0 : notified = 1;
249 0 : }
250 0 : if( FD_UNLIKELY( waited >= (long)5e9 ) ) FD_LOG_ERR(( "waited too long for process to exit" ));
251 0 : }
252 0 : }
253 :
254 0 : #define SCAN_THREADS (16UL)
255 : #define SCAN_PIDS_MAX (65536UL)
256 : #define MATCHED_MAX (1024UL)
257 :
258 : struct kill_scan {
259 : config_t const * config;
260 : ulong const * pids;
261 : ulong pid_cnt;
262 : ulong idx; /* this worker's stride offset */
263 : ulong matched [ MATCHED_MAX ];
264 : ulong matched_start[ MATCHED_MAX ];
265 : int match_err [ MATCHED_MAX ];
266 : ulong matched_cnt;
267 : };
268 :
269 : static void *
270 0 : kill_scan_thread( void * arg ) {
271 0 : struct kill_scan * scan = arg;
272 0 : for( ulong i=scan->idx; i<scan->pid_cnt; i+=SCAN_THREADS ) {
273 : /* Bind identity before inspecting: (pid, starttime) is unique,
274 : pids recycle. 0 (gone) drops the match, sidestepping reuse. */
275 0 : ulong start_time = proc_start_time( scan->pids[ i ] );
276 0 : if( FD_UNLIKELY( !start_time ) ) continue;
277 0 : int err = check_kill( scan->config, scan->pids[ i ] );
278 0 : if( FD_UNLIKELY( err ) ) {
279 0 : if( FD_UNLIKELY( scan->matched_cnt==MATCHED_MAX ) ) FD_LOG_ERR(( "too many processes to kill" ));
280 0 : scan->match_err [ scan->matched_cnt ] = err;
281 0 : scan->matched_start[ scan->matched_cnt ] = start_time;
282 0 : scan->matched [ scan->matched_cnt++ ] = scan->pids[ i ];
283 0 : }
284 0 : }
285 0 : return NULL;
286 0 : }
287 :
288 : /* kill_scan runs check_kill over every pid in /proc on SCAN_THREADS
289 : threads (the per-process checks are independent kernel reads).
290 : Writes up to MATCHED_MAX (pid, starttime, reason) triples; returns
291 : the count. */
292 :
293 : static ulong
294 : kill_scan( config_t const * config,
295 : ulong matched[ static MATCHED_MAX ],
296 : ulong matched_start[ static MATCHED_MAX ],
297 0 : int match_err[ static MATCHED_MAX ] ) {
298 0 : DIR * dir = opendir( "/proc" );
299 0 : if( FD_UNLIKELY( !dir ) ) FD_LOG_ERR(( "error opening `/proc` (%i-%s)", errno, fd_io_strerror( errno ) ));
300 :
301 0 : static ulong pids[ SCAN_PIDS_MAX ];
302 0 : ulong matched_cnt = 0UL;
303 :
304 0 : int done = 0;
305 0 : while( !done ) {
306 0 : ulong pid_cnt = 0UL;
307 0 : for(;;) {
308 0 : errno = 0;
309 0 : struct dirent * entry = readdir( dir );
310 0 : if( FD_UNLIKELY( !entry ) ) { done = 1; break; }
311 0 : if( FD_UNLIKELY( entry->d_name[0] == '.' ) ) continue;
312 0 : char * endptr;
313 0 : ulong pid = strtoul( entry->d_name, &endptr, 10 );
314 0 : if( FD_UNLIKELY( *endptr || pid==(ulong)getpid() ) ) continue;
315 0 : pids[ pid_cnt++ ] = pid;
316 0 : if( FD_UNLIKELY( pid_cnt==SCAN_PIDS_MAX ) ) break;
317 0 : }
318 0 : if( FD_UNLIKELY( errno ) ) FD_LOG_ERR(( "readdir() (%i-%s)", errno, fd_io_strerror( errno ) ));
319 0 : if( FD_UNLIKELY( !pid_cnt ) ) break;
320 :
321 0 : static struct kill_scan scans[ SCAN_THREADS ];
322 0 : pthread_t threads[ SCAN_THREADS ];
323 0 : for( ulong t=0UL; t<SCAN_THREADS; t++ ) {
324 : /* Scalars only: a struct assignment would memset the ~20 KiB
325 : match arrays per thread per batch for nothing (matched_cnt
326 : bounds all reads). */
327 0 : scans[ t ].config = config;
328 0 : scans[ t ].pids = pids;
329 0 : scans[ t ].pid_cnt = pid_cnt;
330 0 : scans[ t ].idx = t;
331 0 : scans[ t ].matched_cnt = 0UL;
332 0 : if( FD_UNLIKELY( pthread_create( &threads[ t ], NULL, kill_scan_thread, &scans[ t ] ) ) ) FD_LOG_ERR(( "pthread_create failed" ));
333 0 : }
334 :
335 0 : for( ulong t=0UL; t<SCAN_THREADS; t++ ) {
336 0 : if( FD_UNLIKELY( pthread_join( threads[ t ], NULL ) ) ) FD_LOG_ERR(( "pthread_join failed" ));
337 0 : for( ulong i=0UL; i<scans[ t ].matched_cnt; i++ ) {
338 0 : if( FD_UNLIKELY( matched_cnt==MATCHED_MAX ) ) FD_LOG_ERR(( "too many processes to kill" ));
339 0 : match_err [ matched_cnt ] = scans[ t ].match_err [ i ];
340 0 : matched_start[ matched_cnt ] = scans[ t ].matched_start[ i ];
341 0 : matched [ matched_cnt++ ] = scans[ t ].matched [ i ];
342 0 : }
343 0 : }
344 0 : }
345 :
346 0 : if( FD_UNLIKELY( -1==closedir( dir ) ) ) FD_LOG_ERR(( "closedir (%i-%s)", errno, fd_io_strerror( errno ) ));
347 0 : return matched_cnt;
348 0 : }
349 :
350 : static void
351 0 : init( config_t const * config ) {
352 0 : ulong matched[ MATCHED_MAX ];
353 0 : ulong matched_start[ MATCHED_MAX ];
354 0 : int match_err[ MATCHED_MAX ];
355 0 : ulong matched_cnt = kill_scan( config, matched, matched_start, match_err );
356 :
357 0 : for( ulong i=0UL; i<matched_cnt; i++ ) {
358 : /* Revalidate identity right before the signal: if the pid was
359 : recycled since the scan, the new owner is not our target. */
360 0 : if( FD_UNLIKELY( proc_start_time( matched[ i ] )!=matched_start[ i ] ) ) continue;
361 0 : char proc_cmdline[ PATH_MAX ];
362 0 : cmdline( proc_cmdline, PATH_MAX, matched[ i ] );
363 0 : FD_LOG_NOTICE(( "killing process `%s` (%lu): %s", proc_cmdline, matched[ i ], kill_reason( match_err[ i ] ) ));
364 0 : if( FD_UNLIKELY( -1==kill( (int)matched[ i ], SIGKILL ) && errno!=ESRCH ) ) FD_LOG_ERR(( "kill failed (%i-%s)", errno, fd_io_strerror( errno ) ));
365 0 : }
366 :
367 0 : long started = fd_log_wallclock();
368 0 : for( ulong i=0; i<matched_cnt; i++ ) wait_dead( started, matched[ i ], matched_start[ i ] );
369 0 : }
370 :
371 : static configure_result_t
372 : check( config_t const * config,
373 0 : int check_type FD_PARAM_UNUSED ) {
374 0 : ulong matched[ MATCHED_MAX ];
375 0 : ulong matched_start[ MATCHED_MAX ];
376 0 : int match_err[ MATCHED_MAX ];
377 0 : ulong matched_cnt = kill_scan( config, matched, matched_start, match_err );
378 :
379 0 : if( FD_UNLIKELY( matched_cnt ) ) {
380 0 : char proc_cmdline[ PATH_MAX ];
381 0 : cmdline( proc_cmdline, PATH_MAX, matched[ 0 ] );
382 0 : NOT_CONFIGURED( "process `%s` (%lu) %s", proc_cmdline, matched[ 0 ], kill_reason( match_err[ 0 ] ) );
383 0 : }
384 :
385 0 : CONFIGURE_OK();
386 0 : }
387 :
388 : configure_stage_t fd_cfg_stage_kill = {
389 : .name = NAME,
390 : .always_recreate = 0,
391 : .enabled = NULL,
392 : .init_perm = init_perm,
393 : .fini_perm = NULL,
394 : .init = init,
395 : .fini = NULL,
396 : .check = check,
397 : };
398 :
399 : #undef NAME
|