snapd is the background daemon that manages snap packages across Linux distributions. It's written in Go and consists of a daemon (snapd), CLI client (snap), and sandbox execution components (snap-confine, snap-exec).
See ARCHITECTURE.md for detailed diagrams and explanations.
The core architecture is based on overlord.Overlord which coordinates state managers:
- State managers implement
StateManagerinterface withEnsure(). Managers may optionally define the methodsStartUp()orStop()as defined by theStateStarterUpandStateStopperinterfaces, respectively. - All operations are persisted to survive reboots via
overlord/state.State(backed bystate.json) - Operations are modeled as
state.Change→ graph ofstate.Taskwith do/undo handlers state.TaskRunnerexecutes tasks, spawning goroutines for handlers
Key state managers:
overlord/snapstate: Snap lifecycle (install/remove/update), managesSnapStateper snapoverlord/ifacestate: Interface connections, security profiles viainterfaces.Repositoryoverlord/assertstate: Signed assertion database (snap-declaration, snap-revision)overlord/devicestate: Device identity, registration, Ubuntu Core installation/remodelingoverlord/hookstate: Snap hook execution
Critical import rules:
snapstateis imported BY other managers but CANNOT import them- Circular imports broken via function hook variables (see
snapstate.ValidateRefreshes) - Hook variables must be assigned in
init()orManager()constructors
snap run <snap.app> → exec(snap-confine) → exec(snap-exec) → actual app
[sandbox setup] [final prep]
snap-confine uses capabilities to set up mount namespace, AppArmor profiles,
then execs snap-exec which runs the actual snap binary.
Task handlers follow strict conventions:
func (m *SnapManager) doMountSnap(t *state.Task, _ *tomb.Tomb) error {
st := t.State()
st.Lock()
defer st.Unlock() // Auto-commits state changes
// Extract parameters from task
var snapsup SnapSetup
t.Get("snap-setup", &snapsup)
// Slow operations (I/O, network) require unlocking:
st.Unlock()
defer st.Lock()
// ... copy/download/mount operations ...
// Non idempotent operations need to set task status before returning.
st.Lock()
t.SetStatus(state.DoneStatus)
st.Unlock()
return nil // TaskRunner sets task to DoneStatus
}
func (m *SnapManager) undoMountSnap(t *state.Task, _ *tomb.Tomb) error {
// Undo must be symmetric and idempotent
}State locking rules:
- Start handlers with
st.Lock(); defer st.Unlock() - Release lock only for slow I/O/network operations
- Working state + status changes must be atomic via
Task.SetStatus()before unlock
Build natively:
Note that while there are many binaries, usually you only need snap and snapd for development. Many go binaries have special build rules (.e.g precise static linking). Snapd can be built with keys to the production
snap store, or with test keys that allow installing snaps
signed with the well-known, insecure test key.
Building several elements of snapd individually:
go build -o /tmp/build/snap ./cmd/snap
go build -o /tmp/build/snapd ./cmd/snapd
go build -o /tmp/build ./... # All binariesYou may want to build the snapd snap package with snapcraft pack instead, as that constructs a complete, cohesive set of programs.
Run checks (required before commits):
./run-checks # Runs: go fmt, go vet, golangci-lint, unit tests, static checksUnit tests:
go test -check.f TestName # Run specific test
go test -v -check.vv # Verbose mode for debugging hangs
LANG=C.UTF-8 go test # Required locale for many tests
make -C cmd check # C unit testsIntegration tests (spread):
./run-spread garden:ubuntu-22.04-64 # Builds test snapd snap automatically
NO_REBUILD=1 ./run-spread garden:... # Skip rebuild when iterating
./run-spread -reuse garden:ubuntu-22.04-64 # Reuse systems (faster iteration)Build snapd snap (preferred for testing):
snapcraft # Uses build-aux/snap/snapcraft.yaml
sudo snap install --dangerous snapd_*.snapAbbreviated coding conventions. See CODING.md for details.
- Follow
gofmt -s(enforced byrun-checks) - Error messages: lowercase, no period, "cannot X" not "failed to X"
- Error types: introduce
*Errorstructs only when callers need to inspect them - Check similar code in same package for naming consistency
- Packages should have clear, focused responsibilities at consistent abstraction levels
- Abstract/primitive packages at bottom (e.g.,
boot→bootloader) - Application-specific packages at top (e.g.,
snapstate→snapstate/backend→boot) *utilpackages: Cannot import non-util packages, minimize dependenciesoverlord/*statepackages: Only for snapd daemon, not imported by CLI tools- Exception: subset of
overlord/configstate/configcore(vianomanagersbuild tag)
- Exception: subset of
// Prefix internal programming errors
return fmt.Errorf("internal error: unexpected state %v", state)
// Use "cannot" for user-facing errors
return fmt.Errorf("cannot install snap: %v", err)
// Keep error chains concise—avoid "cannot: cannot: cannot"Use gocheck (not stdlib testing):
package mypackage_test // Test from exported API perspective
import . "gopkg.in/check.v1"
type mySuite struct{}
var _ = Suite(&mySuite{})
func (s *mySuite) TestFeature(c *C) {
c.Assert(value, Equals, expected)
}Note that as a special exception, benchmarks are expected
to use stdlib testing package.
Export internals via export_test.go:
// export_test.go (in package mypackage)
var TimeNow = timeNow // Export unexported var for testing
func MockTimeNow(f func() time.Time) (restore func()) {
restore = testutil.Backup(&timeNow)
timeNow = f
return restore
}Test requirements:
- Test both
doandundotask handlers symmetrically - Verify handlers are idempotent (can safely re-run after partial execution)
- Test error paths that perform cleanup
- Minimize mocking—mock at system boundaries (systemd, store) not internal packages
- Use
<package>testhelpers (e.g.,assertstest,devicestatetest) for complex fixtures
Spread test section order (enforced by CI):
summary(required)details(required)backends,systems,manual,priority,warn-timeout,kill-timeoutenvironment,prepare,restore,debugexecute(required)
Running specific tests:
To run specific go tests using the check framework, use commands like this:
go test -v "${package_path}" -check.v -check.f "${test_pattern}"
PR format:
- Title:
affected/packages: short summary in lowercase - Keep diffs ≤500 lines (split if larger)
- Separate refactoring from behavior changes
- Refactoring must not touch tests unless unavoidable
Commit messages:
overlord/snapstate: add helper to get gating holds
gadget,image: remove LayoutConstraints struct
o/snapstate: add user and gating holds helpers # Abbreviate when obvious
many: correct struct fields and output keys # Many packages affected
spread: remove old release of distribution # spread.yaml affected
Merging strategy:
- Prefer "Squash and Merge" (simplifies cherry-picking)
- Use "Rebase and Merge" only when commit history is valuable
- Never use "Create a merge commit"
overlord/README.md: Deep dive on state managers, task lifecycle, conflictsARCHITECTURE.md: Entry points, execution pipeline, manager responsibilitiesCODING.md: Full coding conventions, error handling, testing philosophyspread.yaml: Integration test configuration with backend definitions- Task parameters: Via
task.Get("snap-setup", &snapsup)asSnapSetupstructs - Manager caching:
state.State.Cache()with private keys for manager instances
Debug snapd daemon:
sudo systemctl stop snapd.service snapd.socket
sudo SNAPD_DEBUG=1 SNAPD_DEBUG_HTTP=3 ./snapd
# SNAPD_DEBUG_HTTP: 1=requests, 2=responses, 4=bodies (bitfield)Debug snap CLI:
SNAP_CLIENT_DEBUG_HTTP=7 snap install ... # Same bitfield as aboveInterfaces define how snaps access system resources and interact with each other. Each interface consists of plugs (consumers) and slots (providers).
Every interface must:
- Be registered via
registerIface()in itsinit()function - Implement the
Interfaceinterface (at minimumName()andAutoConnect()) - Live in
interfaces/builtin/package
Common interface pattern:
type myInterface struct {
commonInterface // Embeds standard behavior
}
func (iface *myInterface) Name() string {
return "my-interface"
}
func init() {
registerIface(&myInterface{commonInterface{
name: "my-interface",
summary: "allows access to X",
implicitOnCore: true,
baseDeclarationPlugs: myBaseDeclarationPlugs,
baseDeclarationSlots: myBaseDeclarationSlots,
}})
}Interfaces generate security profiles by implementing backend-specific methods:
AppArmorConnectedPlug/Slot: AppArmor rules when connectedAppArmorPermanentPlug/Slot: AppArmor rules always presentSecCompConnectedPlug/Slot: Seccomp syscall filters when connectedUDevConnectedPlug/Slot: UDev rules for device accessKModConnectedPlug/Slot: Kernel modules to load
Example AppArmor snippet:
func (iface *myInterface) AppArmorConnectedPlug(spec *apparmor.Specification,
plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
spec.AddSnippet("/dev/my-device rw,")
return nil
}Base declarations define default connection/installation policies:
const myBaseDeclarationPlugs = `
my-interface:
allow-installation: false # Super-privileged, needs snap-declaration
deny-auto-connection: true # Manual connection required
`Policy evaluation order (first match wins):
deny-*in plug snap-declarationallow-*in plug snap-declarationdeny-*in slot snap-declarationallow-*in slot snap-declarationdeny-*in plug base-declarationallow-*in plug base-declarationdeny-*in slot base-declarationallow-*in slot base-declaration
Implement sanitizers to validate plug/slot attributes:
func (iface *myInterface) BeforePreparePlug(plug *snap.PlugInfo) error {
path, ok := plug.Attrs["path"].(string)
if !ok || path == "" {
return fmt.Errorf("my-interface must contain path attribute")
}
return nil
}- Test both plug and slot sides
- Test connection scenarios
- Test AppArmor/seccomp snippet generation
- Verify base declaration policy evaluation
- Use
ifacetest.BackendSuitefor backend tests
interfaces/builtin/README.md: Complete policy evaluation guideinterfaces/core.go: Core interface types and sanitizer interfacesinterfaces/builtin/common.go:commonInterfacewith standard behaviorinterfaces/repo.go: Interface repository managing connections
- State transitions are persistent: All
state.Changeandstate.Tasksurvive restarts - Task handlers are retriable: Design for idempotency
- Device context is contextual: Use
DeviceCtx(task)in handlers, notDeviceCtxFromState() - Conflicts prevent concurrent ops: Check
snapstate/conflict.gofor snap operation serialization - Backend abstraction: Use
snapstate/backendfor disk state, never manipulate directly - Interface security profiles are additive: Each connected interface adds to AppArmor/seccomp profiles