gitee.com/mysnapcore/mysnapd@v0.1.0/interfaces/builtin/custom_device.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2022 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package builtin 21 22 import ( 23 "bytes" 24 "errors" 25 "fmt" 26 "regexp" 27 "strings" 28 29 "gitee.com/mysnapcore/mysnapd/interfaces" 30 "gitee.com/mysnapcore/mysnapd/interfaces/apparmor" 31 "gitee.com/mysnapcore/mysnapd/interfaces/udev" 32 "gitee.com/mysnapcore/mysnapd/interfaces/utils" 33 "gitee.com/mysnapcore/mysnapd/snap" 34 "gitee.com/mysnapcore/mysnapd/strutil" 35 ) 36 37 const customDeviceSummary = `provides access to custom devices specified via the gadget snap` 38 39 const customDeviceBaseDeclarationSlots = ` 40 custom-device: 41 allow-installation: false 42 allow-connection: 43 plug-attributes: 44 content: $SLOT(custom-device) 45 deny-auto-connection: true 46 ` 47 48 var ( 49 // A cryptic, uninformative error message that we use only on impossible code paths 50 customDeviceInternalError = errors.New(`custom-device interface internal error`) 51 52 // Validating regexp for filesystem paths 53 customDevicePathRegexp = regexp.MustCompile(`^/[^"@]*$`) 54 55 // Validating regexp for udev device names. 56 // We forbid: 57 // - `|`: it's valid for udev, but more work for us 58 // - `{}`: have a special meaning for AppArmor 59 // - `"`: it's just dangerous (both for udev and AppArmor) 60 // - `\`: also dangerous 61 customDeviceUDevDeviceRegexp = regexp.MustCompile(`^/dev/[^"|{}\\]+$`) 62 63 // Validating regexp for udev tag values (all but kernel devices) 64 customDeviceUDevValueRegexp = regexp.MustCompile(`^[^"{}\\]+$`) 65 ) 66 67 // customDeviceInterface allows sharing customDevice between snaps 68 type customDeviceInterface struct{} 69 70 func (iface *customDeviceInterface) validateFilePath(path string, attrName string) error { 71 if !customDevicePathRegexp.MatchString(path) { 72 return fmt.Errorf(`custom-device %q path must start with / and cannot contain special characters: %q`, attrName, path) 73 } 74 75 if !cleanSubPath(path) { 76 return fmt.Errorf(`custom-device %q path is not clean: %q`, attrName, path) 77 } 78 79 if _, err := utils.NewPathPattern(path); err != nil { 80 return fmt.Errorf(`custom-device %q path cannot be used: %v`, attrName, err) 81 } 82 83 // We don't allow "**" because that's an AppArmor specific globbing pattern 84 // which we don't want to expose in our API contract. 85 if strings.Contains(path, "**") { 86 return fmt.Errorf(`custom-device %q path contains invalid glob pattern "**"`, attrName) 87 } 88 89 return nil 90 } 91 92 func (iface *customDeviceInterface) validateDevice(path string, attrName string) error { 93 // The device must satisfy udev's device name rules and generic path rules 94 if !customDeviceUDevDeviceRegexp.MatchString(path) { 95 return fmt.Errorf(`custom-device %q path must start with /dev/ and cannot contain special characters: %q`, attrName, path) 96 } 97 98 if err := iface.validateFilePath(path, attrName); err != nil { 99 return err 100 } 101 102 return nil 103 } 104 105 func (iface *customDeviceInterface) validatePaths(attrName string, paths []string) error { 106 for _, path := range paths { 107 if err := iface.validateFilePath(path, attrName); err != nil { 108 return err 109 } 110 } 111 112 return nil 113 } 114 115 func (iface *customDeviceInterface) validateUDevValue(value interface{}) error { 116 stringValue, ok := value.(string) 117 if !ok { 118 return fmt.Errorf(`value "%v" is not a string`, value) 119 } 120 121 if !customDeviceUDevValueRegexp.MatchString(stringValue) { 122 return fmt.Errorf(`value "%v" contains invalid characters`, stringValue) 123 } 124 125 return nil 126 } 127 128 func (iface *customDeviceInterface) validateUDevValueMap(value interface{}) error { 129 valueMap, ok := value.(map[string]interface{}) 130 if !ok { 131 return fmt.Errorf(`value "%v" is not a map`, value) 132 } 133 134 for key, val := range valueMap { 135 if !customDeviceUDevValueRegexp.MatchString(key) { 136 return fmt.Errorf(`key "%v" contains invalid characters`, key) 137 } 138 if err := iface.validateUDevValue(val); err != nil { 139 return err 140 } 141 } 142 143 return nil 144 } 145 146 func (iface *customDeviceInterface) validateUDevTaggingRule(rule map[string]interface{}, devices []string) error { 147 hasKernelTag := false 148 for key, value := range rule { 149 var err error 150 switch key { 151 case "subsystem": 152 err = iface.validateUDevValue(value) 153 case "kernel": 154 hasKernelTag = true 155 err = iface.validateUDevValue(value) 156 if err == nil { 157 deviceName := value.(string) 158 // furthermore, the kernel name must match the name of one of 159 // the given devices 160 if !strutil.ListContains(devices, "/dev/"+deviceName) { 161 err = fmt.Errorf(`%q does not match a specified device`, deviceName) 162 } 163 } 164 case "attributes", "environment": 165 err = iface.validateUDevValueMap(value) 166 default: 167 err = errors.New(`unknown tag`) 168 } 169 170 if err != nil { 171 return fmt.Errorf(`custom-device "udev-tagging" invalid %q tag: %v`, key, err) 172 } 173 } 174 175 if !hasKernelTag { 176 return errors.New(`custom-device udev tagging rule missing mandatory "kernel" key`) 177 } 178 179 return nil 180 } 181 182 func (iface *customDeviceInterface) Name() string { 183 return "custom-device" 184 } 185 186 func (iface *customDeviceInterface) StaticInfo() interfaces.StaticInfo { 187 return interfaces.StaticInfo{ 188 Summary: customDeviceSummary, 189 BaseDeclarationSlots: customDeviceBaseDeclarationSlots, 190 } 191 } 192 193 func (iface *customDeviceInterface) BeforePrepareSlot(slot *snap.SlotInfo) error { 194 if slot.Attrs == nil { 195 slot.Attrs = make(map[string]interface{}) 196 } 197 customDeviceAttr, isSet := slot.Attrs["custom-device"] 198 customDevice, ok := customDeviceAttr.(string) 199 if isSet && !ok { 200 return fmt.Errorf(`custom-device "custom-device" attribute must be a string, not %v`, 201 customDeviceAttr) 202 } 203 if customDevice == "" { 204 // custom-device defaults to "slot" name if unspecified 205 slot.Attrs["custom-device"] = slot.Name 206 } 207 208 var devices []string 209 err := slot.Attr("devices", &devices) 210 if err != nil && !errors.Is(err, snap.AttributeNotFoundError{}) { 211 return err 212 } 213 for _, device := range devices { 214 if err := iface.validateDevice(device, "devices"); err != nil { 215 return err 216 } 217 } 218 219 var readDevices []string 220 err = slot.Attr("read-devices", &readDevices) 221 if err != nil && !errors.Is(err, snap.AttributeNotFoundError{}) { 222 return err 223 } 224 for _, device := range readDevices { 225 if err := iface.validateDevice(device, "read-devices"); err != nil { 226 return err 227 } 228 if strutil.ListContains(devices, device) { 229 return fmt.Errorf(`cannot specify path %q both in "devices" and "read-devices" attributes`, device) 230 } 231 } 232 233 allDevices := devices 234 allDevices = append(allDevices, readDevices...) 235 236 // validate files 237 var filesMap map[string][]string 238 err = slot.Attr("files", &filesMap) 239 if err != nil && !errors.Is(err, snap.AttributeNotFoundError{}) { 240 return err 241 } 242 for key, val := range filesMap { 243 switch key { 244 case "read": 245 if err := iface.validatePaths("read", val); err != nil { 246 return err 247 } 248 case "write": 249 if err := iface.validatePaths("write", val); err != nil { 250 return err 251 } 252 default: 253 return fmt.Errorf(`cannot specify %q in "files" section, only "read" and "write" allowed`, key) 254 } 255 } 256 257 if len(allDevices) == 0 && len(filesMap) == 0 { 258 return fmt.Errorf("cannot use custom-device slot without any files or devices") 259 } 260 261 var udevTaggingRules []map[string]interface{} 262 err = slot.Attr("udev-tagging", &udevTaggingRules) 263 if err != nil && !errors.Is(err, snap.AttributeNotFoundError{}) { 264 return err 265 } 266 for _, udevTaggingRule := range udevTaggingRules { 267 if err := iface.validateUDevTaggingRule(udevTaggingRule, allDevices); err != nil { 268 return err 269 } 270 } 271 272 return nil 273 } 274 275 func (iface *customDeviceInterface) BeforePreparePlug(plug *snap.PlugInfo) error { 276 customDeviceAttr, isSet := plug.Attrs["custom-device"] 277 customDevice, ok := customDeviceAttr.(string) 278 if isSet && !ok { 279 return fmt.Errorf(`custom-device "custom-device" attribute must be a string, not %v`, 280 plug.Attrs["custom-device"]) 281 } 282 if customDevice == "" { 283 if plug.Attrs == nil { 284 plug.Attrs = make(map[string]interface{}) 285 } 286 // custom-device defaults to "plug" name if unspecified 287 plug.Attrs["custom-device"] = plug.Name 288 } 289 290 return nil 291 } 292 293 func (iface *customDeviceInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { 294 snippet := &bytes.Buffer{} 295 emitRule := func(paths []string, permissions string) { 296 for _, path := range paths { 297 fmt.Fprintf(snippet, "\"%s\" %s,\n", path, permissions) 298 } 299 } 300 301 // get all attributes without validation, since that was done before; 302 // should an error occur, we'll simply not write any rule. 303 304 var devicePaths []string 305 _ = slot.Attr("devices", &devicePaths) 306 emitRule(devicePaths, "rw") 307 308 var readDevicePaths []string 309 _ = slot.Attr("read-devices", &readDevicePaths) 310 emitRule(readDevicePaths, "r") 311 312 var filesMap map[string][]string 313 err := slot.Attr("files", &filesMap) 314 if err != nil && !errors.Is(err, snap.AttributeNotFoundError{}) { 315 return err 316 } 317 for key, val := range filesMap { 318 perm := "" 319 switch key { 320 case "read": 321 perm = "r" 322 case "write": 323 perm = "rw" 324 default: 325 return fmt.Errorf(`cannot specify %q in "files" section, only "read" and "write" allowed`, key) 326 } 327 328 emitRule(val, perm) 329 } 330 331 spec.AddSnippet(snippet.String()) 332 return nil 333 } 334 335 // extractStringMapAttribute looks up the given key in the container, and 336 // returns its value as a map[string]string. 337 // No validation is performed, since it already occurred before connecting the 338 // interface. 339 func (iface *customDeviceInterface) extractStringMapAttribute(container map[string]interface{}, key string) map[string]string { 340 valueMap, ok := container[key].(map[string]interface{}) 341 if !ok { 342 return nil 343 } 344 345 stringMap := make(map[string]string, len(valueMap)) 346 for key, value := range valueMap { 347 stringMap[key] = value.(string) 348 } 349 350 return stringMap 351 } 352 353 func (iface *customDeviceInterface) UDevConnectedPlug(spec *udev.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { 354 // Collect all the device paths specified in either the "devices" or 355 // "read-devices" attributes. 356 var devicePaths []string 357 _ = slot.Attr("devices", &devicePaths) 358 var readDevicePaths []string 359 _ = slot.Attr("read-devices", &readDevicePaths) 360 allDevicePaths := devicePaths 361 allDevicePaths = append(allDevicePaths, readDevicePaths...) 362 363 // Generate a basic udev rule for each device; we put them into a map 364 // indexed by the device name, so that we can overwrite the entry later 365 // with a more specific rule. 366 deviceRules := make(map[string]string, len(allDevicePaths)) 367 for _, devicePath := range allDevicePaths { 368 if strings.HasPrefix(devicePath, "/dev/") { 369 deviceName := devicePath[5:] 370 deviceRules[deviceName] = fmt.Sprintf(`KERNEL=="%s"`, deviceName) 371 } 372 } 373 374 // Generate udev rules from the "udev-tagging" attribute; note that these 375 // rules might override the simpler KERNEL=="<device>" rules we computed 376 // above -- that's fine. 377 var udevTaggingRules []map[string]interface{} 378 _ = slot.Attr("udev-tagging", &udevTaggingRules) 379 for _, udevTaggingRule := range udevTaggingRules { 380 rule := &bytes.Buffer{} 381 382 deviceName, ok := udevTaggingRule["kernel"].(string) 383 if !ok { 384 return customDeviceInternalError 385 } 386 387 fmt.Fprintf(rule, `KERNEL=="%s"`, deviceName) 388 389 if subsystem, ok := udevTaggingRule["subsystem"].(string); ok { 390 fmt.Fprintf(rule, `, SUBSYSTEM=="%s"`, subsystem) 391 } 392 393 environment := iface.extractStringMapAttribute(udevTaggingRule, "environment") 394 for variable, value := range environment { 395 fmt.Fprintf(rule, `, ENV{%s}=="%s"`, variable, value) 396 } 397 398 attributes := iface.extractStringMapAttribute(udevTaggingRule, "attributes") 399 for variable, value := range attributes { 400 fmt.Fprintf(rule, `, ATTR{%s}=="%s"`, variable, value) 401 } 402 403 deviceRules[deviceName] = rule.String() 404 } 405 406 // Now write all the rules 407 for _, rule := range deviceRules { 408 spec.TagDevice(rule) 409 } 410 411 return nil 412 } 413 414 func (iface *customDeviceInterface) AutoConnect(plug *snap.PlugInfo, slot *snap.SlotInfo) bool { 415 // allow what declarations allowed 416 return true 417 } 418 419 func init() { 420 registerIface(&customDeviceInterface{}) 421 }