# One generation of an isolated exec-backed instance (zero-downtime # deploys design §7.6). The instance string is ":" # (%I), and the trampoline and the exit writer split it. systemd has no # specifier for part of an instance string, and ':' is BindPaths='s field # separator, so everything keyed by the host ID alone (the slice, the data # mounts) or by the generation (the executable mount, the log socket) # comes from a runtime drop-in the control service writes before each # start. Without that drop-in this unit has an empty /data and its # trampoline refuses, and the control service will not start it unless # systemd reports the drop-in's slice. carlos-instance@.service stays for # every box and binary that still runs it. [Unit] Description=carlos exec-backed instance generation %I # Stop-only coupling, as carlos-instance@.service (codex R1-7). BindsTo=carlos-edge.service Requires=data-instances.mount run-carlos-instances.mount run-carlos-logs.mount After=carlos-edge.service data-instances.mount run-carlos-instances.mount run-carlos-logs.mount [Service] Type=simple ExecStart=/opt/carlos/carlos instance-exec run %I # The tenant runs in its own data directory (#256 D). %I here is # :, not the host's id, so the real directory comes from the # generation's runtime drop-in, which overrides this line. This one only # applies if the drop-in did not, and then the start is already refused # (the control service checks the drop-in's slice first), and /data is an # empty tmpfs, so the chdir fails closed. WorkingDirectory=/data/instances ExecStopPost=/opt/carlos/carlos instance-exit run %I User=carlos Group=carlos # The activator owns recycling; systemd must not race it. Restart=no KillMode=mixed TimeoutStopSec=20 # The main process is the trampoline, which stays as the namespace's init # and runs the app as its child (#126): it forwards the SIGTERM and exits # 128+n when the app dies by signal n. These are SIGHUP, SIGINT, SIGPIPE # and SIGTERM, the deaths systemd counts as clean for a main process, so a # stop still leaves the unit inactive rather than failed. SuccessExitStatus=129 130 141 143 StandardError=inherit # --- the namespace (titogo@.service, 2026-07-31 design, on systemd 257) --- ProtectSystem=strict TemporaryFileSystem=/data:ro TemporaryFileSystem=/run/carlos:ro # Every other tree a carlos-uid unit writes on the box is hidden the same # way (codex R9-2): the restore drills' cache under /var/cache, and # /var/lib as a class. An instance needs nothing from either. TemporaryFileSystem=/var/cache:ro TemporaryFileSystem=/var/lib:ro # The version store holds every app's build on the box, world-readable so # a unit can exec its own (#214 item 2). An instance sees none of it: an # empty /opt/carlos with the platform binary that ExecStart= and # ExecStopPost= run put back, and, outside this block because it is per # instance, its own build at /opt/carlos/live/. TemporaryFileSystem=/opt/carlos:ro BindReadOnlyPaths=/opt/carlos/carlos # Root-written credentials a carlos-GROUP process may read live here: # /etc/carlos/relay-masters is root:carlos 0640 for carlos-relay.service, # and an instance's Group=carlos would read it too (codex R10-1). An # instance gets its configuration through its launch file and needs # nothing from /etc/carlos. /run/carlos-control is the root control # service's socket directory (design §5.2): hiding it here too means a # connect from inside any instance's namespace is impossible outright, # not merely refused by the peer check on the far end (final review, # plan 2, item 3). InaccessiblePaths=/etc/carlos /run/carlos-control ProtectHome=yes PrivateTmp=yes PrivateDevices=yes # PrivateDevices= bind-mounts the HOST's /dev/shm into the private /dev, # and every instance is uid carlos, so a sibling's shared memory would be # one open away. Each unit gets its own, charged to its own cgroup. TemporaryFileSystem=/dev/shm:mode=1777,nosuid,nodev # POSIX message queues and SysV IPC belong to an IPC namespace, and every # instance is uid carlos, so a sibling's queue would be one mq_open away # (#178). PrivateIPC= gives each unit its own namespace, and mq_open() # goes through that namespace's own internal mount. PrivateDevices= # carries the host's /dev/mqueue into the private /dev the way it carries # /dev/shm, and that mount would still list and open the host namespace's # queues, so it is replaced the same way. PrivateIPC=yes TemporaryFileSystem=/dev/mqueue:ro # /sys/fs/cgroup read-only: the edge's delegated subtree is carlos-owned, # and its cgroup.kill / cgroup.freeze would otherwise be one write away # for a same-uid tenant (codex R9-1). ProtectControlGroups=yes ProtectProc=invisible ProcSubset=pid PrivatePIDs=yes RestrictNamespaces=yes NoNewPrivileges=yes CapabilityBoundingSet= RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX # Abstract unix sockets live in the network namespace, which instances # share with the box because they need outbound TCP, and the line above # cannot tell them from path sockets (#178). The trampoline scopes them # with Landlock before it execs the app (instanceexec.ScopeAbstractUnix): # an instance can connect only to abstract sockets its own processes # bound. A sibling can still bind a name first; that is a refusal of # service, never a connection. # # Egress (#214 item 3): the internet, and nothing on the box or private. # Loopback is where the console and other tenants' addr upstreams listen; # RFC 1918, ULA, link-local and CGNAT are the VPC, other boxes and the # cloud's internal services. IMDS stays named so the property is # greppable. A cgroup filter: it drops in both directions for every # socket the unit opens. The one thing an instance needs from these # ranges is the box's DNS resolver, so the control service re-opens # exactly the nameservers the instance sees with IPAddressAllow= on # carlos-instances.slice (an allow wins, and a slice's lists reach every # unit below it). The probe checks both halves. IPAddressDeny=localhost multicast 0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 169.254.0.0/16 169.254.169.254/32 172.16.0.0/12 192.0.0.0/24 192.168.0.0/16 198.18.0.0/15 240.0.0.0/4 64:ff9b::/96 fc00::/7 fd00:ec2::254/128 fe80::/10 fec0::/10 # An allow opens an address both ways and on every port, so a loopback # resolver can never be opened: a tenant could bind systemd-resolved's # 127.0.0.53 itself and reach every listener bound to all addresses # through it. Where resolved runs, instances resolve through its # upstreams instead, and the control service opens those. The "-" makes # this a no-op on a box without resolved. BindReadOnlyPaths=-/run/systemd/resolve/resolv.conf:/etc/resolv.conf ProtectKernelTunables=yes ProtectKernelModules=yes ProtectKernelLogs=yes ProtectClock=yes ProtectHostname=yes RemoveIPC=yes LockPersonality=yes RestrictRealtime=yes # No RestrictSUIDSGID=: systemd cannot read the mode inside openat2's # open_how, so it refuses openat2 outright (ENOSYS), and every instancefs # open in here is an openat2 (#126, first roll; systemd 257). What it # guarded is covered: with no capabilities an instance can only make a # set-id file owned by carlos, which gains nothing for another carlos # process, NoNewPrivileges= voids it in here, and no other uid can reach # one (the tenant trees are nosuid, PrivateTmp's sit under a root 0700 # directory). MemoryDenyWriteExecute=yes SystemCallArchitectures=native # --- per-service resources; the tier lives on the host slice (§4.3) --- TasksMax=256 # The kernel picks a tenant before it picks anything else in carlos.slice. OOMScoreAdjust=500 # One OOM kill takes every process of THIS service (systemd sets # memory.oom.group on the service cgroup for OOMPolicy=kill). The sidecar # is a sibling service with its own family rule (§4.3). OOMPolicy=kill