"""Capability-free Windows AppContainer process launcher. The HTTP broker must stay outside this boundary. Only approved immutable runtime, parser source and model directories receive read/execute access. Documents travel through anonymous pipes, never a temporary plaintext file. No network capability or loopback exemption is assigned. Windows supplies the package-private storage. """ import ctypes import hashlib import os import subprocess import sys import threading from pathlib import Path from ctypes import wintypes as W class SandboxError(RuntimeError): pass def _verify_read_acl(path, sid): """Read the DACL directly; startup never needs WRITE_DAC or icacls.""" a = ctypes.WinDLL('advapi32', use_last_error=True) k = ctypes.WinDLL('kernel32', use_last_error=True) a.GetNamedSecurityInfoW.argtypes = [W.LPWSTR, W.DWORD, W.DWORD, ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p)] a.GetNamedSecurityInfoW.restype = W.DWORD a.GetAce.argtypes = [ctypes.c_void_p, W.DWORD, ctypes.POINTER(ctypes.c_void_p)] a.GetAce.restype = W.BOOL a.EqualSid.argtypes = [ctypes.c_void_p, ctypes.c_void_p]; a.EqualSid.restype = W.BOOL k.LocalFree.argtypes = [ctypes.c_void_p]; k.LocalFree.restype = ctypes.c_void_p dacl = ctypes.c_void_p(); descriptor = ctypes.c_void_p() if a.GetNamedSecurityInfoW(str(path), 1, 4, None, None, ctypes.byref(dacl), None, ctypes.byref(descriptor)): raise SandboxError('Approved runtime ACL could not be inspected') try: if not dacl: raise SandboxError('Approved runtime has unrestricted access') count = ctypes.c_ushort.from_address(dacl.value + 4).value allow = 0 required = 0x1200a9 # FILE_GENERIC_READ | FILE_GENERIC_EXECUTE for index in range(count): ace = ctypes.c_void_p() if not a.GetAce(dacl, index, ctypes.byref(ace)): raise SandboxError('Runtime ACL is invalid') kind = ctypes.c_ubyte.from_address(ace.value).value flags = ctypes.c_ubyte.from_address(ace.value + 1).value if flags & 8: continue # INHERIT_ONLY does not apply to this object. mask = ctypes.c_uint32.from_address(ace.value + 4).value # Conservative: a relevant deny ACE requires administrator review. if kind == 1 and mask & (required | 0x90000000): raise SandboxError('Approved runtime has conflicting deny permissions') if kind == 0 and a.EqualSid(ctypes.c_void_p(ace.value + 8), sid): if mask & 0x10000000: mask |= 0x1f01ff if mask & 0x80000000: mask |= 0x120089 if mask & 0x20000000: mask |= 0x1200a0 allow |= mask if allow & required != required: raise SandboxError('Runtime ACL provisioning is required before inspector startup') if allow & 0x400d0116: # write/append/EA/attributes/delete/WRITE_DAC/WRITE_OWNER raise SandboxError('AppContainer runtime access must be read-only') finally: k.LocalFree(descriptor) def _api(): if sys.platform != 'win32': raise SandboxError('AppContainer requires Windows') k = ctypes.WinDLL('kernel32', use_last_error=True) u = ctypes.WinDLL('userenv', use_last_error=True) a = ctypes.WinDLL('advapi32', use_last_error=True) for name, args, result in [ ('CloseHandle', [W.HANDLE], W.BOOL), ('CreatePipe', [ctypes.POINTER(W.HANDLE), ctypes.POINTER(W.HANDLE), ctypes.c_void_p, W.DWORD], W.BOOL), ('SetHandleInformation', [W.HANDLE, W.DWORD, W.DWORD], W.BOOL), ('InitializeProcThreadAttributeList', [ctypes.c_void_p, W.DWORD, W.DWORD, ctypes.POINTER(ctypes.c_size_t)], W.BOOL), ('UpdateProcThreadAttribute', [ctypes.c_void_p, W.DWORD, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_void_p], W.BOOL), ('DeleteProcThreadAttributeList', [ctypes.c_void_p], None), ('CreateProcessW', [W.LPCWSTR, W.LPWSTR, ctypes.c_void_p, ctypes.c_void_p, W.BOOL, W.DWORD, ctypes.c_void_p, W.LPCWSTR, ctypes.c_void_p, ctypes.c_void_p], W.BOOL), ('WaitForSingleObject', [W.HANDLE, W.DWORD], W.DWORD), ('TerminateProcess', [W.HANDLE, W.UINT], W.BOOL), ('GetExitCodeProcess', [W.HANDLE, ctypes.POINTER(W.DWORD)], W.BOOL), ('LocalFree', [ctypes.c_void_p], ctypes.c_void_p), ('CreateJobObjectW', [ctypes.c_void_p, W.LPCWSTR], W.HANDLE), ('SetInformationJobObject', [W.HANDLE, ctypes.c_int, ctypes.c_void_p, W.DWORD], W.BOOL), ('AssignProcessToJobObject', [W.HANDLE, W.HANDLE], W.BOOL), ('ResumeThread', [W.HANDLE], W.DWORD), ]: fn = getattr(k, name); fn.argtypes = args; fn.restype = result u.CreateAppContainerProfile.argtypes = [W.LPCWSTR, W.LPCWSTR, W.LPCWSTR, ctypes.c_void_p, W.DWORD, ctypes.POINTER(ctypes.c_void_p)] u.CreateAppContainerProfile.restype = ctypes.c_long u.DeriveAppContainerSidFromAppContainerName.argtypes = [W.LPCWSTR, ctypes.POINTER(ctypes.c_void_p)] u.DeriveAppContainerSidFromAppContainerName.restype = ctypes.c_long a.ConvertSidToStringSidW.argtypes = [ctypes.c_void_p, ctypes.POINTER(W.LPWSTR)] a.ConvertSidToStringSidW.restype = W.BOOL a.FreeSid.argtypes = [ctypes.c_void_p]; a.FreeSid.restype = ctypes.c_void_p return k, u, a class AppContainer: """Broker-owned launcher. Caller supplies trusted deployment directories.""" def __init__(self, read_directories, *, provision=False): self.k, u, self.a = _api() roots_input = [Path(p).absolute() for p in read_directories] for root in roots_input: for component in [root, *root.parents]: if component.lstat().st_file_attributes & 0x400: raise SandboxError('Reparse point in approved sandbox root') roots = tuple(sorted({str(p.resolve(strict=True)) for p in roots_input})) if not roots or any(not Path(p).is_dir() or Path(p).parent == Path(p) for p in roots): raise SandboxError('Invalid approved sandbox runtime directories') self.name = 'SafetyNet.Parser.' + hashlib.sha256('\0'.join(roots).encode()).hexdigest()[:24] self.sid = ctypes.c_void_p() result = u.CreateAppContainerProfile(self.name, 'SafetyNet parser', 'Offline content inspection', None, 0, ctypes.byref(self.sid)) if result != 0 and u.DeriveAppContainerSidFromAppContainerName(self.name, ctypes.byref(self.sid)) != 0: raise SandboxError('AppContainer profile could not be established') sid_text = W.LPWSTR() if not self.a.ConvertSidToStringSidW(self.sid, ctypes.byref(sid_text)): raise SandboxError('AppContainer identity unavailable') try: self.sid_string = sid_text.value finally: self.k.LocalFree(sid_text) # Package SID grants are specific to this deployment, never Everyone or # ALL APPLICATION PACKAGES. No writable permission is added to source. for root in roots: # Reject junctions/symlinks before recursive grants; /L alone is not # sufficient evidence that every possible reparse target stays local. entries = [Path(root)] for directory, folders, files in os.walk(root, followlinks=False): for name in folders + files: entry = Path(directory) / name if entry.lstat().st_file_attributes & 0x400: raise SandboxError('Reparse point in approved sandbox runtime') entries.append(entry) if provision: completed = subprocess.run(['icacls.exe', root, '/grant:r', '*' + self.sid_string + ':(RX)', '/T', '/L', '/Q'], capture_output=True, timeout=60, creationflags=subprocess.CREATE_NO_WINDOW) if completed.returncode: raise SandboxError('Approved runtime access could not be established') for entry in entries: _verify_read_acl(entry, self.sid) self.roots = roots def run(self, executable, arguments, data=b'', environment=None, timeout=30, max_output=8*1024*1024): """One bounded pipe exchange. Child descendants remain in broker Job.""" import msvcrt executable = str(Path(executable).resolve(strict=True)) if not any(Path(executable).is_relative_to(Path(root)) for root in self.roots): raise SandboxError('Executable is outside approved runtime') class SA(ctypes.Structure): _fields_ = [('length', W.DWORD), ('descriptor', ctypes.c_void_p), ('inherit', W.BOOL)] class SI(ctypes.Structure): _fields_ = [('cb', W.DWORD), ('reserved', W.LPWSTR), ('desktop', W.LPWSTR), ('title', W.LPWSTR), ('x', W.DWORD), ('y', W.DWORD), ('xs', W.DWORD), ('ys', W.DWORD), ('xc', W.DWORD), ('yc', W.DWORD), ('fill', W.DWORD), ('flags', W.DWORD), ('show', W.WORD), ('reserved2size', W.WORD), ('reserved2', ctypes.c_void_p), ('stdin', W.HANDLE), ('stdout', W.HANDLE), ('stderr', W.HANDLE)] class SIX(ctypes.Structure): _fields_ = [('startup', SI), ('attributes', ctypes.c_void_p)] class PI(ctypes.Structure): _fields_ = [('process', W.HANDLE), ('thread', W.HANDLE), ('pid', W.DWORD), ('tid', W.DWORD)] class SC(ctypes.Structure): _fields_ = [('sid', ctypes.c_void_p), ('capabilities', ctypes.c_void_p), ('count', W.DWORD), ('reserved', W.DWORD)] security = SA(ctypes.sizeof(SA), None, True) handles = [] attribute = None process = PI() job = None owned_files = [] try: ir, iw, or_, ow = W.HANDLE(), W.HANDLE(), W.HANDLE(), W.HANDLE() for read, write in [(ir, iw), (or_, ow)]: if not self.k.CreatePipe(ctypes.byref(read), ctypes.byref(write), ctypes.byref(security), 0): raise SandboxError('Sandbox pipes unavailable') handles.extend([read.value, write.value]) for parent_handle in [iw, or_]: if not self.k.SetHandleInformation(parent_handle, 1, 0): raise SandboxError('Sandbox pipe inheritance unavailable') size = ctypes.c_size_t() self.k.InitializeProcThreadAttributeList(None, 2, 0, ctypes.byref(size)) attribute = ctypes.create_string_buffer(size.value) if not self.k.InitializeProcThreadAttributeList(attribute, 2, 0, ctypes.byref(size)): raise SandboxError('Sandbox launch attributes unavailable') caps = SC(self.sid, None, 0, 0) inheritance = (W.HANDLE * 2)(ir.value, ow.value) for key, value in [(0x20009, caps), (0x20002, inheritance)]: if not self.k.UpdateProcThreadAttribute(attribute, 0, key, ctypes.byref(value), ctypes.sizeof(value), None, None): raise SandboxError('Sandbox launch boundary unavailable') startup = SIX(); startup.startup.cb = ctypes.sizeof(SIX) startup.startup.flags = 0x100; startup.startup.stdin = ir; startup.startup.stdout = ow; startup.startup.stderr = ow startup.attributes = ctypes.cast(attribute, ctypes.c_void_p) # Explicit environment: no broker credential or inherited deployment secrets. env = {'SystemRoot': os.environ.get('SystemRoot', 'C:\\Windows'), 'PYTHONDONTWRITEBYTECODE': '1', 'PYTHONIOENCODING':'utf-8'} for key in ['USERPROFILE','LOCALAPPDATA','APPDATA','TEMP','TMP','WINDIR']: if key in os.environ: env[key] = os.environ[key] allowed = {'EMBEDDING_MODEL_DIR','EMBEDDING_WORKER_PATH','SEARCHI_FORCE_CPU','GGML_CPU_ONLY','GGML_DISABLE_VULKAN'} if any(key not in allowed or not isinstance(value,str) or '\0' in value for key,value in (environment or {}).items()): raise SandboxError('Unapproved AppContainer environment field') env.update(environment or {}) environment_block = ctypes.create_unicode_buffer('\0'.join(f'{key}={value}' for key, value in sorted(env.items())) + '\0\0') command = ctypes.create_unicode_buffer(subprocess.list2cmdline([executable, *arguments])) if not self.k.CreateProcessW(executable, command, None, None, True, 0x80000|0x400|0x8000000|0x4, environment_block, self.roots[0], ctypes.byref(startup), ctypes.byref(process)): raise SandboxError('AppContainer process launch failed: ' + str(ctypes.get_last_error())) # Suspended launch avoids an execution window before descendant cleanup. class BasicLimits(ctypes.Structure): _fields_ = [('processTime', ctypes.c_int64), ('jobTime', ctypes.c_int64), ('flags', W.DWORD), ('minWorking', ctypes.c_size_t), ('maxWorking', ctypes.c_size_t), ('processes', W.DWORD), ('affinity', ctypes.c_size_t), ('priority', W.DWORD), ('scheduling', W.DWORD)] class Extended(ctypes.Structure): _fields_ = [('basic', BasicLimits), ('io', ctypes.c_uint64 * 6), ('processMemory', ctypes.c_size_t), ('jobMemory', ctypes.c_size_t), ('peakProcess', ctypes.c_size_t), ('peakJob', ctypes.c_size_t)] limits = Extended(); limits.basic.flags = 0x2000 | 0x8; limits.basic.processes = 2 job = self.k.CreateJobObjectW(None, None) if not job or not self.k.SetInformationJobObject(job, 9, ctypes.byref(limits), ctypes.sizeof(limits)) or not self.k.AssignProcessToJobObject(job, process.process): self.k.TerminateProcess(process.process, 1) raise SandboxError('AppContainer descendant containment unavailable') if self.k.ResumeThread(process.thread) == 0xffffffff: raise SandboxError('AppContainer launch resume failed') self.k.CloseHandle(process.thread); process.thread = None for child_handle in [ir.value, ow.value]: self.k.CloseHandle(child_handle); handles.remove(child_handle) writer = os.fdopen(msvcrt.open_osfhandle(iw.value, os.O_WRONLY|os.O_BINARY), 'wb', buffering=0) reader = os.fdopen(msvcrt.open_osfhandle(or_.value, os.O_RDONLY|os.O_BINARY), 'rb', buffering=0) handles.remove(iw.value); handles.remove(or_.value); owned_files = [writer, reader] chunks = []; exceeded = threading.Event(); errors = [] def write(): try: position = 0 while position < len(data): count = writer.write(memoryview(data)[position:]) if not count: raise OSError() position += count except OSError: pass finally: writer.close() def read(): total = 0 try: while True: chunk = reader.read(65536) if not chunk: break total += len(chunk) if total > max_output: exceeded.set(); self.k.TerminateProcess(process.process, 1); break chunks.append(chunk) except OSError: errors.append(True) threads = [threading.Thread(target=write, daemon=True), threading.Thread(target=read, daemon=True)] for thread in threads: thread.start() if self.k.WaitForSingleObject(process.process, int(timeout*1000)) != 0: self.k.TerminateProcess(process.process, 1) self.k.WaitForSingleObject(process.process, 5000) raise SandboxError('AppContainer inspection deadline exceeded') self.k.CloseHandle(job);job=None for thread in threads: thread.join(2) if exceeded.is_set() or errors or any(thread.is_alive() for thread in threads): raise SandboxError('AppContainer response failed bounded exchange') code = W.DWORD() if not self.k.GetExitCodeProcess(process.process, ctypes.byref(code)): raise SandboxError('AppContainer process status unavailable') return code.value, b''.join(chunks) finally: if job: self.k.CloseHandle(job);job=None for file in owned_files: if not file.closed: file.close() for handle in handles: self.k.CloseHandle(handle) if job: self.k.CloseHandle(job) if process.thread: self.k.CloseHandle(process.thread) if process.process: self.k.CloseHandle(process.process) if attribute is not None: self.k.DeleteProcThreadAttributeList(attribute)