lighter 0.13.9
Containers now reach the devices on your Mac's network however lighter is started at login: by brew services or by a launchd agent of your own, as by lighter install. Started by brew services, lighter reached the Mac's router and nothing else on its network, and no setting in System Settings could change that (#101). Thanks to @stuartb55 for the report, which named the launcher, the doctor's line and the workaround, and made the cause one experiment away. Kubernetes clusters whose nodes are containers now run with the network plugins people choose rather than only kind's own: k3d as it comes, and Cilium in kind, kube-proxy replaced (#106, #108). And a machine with cooperative resources no longer stops growing at 449 blocks, about 58 GiB, and no longer keeps a growth it could not make, with the memory behind it, for good (#104). Thanks to @bldr0 for #106, whose report took k3d apart one missing piece at a time; to @victor-vanherpt for #108, which compared the guest's configuration with Cilium's requirements line by line; and to @petrfilip for #104 and #105, which found both halves of the stall, the leak in the kernel's count and the wait on the host, and measured them on the machine that had it.
What went wrong
Started by brew services, lighter could not reach the Mac's network (#101). macOS asks before an app talks to other devices on the Mac's network, and gives the permission to the app: for lighter, lighter.app, which says why it needs it and is listed in System Settings › Privacy & Security › Local Network. lighter's own login agent (lighter install) runs lighter.app. The Homebrew formula's service ran the lighter command instead, which macOS never asks about and never allows. A container's connection to a camera, a NAS or a printer opened and was closed at once: TLS failed with an unexpected EOF, HTTP got no answer, and an NVR's RTSP stream died. lighter doctor said local network denied, and allowing every lighter entry in System Settings changed nothing.
A container's TXT, SRV or MX lookup failed while the Mac's first nameserver was down. lighter answers those by asking the nameservers the Mac's configuration picks for the name, but it asked only the first. When that one stopped answering, as an ISP's IPv6 nameserver did on our network while 0.13.9 was being qualified, the Mac's own resolver moved on to the next and every app on the Mac kept working; a container waited five seconds and got nothing.
A file's link count could read one too many in a shared folder. A name unlinked and linked straight back, as pjdfstest does and a build that replaces a hard link can, was counted twice until the unlink landed on the Mac: stat said three names for two. It took a busy Mac, two runs in forty.
A cooperative machine stopped growing at 449 blocks, and then never shrank (#104). The guest's driver adds a block of the range only while what it has added and not yet onlined stays under a gigabyte. Guest patch 0024 puts each block's page array inside the block, and the driver counted the whole block in when it added it but only the pages past the array out when the block came online: two megabytes of every plugged block stayed counted as offline. At 449 blocks (2 GiB of base and 56 GiB of range) the next block no longer fit, the driver returned -ENOSPC, which it never retries, and the guest stopped at about 58 GiB whatever its ceiling. The host then waited on the growth it had offered, as it waits on any plug in flight, with no limit: the range never shrank, and the balloon, offered memory only once the range is out, took nothing either. A guest held 57 GiB of the Mac's memory for nine hours with 55 GiB of it free.
The guest could read an offer of memory half old and half new. The driver reads the 64-bit offer in two halves and trusts them when the device's configuration generation has not moved between; the host stored a new offer before it moved the generation, so an offer landing between the halves was read as neither. A shrink from 0x1_0800_0000 to 0xf800_0000 could read as 128 MiB. It needs the two reads to straddle the store, microseconds apart, and was found reading the code rather than seen (@petrfilip).
k3d could not start a cluster (#106). k3s's network, flannel, tunnels between nodes over VXLAN, which the guest's kernel did not have, and the server restarted in a loop on failed to create vxlan device: operation not supported. Past that (flannel's host-gw backend), a pod could not reach a Service whose endpoint was on its own node, since bridged frames never saw conntrack, and kube-router, k3s's network policy controller, skipped itself for want of ipset or aborted every sync for want of physdev and NFLOG, so no NetworkPolicy was enforced.
Cilium could not start in kind (#108). Its agent refuses to start without bpf_get_cgroup_classid() in its socket programs, which the kernel offers only with the net_cls classid; past that, none of its socket programs loaded without the kernel's BTF, and its tunnel needs VXLAN. Its L7 proxy, its kube-proxy replacement's cleanup of sockets to deleted backends and its bandwidth manager each wanted something more.
What changed
- The machine is always
lighter.app. Started any other way, by a launchd agent or by hand, it becomeslighter.appbefore it starts the virtual machine, in the same process, so launchd still watches it. brew servicesruns whatlighter installruns:lighter.app, as an interactive process, logging to~/.lighter/machine.log, the loglighter doctorand bug reports ask for. It logged to Homebrew'svar/log/lighter.logbefore. The formula change reached 0.13.8 on the day of the report;brew update && brew services restart lighterapplies it.- The next nameserver is asked when the first does not answer, a second later, as the Mac's resolver does, and the first answer from any is the one a container gets. When none answers within four seconds the container is told so (SERVFAIL) inside the five it waits, rather than left to time out.
- A link's count is the Mac's once the unlinks before it land, as
statalready waited for. lighter doctornames a machine running outsidelighter.app, and for a denied local network says what to allow:lighter.app, or, for a machine started from an app's terminal, that app, whose permission macOS gives a program it starts. Terminal and ssh need none.- Guest patch 0024 counts a block's page array with its pages when the block comes online and goes offline, so the driver adds blocks to the whole of the range: a guest offered 60 GiB at once plugged all 480 blocks, where 0.13.8's stopped at 449.
- A growth the guest has plugged nothing towards for five and a half minutes is taken back to what it has plugged, past the driver's longest wait between tries at a block it could not plug yet, and the range does not grow past it again until the guest shrinks or ten minutes pass. It is the floor an unplug that could not finish already had, the other way up. On 0.13.8's kernel, which still stops at 449 blocks, the range came back down to a gigabyte once the offer was taken back. A line, the poll and a container's start now size the range one at a time.
- An offer moves together with the configuration generation, under the transport the guest reads both through.
- The guest's kernel carries what Kubernetes nodes in containers load: VXLAN and GENEVE, bridged netfilter, ipset with
xt_set, physdev and NFLOG, the net_cls classid, TPROXY, the socket match, CT,sock_diagdestroy, cls_bpf and fq. And BTF (/sys/kernel/btf/vmlinux), which every eBPF program built once for all kernels relocates against: Cilium's, and bpftrace's and Tetragon's too. And tracing for BPF: socket iterators, uprobes and tracepoints, with no function tracer. The kernel image is 72 MB, from 57; BTF is 8 MiB of the guest's memory, and tracing makes boot 10 to 15 ms longer. - Bridged netfilter stays off in the guest's own network namespace, where lighter carries a container's UDP by TPROXY: with it on, the kernel runs the IP hooks in bridge context and then drops the socket TPROXY assigned, and the datagram reaches nothing. Init turns its three switches off and binds them read-only, so dockerd cannot turn them on. The nodes' own namespaces have it on, as Kubernetes needs. A Docker network with inter-container traffic off (
enable_icc=false) is refused, as it was before. lighter doctorsays how the guest carried streams: for published ports and for containers' outbound connections, how many were joined in the guest's kernel and how many copied by its agent, and the last join the kernel refused (#107). A guest copying every stream is a warning.
Upgrading
brew upgrade lighter
lighter restart
If brew services starts lighter, brew services restart lighter instead of lighter restart. Or lighter update download then lighter upgrade --restart if you installed with the install script.
Also
- The guest is Linux 6.18.52, as in 0.13.8, with patch 0024 changed and the configuration above, on Alpine 3.22.7 (from 3.22.6): OpenSSL 3.5.9, expat 2.10.0, zlib 1.3.2-r1, musl 1.2.5-r13, brotli 1.1.1 and the CA certificates of 2026-10-20. The data epoch remains 1.
docs/kubernetes.mdhas k3d and Cilium, and gate m22 runs both: Service DNS and HTTP from pods on both nodes, a deny-all NetworkPolicy that blocks and lifts, and a container's own UDP beside the cluster.- The release archive now carries
THIRD-PARTY-NOTICES: the licences of the Rust crates and the C and C++ libraries built into lighter, and of Linux and every Alpine package in the guest. The complete source of the Linux guest, Linux with lighter's patches and every Alpine package in it, is attached to the release aslighter-0.13.9-guest-source.tar. - The README's licence section is shorter, and the pull request template asks for less (#102).
Checksums
f5b77fac2e25fc36c845bb6a90f2662ca8c6cd1914b0167df8f740b805e6d0a6 lighter-0.13.9-arm64.tar.gz
396dec2159bb4a8111b9c3ef1c263bd9811c8a7e1cb7a78fb2f6d7689cf76357 lighter-0.13.9-arm64
0ed537be79323041af7c6b1ce33d58c97a32be64e149b038ce016d6c58e43c29 lighter-0.13.9-guest-source.tar
Notarization: eadcde13-1d03-4e8c-ab28-0cb45b0c5cb3 (Accepted).