run-bounded.py (2715B)
1 #!/usr/bin/env python3 2 """Run site checks with per-process data limits, aggregate RSS guard and deadline. 3 4 Use inside a Nix tooling shell. Chromium reserves a huge PROT_NONE address space, 5 so bound writable data rather than virtual address reservations. RLIMIT_DATA also 6 covers anonymous writable mmap on Linux. The aggregate /proc guard covers the 7 whole process group, including Chromium and Workers; it is sampled, not a cgroup. 8 """ 9 import argparse 10 import os 11 from pathlib import Path 12 import resource 13 import signal 14 import subprocess 15 import time 16 17 18 def main(): 19 p = argparse.ArgumentParser(description=__doc__) 20 p.add_argument('--seconds', type=int, default=300) 21 p.add_argument('--memory-mib', type=int, default=4096) 22 p.add_argument('command', nargs=argparse.REMAINDER) 23 args = p.parse_args() 24 command = args.command[1:] if args.command[:1] == ['--'] else args.command 25 if not command or args.seconds <= 0 or args.memory_mib <= 0: 26 p.error('positive limits and a command are required') 27 limit = args.memory_mib * 1024 * 1024 28 29 def limits(): 30 resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) 31 resource.setrlimit(resource.RLIMIT_DATA, (limit, limit)) 32 33 child = subprocess.Popen(command, start_new_session=True, preexec_fn=limits) 34 started = time.monotonic() 35 peak = 0 36 reason = None 37 try: 38 while child.poll() is None: 39 resident = 0 40 for path in Path('/proc').glob('[0-9]*/status'): 41 try: 42 fields = dict(line.split(':', 1) for line in path.read_text().splitlines() if ':' in line) 43 # /proc can be mounted from the outer PID namespace. The 44 # last NSpgid is the ID seen by this sandbox's subprocess. 45 if int(fields.get('NSpgid', '-1').split()[-1]) == child.pid: 46 resident += int(fields.get('VmRSS', '0 kB').split()[0]) * 1024 47 except (OSError, ValueError, IndexError): 48 pass 49 peak = max(peak, resident) 50 if resident > limit: 51 reason = 'aggregate RSS guard exceeded' 52 break 53 if time.monotonic() - started > args.seconds: 54 reason = 'whole-job deadline exceeded' 55 break 56 time.sleep(.1) 57 finally: 58 try: 59 os.killpg(child.pid, signal.SIGKILL) 60 except ProcessLookupError: 61 pass 62 child.wait() 63 print(f'Bounded check: peak sampled group RSS {peak / 1024**2:.1f} MiB; {time.monotonic()-started:.1f}s; {reason or "completed"}', flush=True) 64 raise SystemExit(1 if reason else child.returncode) 65 66 67 if __name__ == '__main__': 68 main()