mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
241 lines
6.4 KiB
Go
241 lines
6.4 KiB
Go
// -*- Mode: Go; indent-tabs-mode: t -*-
|
|
|
|
/*
|
|
* Copyright (C) 2021 Canonical Ltd
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 3 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
package builtin
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/snapcore/snapd/interfaces"
|
|
"github.com/snapcore/snapd/interfaces/kmod"
|
|
"github.com/snapcore/snapd/snap"
|
|
)
|
|
|
|
const kernelModuleLoadSummary = `allows constrained control over kernel module loading`
|
|
|
|
const kernelModuleLoadBaseDeclarationPlugs = `
|
|
kernel-module-load:
|
|
allow-installation: false
|
|
deny-auto-connection: true
|
|
`
|
|
|
|
const kernelModuleLoadBaseDeclarationSlots = `
|
|
kernel-module-load:
|
|
allow-installation:
|
|
slot-snap-type:
|
|
- core
|
|
deny-connection: true
|
|
`
|
|
|
|
var modulesAttrTypeError = errors.New(`kernel-module-load "modules" attribute must be a list of dictionaries`)
|
|
|
|
// kernelModuleLoadInterface allows creating transient and persistent modules
|
|
type kernelModuleLoadInterface struct {
|
|
commonInterface
|
|
}
|
|
|
|
type loadOption int
|
|
|
|
const (
|
|
loadNone loadOption = iota
|
|
loadDenied
|
|
loadOnBoot
|
|
loadDynamic
|
|
)
|
|
|
|
type ModuleInfo struct {
|
|
name string
|
|
load loadOption
|
|
options string
|
|
}
|
|
|
|
var kernelModuleNameRegexp = regexp.MustCompile(`^[-a-zA-Z0-9_]+$`)
|
|
var kernelModuleOptionsRegexp = regexp.MustCompile(`^([a-zA-Z][a-zA-Z0-9_]*(=[[:graph:]]+)? *)+$`)
|
|
|
|
func enumerateModules(plug interfaces.Attrer, handleModule func(moduleInfo *ModuleInfo) error) error {
|
|
var modules []map[string]interface{}
|
|
err := plug.Attr("modules", &modules)
|
|
if err != nil && !errors.Is(err, snap.AttributeNotFoundError{}) {
|
|
return modulesAttrTypeError
|
|
}
|
|
|
|
for _, module := range modules {
|
|
name, ok := module["name"].(string)
|
|
if !ok {
|
|
return errors.New(`kernel-module-load "name" must be a string`)
|
|
}
|
|
|
|
var load loadOption
|
|
if loadAttr, found := module["load"]; found {
|
|
loadString, ok := loadAttr.(string)
|
|
if !ok {
|
|
return errors.New(`kernel-module-load "load" must be a string`)
|
|
}
|
|
|
|
switch loadString {
|
|
case "denied":
|
|
load = loadDenied
|
|
case "on-boot":
|
|
load = loadOnBoot
|
|
case "dynamic":
|
|
load = loadDynamic
|
|
default:
|
|
return fmt.Errorf(`kernel-module-load "load" value is unrecognized: %q`, loadString)
|
|
}
|
|
}
|
|
|
|
var options string
|
|
if optionsAttr, found := module["options"]; found {
|
|
options, ok = optionsAttr.(string)
|
|
if !ok {
|
|
return errors.New(`kernel-module-load "options" must be a string`)
|
|
}
|
|
}
|
|
|
|
moduleInfo := &ModuleInfo{
|
|
name: name,
|
|
load: load,
|
|
options: options,
|
|
}
|
|
|
|
if err := handleModule(moduleInfo); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateNameAttr(name string) error {
|
|
if !kernelModuleNameRegexp.MatchString(name) {
|
|
return errors.New(`kernel-module-load "name" attribute is not a valid module name`)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateOptionsAttr(moduleInfo *ModuleInfo) error {
|
|
if moduleInfo.options == "" {
|
|
return nil
|
|
}
|
|
|
|
if moduleInfo.load == loadDenied {
|
|
return errors.New(`kernel-module-load "options" attribute incompatible with "load: denied"`)
|
|
}
|
|
|
|
dynamicLoadingWithAnyOptions := moduleInfo.load == loadDynamic && moduleInfo.options == "*"
|
|
if !dynamicLoadingWithAnyOptions && !kernelModuleOptionsRegexp.MatchString(moduleInfo.options) {
|
|
return fmt.Errorf(`kernel-module-load "options" attribute contains invalid characters: %q`, moduleInfo.options)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validateModuleInfo(moduleInfo *ModuleInfo) error {
|
|
if err := validateNameAttr(moduleInfo.name); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := validateOptionsAttr(moduleInfo); err != nil {
|
|
return err
|
|
}
|
|
|
|
if moduleInfo.options == "" && moduleInfo.load == loadNone {
|
|
return errors.New(`kernel-module-load: must specify at least "load" or "options"`)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (iface *kernelModuleLoadInterface) BeforeConnectPlug(plug *interfaces.ConnectedPlug) error {
|
|
numModulesEntries := 0
|
|
err := enumerateModules(plug, func(moduleInfo *ModuleInfo) error {
|
|
numModulesEntries++
|
|
return validateModuleInfo(moduleInfo)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if numModulesEntries == 0 {
|
|
return modulesAttrTypeError
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (iface *kernelModuleLoadInterface) KModConnectedPlug(spec *kmod.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
|
|
snapInfo := plug.Snap()
|
|
commonDataDir := snapInfo.CommonDataDir()
|
|
|
|
err := enumerateModules(plug, func(moduleInfo *ModuleInfo) error {
|
|
var err error
|
|
switch moduleInfo.load {
|
|
case loadDenied:
|
|
err = spec.DisallowModule(moduleInfo.name)
|
|
case loadOnBoot:
|
|
err = spec.AddModule(moduleInfo.name)
|
|
if err != nil {
|
|
break
|
|
}
|
|
fallthrough
|
|
case loadNone, loadDynamic:
|
|
if len(moduleInfo.options) > 0 && moduleInfo.options != "*" {
|
|
// module options might include filesystem paths. Beside
|
|
// supporting hardcoded paths, it makes sense to support also
|
|
// paths to files provided by the snap; for this reason, we
|
|
// support expanding the $SNAP_COMMON variable here.
|
|
// We do not use os.Expand() because that supports both $ENV
|
|
// and ${ENV}, and we'd rather not alter the options which
|
|
// contain a "$" but are not meant to be expanded. Instead,
|
|
// just look for the "$SNAP_COMMON/" string and replace it; the
|
|
// extra "/" at the end ensures that the variable is
|
|
// terminated.
|
|
options := strings.ReplaceAll(moduleInfo.options, "$SNAP_COMMON/", commonDataDir+"/")
|
|
err = spec.SetModuleOptions(moduleInfo.name, options)
|
|
}
|
|
default:
|
|
// we can panic, this will be catched on validation
|
|
panic("Unsupported module load option")
|
|
}
|
|
return err
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (iface *kernelModuleLoadInterface) AutoConnect(*snap.PlugInfo, *snap.SlotInfo) bool {
|
|
return true
|
|
}
|
|
|
|
func init() {
|
|
registerIface(&kernelModuleLoadInterface{
|
|
commonInterface: commonInterface{
|
|
name: "kernel-module-load",
|
|
summary: kernelModuleLoadSummary,
|
|
baseDeclarationPlugs: kernelModuleLoadBaseDeclarationPlugs,
|
|
baseDeclarationSlots: kernelModuleLoadBaseDeclarationSlots,
|
|
implicitOnCore: true,
|
|
implicitOnClassic: true,
|
|
},
|
|
})
|
|
}
|