gitee.com/mysnapcore/mysnapd@v0.1.0/interfaces/builtin/shared_memory.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 "io" 27 "path/filepath" 28 "strings" 29 30 "gitee.com/mysnapcore/mysnapd/dirs" 31 "gitee.com/mysnapcore/mysnapd/interfaces" 32 "gitee.com/mysnapcore/mysnapd/interfaces/apparmor" 33 "gitee.com/mysnapcore/mysnapd/interfaces/mount" 34 "gitee.com/mysnapcore/mysnapd/osutil" 35 "gitee.com/mysnapcore/mysnapd/snap" 36 ) 37 38 const sharedMemorySummary = `allows two snaps to use predefined shared memory objects` 39 40 // The plug side of shared-memory can operate in two modes: if the 41 // private attribute is set to true, then it can be connected to the 42 // implicit system slot to be given a private version of /dev/shm. 43 // 44 // For a plug without that attribute set, it will connect to a 45 // matching application snap slot - this is permitted even though the 46 // interface is super-privileged because using a slot requires a store 47 // declaration anyways so just declaring a plug will not grant access 48 // unless a slot was also granted at some point. 49 const sharedMemoryBaseDeclarationPlugs = ` 50 shared-memory: 51 allow-connection: 52 - 53 plug-attributes: 54 private: false 55 slot-attributes: 56 shared-memory: $PLUG(shared-memory) 57 - 58 plug-attributes: 59 private: true 60 slot-snap-type: 61 - core 62 allow-auto-connection: 63 - 64 plug-attributes: 65 private: false 66 slot-publisher-id: 67 - $PLUG_PUBLISHER_ID 68 slot-attributes: 69 shared-memory: $PLUG(shared-memory) 70 - 71 plug-attributes: 72 private: true 73 slot-snap-type: 74 - core 75 ` 76 77 // shared-memory slots can appear either as an implicit system slot, 78 // or as a slot on an application snap. 79 // 80 // The implicit version of the slot is intended to auto-connect with 81 // plugs that have the private attribute set to true. 82 // 83 // Slots on app snaps connect to non-private plugs. They are are 84 // super-privileged and thus denied to any snap except those that get 85 // a store declaration to do so, but the intent is for application or 86 // gadget snaps to use the slot much like the content interface. 87 const sharedMemoryBaseDeclarationSlots = ` 88 shared-memory: 89 allow-installation: 90 slot-snap-type: 91 - app 92 - gadget 93 - core 94 slot-snap-id: 95 - PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4 96 - 99T7MUlRhtI3U0QFgl5mXXESAiSwt776 97 deny-auto-connection: true 98 ` 99 100 const sharedMemoryPrivateConnectedPlugAppArmor = ` 101 # Description: Allow access to everything in private /dev/shm 102 "/dev/shm/*" mrwlkix, 103 ` 104 105 func validateSharedMemoryPath(path string) error { 106 if len(path) == 0 { 107 return fmt.Errorf("shared-memory interface path is empty") 108 } 109 110 if strings.TrimSpace(path) != path { 111 return fmt.Errorf("shared-memory interface path has leading or trailing spaces: %q", path) 112 } 113 114 // allow specifically only "*" globbing character, but disallow all other 115 // AARE characters 116 117 // same as from ValidateNoAppArmorRegexp, but with globbing 118 const aareWithoutGlob = `?[]{}^"` + "\x00" 119 if strings.ContainsAny(path, aareWithoutGlob) { 120 return fmt.Errorf("shared-memory interface path is invalid: %q contains a reserved apparmor char from %s", path, aareWithoutGlob) 121 } 122 123 // in addition to only allowing "*", we don't want to allow double "**" 124 // because "**" can traverse sub-directories as well which we don't want 125 if strings.Contains(path, "**") { 126 return fmt.Errorf("shared-memory interface path is invalid: %q contains ** which is unsupported", path) 127 } 128 129 // TODO: consider whether we should remove this check and allow full SHM path 130 if strings.Contains(path, "/") { 131 return fmt.Errorf("shared-memory interface path should not contain '/': %q", path) 132 } 133 134 // The check above protects from most unclean paths, but one could still specify ".." 135 if !cleanSubPath(path) { 136 return fmt.Errorf("shared-memory interface path is not clean: %q", path) 137 } 138 139 return nil 140 } 141 142 func stringListAttribute(attrer interfaces.Attrer, key string) ([]string, error) { 143 var stringList []string 144 err := attrer.Attr(key, &stringList) 145 if err != nil && !errors.Is(err, snap.AttributeNotFoundError{}) { 146 value, _ := attrer.Lookup(key) 147 return nil, fmt.Errorf(`shared-memory %q attribute must be a list of strings, not "%v"`, key, value) 148 } 149 150 return stringList, nil 151 } 152 153 // sharedMemoryInterface allows sharing sharedMemory between snaps 154 type sharedMemoryInterface struct{} 155 156 func (iface *sharedMemoryInterface) Name() string { 157 return "shared-memory" 158 } 159 160 func (iface *sharedMemoryInterface) StaticInfo() interfaces.StaticInfo { 161 return interfaces.StaticInfo{ 162 Summary: sharedMemorySummary, 163 BaseDeclarationPlugs: sharedMemoryBaseDeclarationPlugs, 164 BaseDeclarationSlots: sharedMemoryBaseDeclarationSlots, 165 AffectsPlugOnRefresh: true, 166 ImplicitOnCore: true, 167 ImplicitOnClassic: true, 168 } 169 } 170 171 func (iface *sharedMemoryInterface) BeforePrepareSlot(slot *snap.SlotInfo) error { 172 sharedMemoryAttr, isSet := slot.Attrs["shared-memory"] 173 sharedMemory, ok := sharedMemoryAttr.(string) 174 if isSet && !ok { 175 return fmt.Errorf(`shared-memory "shared-memory" attribute must be a string, not %v`, 176 slot.Attrs["shared-memory"]) 177 } 178 if sharedMemory == "" { 179 if slot.Attrs == nil { 180 slot.Attrs = make(map[string]interface{}) 181 } 182 // shared-memory defaults to "slot" name if unspecified 183 slot.Attrs["shared-memory"] = slot.Name 184 } 185 186 readPaths, err := stringListAttribute(slot, "read") 187 if err != nil { 188 return err 189 } 190 191 writePaths, err := stringListAttribute(slot, "write") 192 if err != nil { 193 return err 194 } 195 196 // We perform the same validation for read-only and writable paths, so 197 // let's just put them all in the same array 198 allPaths := append(readPaths, writePaths...) 199 if len(allPaths) == 0 { 200 return errors.New(`shared memory interface requires at least a valid "read" or "write" attribute`) 201 } 202 203 for _, path := range allPaths { 204 if err := validateSharedMemoryPath(path); err != nil { 205 return err 206 } 207 } 208 209 return nil 210 } 211 212 type sharedMemorySnippetType int 213 214 const ( 215 snippetForSlot sharedMemorySnippetType = iota 216 snippetForPlug 217 ) 218 219 func writeSharedMemoryPaths(w io.Writer, slot *interfaces.ConnectedSlot, 220 snippetType sharedMemorySnippetType) { 221 emitWritableRule := func(path string) { 222 // Ubuntu 14.04 uses /run/shm instead of the most common /dev/shm 223 fmt.Fprintf(w, "\"/{dev,run}/shm/%s\" mrwlk,\n", path) 224 } 225 226 // All checks were already done in BeforePrepare{Plug,Slot} 227 writePaths, _ := stringListAttribute(slot, "write") 228 for _, path := range writePaths { 229 emitWritableRule(path) 230 } 231 readPaths, _ := stringListAttribute(slot, "read") 232 for _, path := range readPaths { 233 if snippetType == snippetForPlug { 234 // grant read-only access 235 fmt.Fprintf(w, "\"/{dev,run}/shm/%s\" r,\n", path) 236 } else { 237 // the slot must still be granted write access, because the "read" 238 // and "write" attributes are meant to affect the plug only 239 emitWritableRule(path) 240 } 241 } 242 } 243 244 func (iface *sharedMemoryInterface) BeforePreparePlug(plug *snap.PlugInfo) error { 245 privateAttr, isPrivateSet := plug.Attrs["private"] 246 private, ok := privateAttr.(bool) 247 if isPrivateSet && !ok { 248 return fmt.Errorf(`shared-memory "private" attribute must be a bool, not %v`, privateAttr) 249 } 250 if plug.Attrs == nil { 251 plug.Attrs = make(map[string]interface{}) 252 } 253 plug.Attrs["private"] = private 254 255 sharedMemoryAttr, isSet := plug.Attrs["shared-memory"] 256 sharedMemory, ok := sharedMemoryAttr.(string) 257 if isSet && !ok { 258 return fmt.Errorf(`shared-memory "shared-memory" attribute must be a string, not %v`, 259 plug.Attrs["shared-memory"]) 260 } 261 if private { 262 if isSet { 263 return fmt.Errorf(`shared-memory "shared-memory" attribute must not be set together with "private: true"`) 264 } 265 // A private shared-memory plug cannot coexist with 266 // other shared-memory plugs/slots. 267 for _, other := range plug.Snap.Plugs { 268 if other != plug && other.Interface == "shared-memory" { 269 return fmt.Errorf(`shared-memory plug with "private: true" set cannot be used with other shared-memory plugs`) 270 } 271 } 272 for _, other := range plug.Snap.Slots { 273 if other.Interface == "shared-memory" { 274 return fmt.Errorf(`shared-memory plug with "private: true" set cannot be used with shared-memory slots`) 275 } 276 } 277 } else { 278 if sharedMemory == "" { 279 // shared-memory defaults to "plug" name if unspecified 280 plug.Attrs["shared-memory"] = plug.Name 281 } 282 } 283 284 return nil 285 } 286 287 func (iface *sharedMemoryInterface) isPrivate(plug *interfaces.ConnectedPlug) bool { 288 var private bool 289 if err := plug.Attr("private", &private); err == nil { 290 return private 291 } 292 panic("plug is not sanitized") 293 } 294 295 func (iface *sharedMemoryInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { 296 if iface.isPrivate(plug) { 297 spec.AddSnippet(sharedMemoryPrivateConnectedPlugAppArmor) 298 spec.AddUpdateNSf(` # Private /dev/shm 299 /dev/ r, 300 /dev/shm/{,**} rw, 301 mount options=(bind, rw) /dev/shm/snap.%s/ -> /dev/shm/, 302 umount /dev/shm/,`, plug.Snap().InstanceName()) 303 } else { 304 sharedMemorySnippet := &bytes.Buffer{} 305 writeSharedMemoryPaths(sharedMemorySnippet, slot, snippetForPlug) 306 spec.AddSnippet(sharedMemorySnippet.String()) 307 } 308 return nil 309 } 310 311 func (iface *sharedMemoryInterface) AppArmorConnectedSlot(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { 312 if slot.Snap().Type() == snap.TypeOS || slot.Snap().Type() == snap.TypeSnapd { 313 return nil 314 } 315 316 sharedMemorySnippet := &bytes.Buffer{} 317 writeSharedMemoryPaths(sharedMemorySnippet, slot, snippetForSlot) 318 spec.AddSnippet(sharedMemorySnippet.String()) 319 return nil 320 } 321 322 func (iface *sharedMemoryInterface) MountConnectedPlug(spec *mount.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { 323 if !iface.isPrivate(plug) { 324 return nil 325 } 326 327 devShm := filepath.Join(dirs.GlobalRootDir, "/dev/shm") 328 if osutil.IsSymlink(devShm) { 329 return fmt.Errorf(`shared-memory plug with "private: true" cannot be connected if %q is a symlink`, devShm) 330 } 331 332 return spec.AddMountEntry(osutil.MountEntry{ 333 Name: filepath.Join(devShm, "snap."+plug.Snap().InstanceName()), 334 Dir: "/dev/shm", 335 Options: []string{"bind", "rw"}, 336 }) 337 } 338 339 func (iface *sharedMemoryInterface) AutoConnect(plug *snap.PlugInfo, slot *snap.SlotInfo) bool { 340 // allow what declarations allowed 341 return true 342 } 343 344 func init() { 345 registerIface(&sharedMemoryInterface{}) 346 }