11package hostapp
22
33import (
4+ "crypto/sha256"
5+ "encoding/hex"
46 "encoding/json"
57 "fmt"
8+ "io"
69 "log"
710 "os"
811 "path/filepath"
@@ -34,6 +37,7 @@ type Config struct {
3437type Container struct {
3538 Config
3639 MountPath string
40+ HomePath string
3741}
3842
3943var (
@@ -115,6 +119,22 @@ func (container *Container) mount(layerRoot string) (string, error) {
115119 return container .MountPath , nil
116120}
117121
122+ // unmount releases the container's overlay filesystem. It is a no-op for a
123+ // container that was never mounted (MountPath == "").
124+ func (container * Container ) unmount () error {
125+ if container .MountPath == "" {
126+ return nil
127+ }
128+ if err := unix .Unmount (container .MountPath , 0 ); err != nil {
129+ return fmt .Errorf ("unmounting %s: %w" , container .MountPath , err )
130+ }
131+ if Debug {
132+ log .Printf ("Unmounted ID %s from %s" , container .ID , container .MountPath )
133+ }
134+ container .MountPath = ""
135+ return nil
136+ }
137+
118138// initialize reads container config
119139func (container * Container ) initialize (homePath string ) error {
120140 configPath := filepath .Join (homePath , "config.v2.json" )
@@ -127,6 +147,7 @@ func (container *Container) initialize(homePath string) error {
127147 if err := json .NewDecoder (f ).Decode (& container .Config ); err != nil {
128148 return fmt .Errorf ("decoding %s: %w" , configPath , err )
129149 }
150+ container .HomePath = homePath
130151 if Verbose || Debug {
131152 log .Println ("Initialized container:" , container .Config .Name )
132153 }
@@ -192,7 +213,169 @@ func Mount(rootdir string, label string) ([]Container, error) {
192213 return initializeContainers (rootdir , label )
193214}
194215
195- const HOSTOS_BLOCKS_OVERRIDE = "io.balena.image.override"
216+ const (
217+ HOSTOS_BLOCKS_OVERRIDE = "io.balena.image.override"
218+ HOSTOS_BLOCKS_KERNEL_VERSION = "io.balena.image.kernel-version"
219+ HOSTOS_BLOCKS_KERNEL_ABI_ID = "io.balena.image.kernel-abi-id"
220+ CMDLINE_KERNEL_ABI = "balena_kernel_abi"
221+ )
222+
223+ // ParseHostKernelABIID extracts the balena_kernel_abi=<value> token from a
224+ // kernel cmdline string and returns its value. Returns "" when the token is
225+ // absent or carries an empty value, i.e. when the boot path ran a stock
226+ // kernel whose ABI is not knowable.
227+ func ParseHostKernelABIID (cmdline string ) string {
228+ prefix := CMDLINE_KERNEL_ABI + "="
229+ for _ , tok := range strings .Fields (cmdline ) {
230+ if v , ok := strings .CutPrefix (tok , prefix ); ok {
231+ return v
232+ }
233+ }
234+ return ""
235+ }
236+
237+ // GetKernelRelease returns the running kernel's full release string
238+ // (e.g. "6.8.0-100-generic"), as reported by uname(2).
239+ func GetKernelRelease () (string , error ) {
240+ var utsname unix.Utsname
241+ if err := unix .Uname (& utsname ); err != nil {
242+ return "" , fmt .Errorf ("uname syscall failed: %w" , err )
243+ }
244+ return unix .ByteSliceToString (utsname .Release [:]), nil
245+ }
246+
247+ // kernelVersionFromRelease strips the local-version suffix (e.g. "-100-generic",
248+ // "-v8+") from a uname release, leaving the M.m.p version that kernel-version
249+ // compatibility tracks. An empty release yields an empty version.
250+ func kernelVersionFromRelease (release string ) string {
251+ if idx := strings .IndexByte (release , '-' ); idx > 0 {
252+ return release [:idx ]
253+ }
254+ return release
255+ }
256+
257+ // FilterByKernelVersion removes containers whose kernel-version label
258+ // doesn't match the running kernel. Containers without the label always pass.
259+ // An empty kernelVersion disables filtering.
260+ func FilterByKernelVersion (containers []Container , kernelVersion string ) []Container {
261+ if kernelVersion == "" {
262+ return containers
263+ }
264+ var filtered []Container
265+ for _ , c := range containers {
266+ if labelVal , ok := c .Labels [HOSTOS_BLOCKS_KERNEL_VERSION ]; ok && labelVal != kernelVersion {
267+ log .Printf ("Skipping container %s: kernel version %q != running %q" , c .Name , labelVal , kernelVersion )
268+ continue
269+ }
270+ filtered = append (filtered , c )
271+ }
272+ return filtered
273+ }
274+
275+ // ComputeABIID returns the hex-encoded sha256 of the file at path.
276+ // Used to derive io.balena.image.kernel-abi-id from Module.symvers.
277+ func ComputeABIID (path string ) (string , error ) {
278+ f , err := os .Open (path )
279+ if err != nil {
280+ return "" , fmt .Errorf ("opening %s: %w" , path , err )
281+ }
282+ defer f .Close ()
283+ h := sha256 .New ()
284+ if _ , err := io .Copy (h , f ); err != nil {
285+ return "" , fmt .Errorf ("hashing %s: %w" , path , err )
286+ }
287+ return hex .EncodeToString (h .Sum (nil )), nil
288+ }
289+
290+ // ResolveExtensionABIID computes the container's kernel ABI ID as
291+ // sha256(<mount>/lib/modules/<release>/Module.symvers), where release is the
292+ // running kernel's uname release.
293+ //
294+ // Returns "" with no error if the extension carries no kernel modules for the
295+ // running release. Returns an error if /lib/modules/<release> exists
296+ // but Module.symvers is missing (broken extension), if the label disagrees with
297+ // the computed value, or if release is empty (running kernel unknown) for a
298+ // mounted extension.
299+ func (c * Container ) ResolveExtensionABIID (release string ) (string , error ) {
300+ if c .MountPath == "" {
301+ return "" , nil
302+ }
303+ if release == "" {
304+ return "" , fmt .Errorf ("extension %s: running kernel release unknown" , c .Name )
305+ }
306+ modDir := filepath .Join (c .MountPath , "lib" , "modules" , release )
307+ if _ , err := os .Stat (modDir ); err != nil {
308+ if os .IsNotExist (err ) {
309+ return "" , nil
310+ }
311+ return "" , fmt .Errorf ("stat %s: %w" , modDir , err )
312+ }
313+ symversPath := filepath .Join (modDir , "Module.symvers" )
314+ if _ , err := os .Stat (symversPath ); err != nil {
315+ if os .IsNotExist (err ) {
316+ return "" , fmt .Errorf ("broken extension %s: %s missing" , c .Name , symversPath )
317+ }
318+ return "" , fmt .Errorf ("stat %s: %w" , symversPath , err )
319+ }
320+ id , err := ComputeABIID (symversPath )
321+ if err != nil {
322+ return "" , err
323+ }
324+ if labelVal , ok := c .Labels [HOSTOS_BLOCKS_KERNEL_ABI_ID ]; ok && labelVal != "" && labelVal != id {
325+ return "" , fmt .Errorf ("extension %s: %s label %q != computed %q" ,
326+ c .Name , HOSTOS_BLOCKS_KERNEL_ABI_ID , labelVal , id )
327+ }
328+ return id , nil
329+ }
330+
331+ // FilterByKernelABIID keeps only those containers safe to mount over the
332+ // running kernel.
333+ //
334+ // An ABI-agnostic extension makes no kernel-ABI claim and always passes.
335+ // A kernel-carrying extension is kept only when its computed ABI equals hostABIID.
336+ func FilterByKernelABIID (containers []Container , release , hostABIID string ) []Container {
337+ var filtered []Container
338+ for i := range containers {
339+ c := & containers [i ]
340+ id , err := c .ResolveExtensionABIID (release )
341+ if err != nil {
342+ log .Printf ("Error: dropping container %s: %v" , c .Name , err )
343+ continue
344+ }
345+ if id == "" {
346+ filtered = append (filtered , * c )
347+ continue
348+ }
349+ if id != hostABIID {
350+ log .Printf ("Skipping container %s: kernel ABI ID %q != host %q" , c .Name , id , hostABIID )
351+ continue
352+ }
353+ filtered = append (filtered , * c )
354+ }
355+ return filtered
356+ }
357+
358+ // SelectMountable filters the already-mounted extensions down to those
359+ // compatible with the running kernel, unmounting every extension it drops.
360+ // Survivors stay mounted for use as overlay lowerdirs.
361+ func SelectMountable (containers []Container , release , hostABIID string ) []Container {
362+ selected := FilterByKernelVersion (containers , kernelVersionFromRelease (release ))
363+ selected = FilterByKernelABIID (selected , release , hostABIID )
364+
365+ keep := make (map [string ]bool , len (selected ))
366+ for _ , c := range selected {
367+ keep [c .MountPath ] = true
368+ }
369+ for i := range containers {
370+ if keep [containers [i ].MountPath ] {
371+ continue
372+ }
373+ if err := containers [i ].unmount (); err != nil {
374+ log .Printf ("Warning: failed to unmount dropped extension %s: %v" , containers [i ].Name , err )
375+ }
376+ }
377+ return selected
378+ }
196379
197380// Extension represents an OS-block overlay extension. Extensions passed in
198381// the leftExtensions slice of BuildOverlayOptions mount left of the hostapp
0 commit comments