Andrew Denner · March 2026 · denner.co Part 3 of the Linux Universal Packages series
I rage-deleted snapd twice. I’m going to explain exactly what I was rage-deleting. The more you understand about Snap’s architecture, the more you either appreciate the engineering or become more annoyed at the design decisions — sometimes both at once. Let’s go.
Snap is not just a package format. It’s a daemon (snapd), a store protocol, a confinement framework, a distribution channel system, and a set of Linux kernel primitives wired together in Go. Understanding Snap means understanding snapd.
# snapd is always running on Ubuntu:
systemctl status snapd
# Active: active (running)
# It exposes a REST API via Unix socket:
ls -la /run/snapd.socket
# srw-rw---- 1 root snapd ... /run/snapd.socket
# The snap CLI just calls this REST API:
sudo snap install vlc
# Under the hood: POST /v2/snaps/vlc {"action":"install"}
The entire Snap CLI is a thin wrapper over the snapd REST API. Every snap command — install, remove, refresh, connect, disconnect — is an HTTP call to /run/snapd.socket. This design means:
snapd maintains its state in /var/lib/snapd/state.json. This JSON file is snapd’s database — it records every installed snap, every connection between snaps, every pending operation, and the complete revision history.
sudo python3 -c "import json,sys; d=json.load(open('/var/lib/snapd/state.json')); print(list(d.keys()))"
# ['data', 'changes', 'tasks', 'last-task-id', 'last-change-id', 'last-notice-id', 'notices']
Every operation in snapd is a change composed of tasks. When you run snap install vlc, snapd creates a change with tasks like download-snap, validate-snap, mount-snap, copy-snap-data, link-snap. If the operation fails halfway, snapd can resume or roll back from the last known-good state. This is the reliability story — snapd never leaves your system in a partial state.
The word “snap” refers to several distinct things in Canonical’s ecosystem:
App snaps: What most people mean. User-installed applications. snap install vlc
Base snaps: Minimal root filesystems that other snaps run on top of. core22 is the Ubuntu 22.04 base. core20, core18 for older bases. When you install VLC, snapd also installs core22 if not present — VLC’s sandbox uses core22 as its filesystem root.
Kernel snaps: On Ubuntu Core (IoT/embedded), the Linux kernel itself is a snap. This allows over-the-air kernel updates with the same rollback mechanism as app snaps.
Gadget snaps: Also Ubuntu Core. Defines the device’s boot configuration, disk layout, and hardware-specific initialization.
Content snaps: Share data (fonts, icons, themes, SDKs) between snaps via the content interface. The GNOME Platform snap is a content snap that provides GTK runtime files, reducing duplication between apps.
For desktop Linux, you’re dealing with app snaps and base snaps. But understanding base snaps explains why snap install sometimes downloads 70MB before it installs your 5MB app — it’s downloading the base runtime if you don’t have it.
/snap/
├── vlc/ # App namespace
│ ├── 3221/ # Revision 3221 (squashfs mounted here)
│ ├── 3198/ # Revision 3198 (previous, kept for rollback)
│ └── current -> 3221/ # Symlink to active revision
├── core22/ # Base snap
│ ├── 607/
│ └── current -> 607/
├── snapd/ # snapd itself (as a snap)
│ └── ...
└── bin/ # Symlinks: snap, snapctl, etc.
/var/lib/snapd/
├── snaps/ # The actual .snap squashfs files
│ ├── vlc_3221.snap # squashfs file (read-only source)
│ └── vlc_3198.snap # Previous revision (kept until manually removed)
├── state.json # snapd's database
├── assertions/ # Cryptographic assertions (more on this below)
└── mount/ # Mount namespace configuration files
/var/snap/
└── vlc/
├── 3221/ # Revision-specific data (wiped on update)
├── current -> 3221/ # Symlink
└── common/ # Persistent data (survives updates)
The squashfs file in /var/lib/snapd/snaps/vlc_3221.snap is mounted read-only at /snap/vlc/3221/ via the kernel loop device. There’s no FUSE involved — snapd uses the kernel’s native loop device and squashfs support:
mount | grep snap
# /dev/loop4 on /snap/vlc/3221 type squashfs (ro,nodev,relatime,errors=continue,threads=single)
This is why Snap’s performance is generally better than AppImage’s FUSE approach — the kernel reads squashfs directly without the userspace round-trip. The latency penalty comes from the AppArmor and seccomp machinery that wraps execution, not the filesystem access itself.
When you launch a Snap application, you don’t directly exec the binary at /snap/vlc/3221/usr/bin/vlc. You go through snap-confine, a small C binary with setuid root permissions that sets up the execution environment:
user launches vlc
→ /snap/bin/vlc (shell wrapper)
→ /usr/lib/snapd/snap-exec
→ /usr/lib/snapd/snap-confine
→ [namespace setup, apparmor transition, seccomp load]
→ /snap/vlc/3221/usr/bin/vlc (confined process)
snap-confine does the following in sequence:
CLONE_NEWNS). Inside this namespace, the snap sees:
/snap/vlc/3221/ mounted at / (the app’s root filesystem via bind mounts)/home, /tmp, specific /run/user paths (as permitted by interfaces)/tmp, /dev/shm (isolated per snap)AppArmor profile transition: Calls aa_change_profile() to transition the process into the snap’s AppArmor profile. After this point, all actions are subject to AppArmor MAC.
seccomp filter load: Loads a seccomp BPF filter that restricts system calls. The filter is generated from the snap’s declarations.
setuid root on snap-confine itself is only used to set up the namespace — the app process drops root before exec.AppArmor (Mandatory Access Control) enforces policy regardless of what the process tries to do with POSIX capabilities. A process confined by AppArmor cannot access resources not listed in its profile, even if the user running it has permission.
Snap generates AppArmor profiles automatically from the snap’s meta/snap.yaml. The profile is generated by snapd and loaded into the kernel when the snap is installed.
# See the generated AppArmor profile for a snap:
cat /var/lib/snapd/apparmor/profiles/snap.vlc.vlc
# The profile controls:
# - File access (read/write/execute paths)
# - Network access
# - D-Bus access (what services it can call, what methods)
# - Signal sending
# - Ptrace
# - Mount/unmount operations
# - Capability acquisition
A snippet of what a typical snap AppArmor profile looks like:
# Allow reading snap content
/snap/vlc/** r,
/snap/vlc/**/**.so* mr,
# Home directory (controlled by home interface)
@{HOME}/ r,
@{HOME}/** rwk, # Only if home interface connected
# Deny dangerous capabilities
deny /proc/sys/kernel/sysrq w,
deny /sys/kernel/security/** rwklx,
deny @{PROC}/@{pid}/mem rwklx,
The granularity is real. If VLC has the home interface connected, it can read/write your home directory. If you disconnect it via snap disconnect vlc:home, the AppArmor profile is regenerated and VLC can no longer access ~ even if it tries.
The key to Snap’s security model is that interfaces map to AppArmor policy fragments. Each interface has a policy file in /usr/share/snappy/interfaces/. When you connect an interface, snapd regenerates the AppArmor profile to include the corresponding policy fragment and reloads it.
# See all interfaces and their connection state for a snap:
snap connections vlc
# Interface Plug Slot Notes
# audio-playback vlc:audio-playback :audio-playback -
# home vlc:home :home -
# network vlc:network :network -
# removable-media vlc:removable-media - - # NOT connected
# Connect removable-media to allow /media/* access:
snap connect vlc:removable-media
# Disconnect home to prevent home directory access:
snap disconnect vlc:home
The interface system is the most technically sophisticated part of Snap. There are ~100 interfaces covering everything from audio-playback to bluetooth-control to docker to ssh-keys.
AppArmor controls access to resources. seccomp (secure computing mode) controls which system calls the process can make. These are two separate but complementary confinement mechanisms.
Snap uses seccomp in filter mode (BPF program that inspects syscall numbers and arguments). The seccomp profile is generated by snapd-seccomp from the snap’s declarations.
A typical snap seccomp profile allows common syscalls while blocking dangerous ones:
# Allowed (representative subset):
read write open close stat fstat lstat poll
select mmap mprotect munmap brk
socket connect accept bind listen
execve exit_group
# Blocked (not listed = SIGSYS):
# kexec_load (load new kernel)
# create_module (load kernel module)
# init_module (insert kernel module)
# perf_event_open (performance monitoring, can be used for side channels)
# ptrace (unless debugging interface connected)
# bpf (eBPF programs)
seccomp filtering happens at the kernel level. If a confined process tries to call a blocked syscall, it receives SIGSYS (bad system call) immediately — the syscall is never executed.
This is meaningfully different from AppArmor. AppArmor can restrict what files you can open, but it can’t stop a process from making a particular syscall. seccomp stops syscalls before they reach the kernel. Together, they provide defense-in-depth: AppArmor denies access to resources, seccomp denies the ability to make certain system calls at all.
Snap’s update system is more sophisticated than most users realize.
Every snap in the Snap Store exists on a channel with a risk level:
stable # What most users want
candidate # Almost ready, needs testing
beta # Active development, may break
edge # Latest commit, CI builds
You can install a specific channel:
snap install vlc --channel=beta
snap switch vlc --channel=stable # Change channel after install
snap install code --channel=latest/stable # Explicit track/risk
Tracks are for major version branching:
snap install chromium --channel=stable # "latest" track implicitly
snap install chromium --channel=107/stable # Pin to Chromium 107
This matters for security: a CVE affecting Chromium 110 doesn’t affect snaps pinned to older tracks. This is the snap hold equivalent for staying on a specific major version.
When snapd refreshes a snap, it requests delta packages from the Snap Store:
/snap/vlc/current symlink is updated atomicallyThe delta mechanism can reduce update sizes by 80-90% for minor updates. Major updates (new bundled libraries) still download most of the image.
Every snap in the Snap Store is cryptographically signed. snapd verifies the full chain before mounting a snap:
# Assertions are stored here:
ls /var/lib/snapd/assertions/
# The chain:
# Snap Store Root Key (Canonical-controlled)
# → Account Key (developer's key)
# → snap-declaration (metadata: interfaces used, slots, plugs)
# → snap-revision (specific version: sha3-384 hash of squashfs)
When snapd downloads a snap, it verifies the snap-revision assertion’s hash matches the downloaded squashfs. This prevents MITM attacks — you can’t intercept and replace a snap in transit because you can’t forge the assertion signatures.
This is a security strength Snap has over AppImage. AppImages have no mandatory signing — assertions in AppImageKit are optional and rarely used. Snap downloads are always verified against Canonical’s key infrastructure.
The notorious Snap startup latency deserves a proper explanation.
On a cold start (first launch, no caches warmed):
squashfs mount: The /snap/vlc/3221/ mount is persistent — it’s mounted at boot. So this is not actually a cold-start cost anymore in recent snapd versions (older versions unmounted between uses).
AppArmor profile load: A cache of compiled AppArmor profiles is maintained. If the cache is valid, this is fast (~1ms). If the profile needs recompilation (after an update), this can take 100-500ms.
snap-confine overhead: Creating the mount namespace, loading seccomp filter, executing the snap-exec chain: ~50-200ms depending on complexity.
squashfs block decompression: The first read of each block incurs decompression overhead. For an app like Firefox that reads thousands of files on startup, this adds up.
Benchmark example on typical SSD hardware:
# Firefox snap cold start
time snap run firefox
# real: 8.3s
# Firefox .deb (apt-installed) cold start
time firefox
# real: 2.1s
# Firefox snap warm start (second launch)
time snap run firefox
# real: 3.1s
The 6-second cold-start penalty is almost entirely squashfs decompression. The warm-start gap (1 second) is snap-confine overhead.
On NVMe SSDs with fast decompression (Ryzen processors have hardware-assisted lz4), the gap narrows. On spinning disks, it widens. This is a hardware problem masquerading as a software problem.
This deserves documentation because it’s still misunderstood.
In 2019, Canonical deprecated the chromium-browser Debian package. They stopped publishing security updates to the .deb. Instead, they maintained the Chromium snap and made apt install chromium-browser on Ubuntu install the snap instead.
The mechanism: Ubuntu patches apt to recognize the chromium-browser package name as a snap trigger. When you apt install chromium-browser, Ubuntu’s package manager calls snap install chromium instead of downloading a .deb.
This was documented in Ubuntu’s release notes. It was also, let’s be honest, surprising and somewhat jarring to experienced Linux users who expected apt install to install a .deb.
The reasoning from Canonical’s side is valid: maintaining Chromium packages for each Ubuntu LTS is genuinely difficult because Chromium’s build system evolves faster than LTS timelines. The snap is better maintained. The security updates arrive faster.
The frustration from users is also valid: intercepting apt install and doing something different from what was asked breaks user expectations and violates the principle of least surprise.
Both things are true.
By 2025–2026, the sharpest edges of this situation have been sanded down. Mozilla now publishes a first-party Firefox .deb via their own PPA, which many users prefer over the snap. Google Chrome has always been a .deb from Google directly. Ungoogled Chromium has active community PPA maintenance. The dominant use case for the Chromium snap — “I just want Chromium on Ubuntu and I don’t want to manage a PPA” — still applies, but the claim that users have to use the snap for a maintained browser is no longer true.
Snap’s center of gravity has shifted clearly toward server/IoT: Ubuntu Core devices, Microk8s, Juju charms, and background services on Ubuntu Server. That’s where Canonical’s investment is concentrated, and it’s where Snap’s architectural decisions make the most sense. The desktop snap wars have largely cooled.
A complete accounting of the Snap footprint:
# Squashfs mount points (one per revision of each snap):
mount | grep squashfs | wc -l
# On a typical Ubuntu desktop: 10-20 loop devices
# Actual disk usage:
du -sh /var/lib/snapd/snaps/ # squashfs files
du -sh /var/snap/ # App data (user data here is yours, don't delete)
du -sh /snap/ # Mount points (no real disk usage, just mounts)
# AppArmor profile cache:
du -sh /var/cache/apparmor/
# On a moderate Ubuntu desktop install:
# /var/lib/snapd/snaps/ ≈ 1-4 GB depending on what's installed
# /var/snap/ ≈ 200MB-2GB (mostly application data you want to keep)
The multiple revisions issue: by default, snapd keeps the current and one previous revision of each snap. That means two copies of every squashfs on disk.
# Show disk usage per snap including all revisions:
snap list --all
# Remove all old revisions:
snap list --all | awk '/disabled/{print $1, $3}' | while read name rev; do
snap remove "$name" --revision="$rev"
done
If you’ve decided snapd is not for you on a non-Ubuntu distro, or you want it gone on Ubuntu:
# Remove all snaps first (in reverse dependency order):
# snap-store, gnome-3-38, gtk-common-themes, firefox, ...
snap list # Check what's installed
for snap in $(snap list | awk 'NR>1{print $1}'); do
sudo snap remove --purge "$snap"
done
# Remove snapd itself:
sudo apt purge snapd
sudo apt-mark hold snapd # Prevent reinstallation by apt
# Clean up:
rm -rf ~/snap # User app data (~/snap/vlc/, ~/snap/firefox/, etc.)
sudo rm -rf /snap # Kernel loop-mounted squashfs images (system-wide mount point)
sudo rm -rf /var/snap # Per-snap revision data
sudo rm -rf /var/lib/snapd # snapd database, downloaded .snap files, assertions
# Optionally: prevent Ubuntu from reinstalling snap packages via apt
# Create a preference file:
cat << 'EOF' | sudo tee /etc/apt/preferences.d/no-snaps.pref
Package: snapd
Pin: release a=*
Pin-Priority: -10
EOF
The apt-mark hold is important — some Ubuntu packages will try to pull snapd back in as a dependency (notably ubuntu-desktop). The no-snaps.pref file is the nuclear option that prevents any snap from being installed via apt.
On an Ubuntu machine where snap-delivered apps are part of the default setup (Firefox, Thunderbird), removing snapd means finding .deb alternatives or Flatpak equivalents. The Firefox PPA maintains a .deb:
sudo add-apt-repository ppa:mozillateam/ppa
echo 'Package: *
Pin: release o=LP-PPA-mozillateam
Pin-Priority: 1001' | sudo tee /etc/apt/preferences.d/mozilla-firefox
sudo apt install firefox
I’ve been hard on Snap. Let me be fair about where it excels.
Server and IoT use cases: On Ubuntu Core, snaps are excellent. The atomic update and rollback mechanism, combined with the confined execution model, makes remote management of headless devices genuinely reliable. When a snap update fails on a Raspberry Pi you can’t physically access, the automatic rollback is not a nice-to-have — it’s the difference between a bricked device and a working one.
The interface system: The depth and granularity of Snap’s interface system is impressive. The ability to see and control exactly what each app can access — and have that control enforced at the kernel level — is a real security feature. snap connections is one of the most useful security introspection tools on Linux.
The update security model: Assertions + signing + delta updates is a well-designed system. The cryptographic chain from Canonical’s root key to individual snap revisions is stronger than most distribution systems.
Next: Part 4 — Flatpak: Portal Architecture and the Bubblewrap Sandbox
Andrew Denner — denner.co — @adenner