gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/interfaces/builtin/content.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016 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 "fmt" 25 "path/filepath" 26 "strings" 27 28 "github.com/snapcore/snapd/interfaces" 29 "github.com/snapcore/snapd/interfaces/apparmor" 30 "github.com/snapcore/snapd/interfaces/mount" 31 "github.com/snapcore/snapd/osutil" 32 "github.com/snapcore/snapd/snap" 33 ) 34 35 const contentSummary = `allows sharing code and data with other snaps` 36 37 const contentBaseDeclarationSlots = ` 38 content: 39 allow-installation: 40 slot-snap-type: 41 - app 42 - gadget 43 allow-connection: 44 plug-attributes: 45 content: $SLOT(content) 46 allow-auto-connection: 47 plug-publisher-id: 48 - $SLOT_PUBLISHER_ID 49 plug-attributes: 50 content: $SLOT(content) 51 ` 52 53 // contentInterface allows sharing content between snaps 54 type contentInterface struct{} 55 56 func (iface *contentInterface) Name() string { 57 return "content" 58 } 59 60 func (iface *contentInterface) StaticInfo() interfaces.StaticInfo { 61 return interfaces.StaticInfo{ 62 Summary: contentSummary, 63 BaseDeclarationSlots: contentBaseDeclarationSlots, 64 65 AffectsPlugOnRefresh: true, 66 } 67 } 68 69 func cleanSubPath(path string) bool { 70 return filepath.Clean(path) == path && path != ".." && !strings.HasPrefix(path, "../") 71 } 72 73 func (iface *contentInterface) BeforePrepareSlot(slot *snap.SlotInfo) error { 74 content, ok := slot.Attrs["content"].(string) 75 if !ok || len(content) == 0 { 76 if slot.Attrs == nil { 77 slot.Attrs = make(map[string]interface{}) 78 } 79 // content defaults to "slot" name if unspecified 80 slot.Attrs["content"] = slot.Name 81 } 82 83 // Error if "read" or "write" are present alongside "source". 84 // TODO: use slot.Lookup() once PR 4510 lands. 85 var unused map[string]interface{} 86 if err := slot.Attr("source", &unused); err == nil { 87 var unused []interface{} 88 if err := slot.Attr("read", &unused); err == nil { 89 return fmt.Errorf(`move the "read" attribute into the "source" section`) 90 } 91 if err := slot.Attr("write", &unused); err == nil { 92 return fmt.Errorf(`move the "write" attribute into the "source" section`) 93 } 94 } 95 96 // check that we have either a read or write path 97 rpath := iface.path(slot, "read") 98 wpath := iface.path(slot, "write") 99 if len(rpath) == 0 && len(wpath) == 0 { 100 return fmt.Errorf("read or write path must be set") 101 } 102 103 // go over both paths 104 paths := rpath 105 paths = append(paths, wpath...) 106 for _, p := range paths { 107 if !cleanSubPath(p) { 108 return fmt.Errorf("content interface path is not clean: %q", p) 109 } 110 } 111 return nil 112 } 113 114 func (iface *contentInterface) BeforePreparePlug(plug *snap.PlugInfo) error { 115 content, ok := plug.Attrs["content"].(string) 116 if !ok || len(content) == 0 { 117 if plug.Attrs == nil { 118 plug.Attrs = make(map[string]interface{}) 119 } 120 // content defaults to "plug" name if unspecified 121 plug.Attrs["content"] = plug.Name 122 } 123 target, ok := plug.Attrs["target"].(string) 124 if !ok || len(target) == 0 { 125 return fmt.Errorf("content plug must contain target path") 126 } 127 if !cleanSubPath(target) { 128 return fmt.Errorf("content interface target path is not clean: %q", target) 129 } 130 131 return nil 132 } 133 134 // path is an internal helper that extract the "read" and "write" attribute 135 // of the slot 136 func (iface *contentInterface) path(attrs interfaces.Attrer, name string) []string { 137 if name != "read" && name != "write" { 138 panic("internal error, path can only be used with read/write") 139 } 140 141 var paths []interface{} 142 var source map[string]interface{} 143 144 if err := attrs.Attr("source", &source); err == nil { 145 // Access either "source.read" or "source.write" attribute. 146 var ok bool 147 if paths, ok = source[name].([]interface{}); !ok { 148 return nil 149 } 150 } else { 151 // Access either "read" or "write" attribute directly (legacy). 152 if err := attrs.Attr(name, &paths); err != nil { 153 return nil 154 } 155 } 156 157 out := make([]string, len(paths)) 158 for i, p := range paths { 159 var ok bool 160 out[i], ok = p.(string) 161 if !ok { 162 return nil 163 } 164 } 165 return out 166 } 167 168 // resolveSpecialVariable resolves one of the three $SNAP* variables at the 169 // beginning of a given path. The variables are $SNAP, $SNAP_DATA and 170 // $SNAP_COMMON. If there are no variables then $SNAP is implicitly assumed 171 // (this is the behavior that was used before the variables were supporter). 172 func resolveSpecialVariable(path string, snapInfo *snap.Info) string { 173 // Content cannot be mounted at arbitrary locations, validate the path 174 // for extra safety. 175 if err := snap.ValidatePathVariables(path); err == nil && strings.HasPrefix(path, "$") { 176 // The path starts with $ and ValidatePathVariables() ensures 177 // path contains only $SNAP, $SNAP_DATA, $SNAP_COMMON, and no 178 // other $VARs are present. It is ok to use 179 // ExpandSnapVariables() since it only expands $SNAP, $SNAP_DATA 180 // and $SNAP_COMMON 181 return snapInfo.ExpandSnapVariables(path) 182 } 183 // Always prefix with $SNAP if nothing else is provided or the path 184 // contains invalid variables. 185 return snapInfo.ExpandSnapVariables(filepath.Join("$SNAP", path)) 186 } 187 188 func sourceTarget(plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot, relSrc string) (string, string) { 189 var target string 190 // The 'target' attribute has already been verified in BeforePreparePlug. 191 _ = plug.Attr("target", &target) 192 source := resolveSpecialVariable(relSrc, slot.Snap()) 193 target = resolveSpecialVariable(target, plug.Snap()) 194 195 // Check if the "source" section is present. 196 var unused map[string]interface{} 197 if err := slot.Attr("source", &unused); err == nil { 198 _, sourceName := filepath.Split(source) 199 target = filepath.Join(target, sourceName) 200 } 201 return source, target 202 } 203 204 func mountEntry(plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot, relSrc string, extraOptions ...string) osutil.MountEntry { 205 options := make([]string, 0, len(extraOptions)+1) 206 options = append(options, "bind") 207 options = append(options, extraOptions...) 208 source, target := sourceTarget(plug, slot, relSrc) 209 return osutil.MountEntry{ 210 Name: source, 211 Dir: target, 212 Options: options, 213 } 214 } 215 216 func (iface *contentInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { 217 contentSnippet := bytes.NewBuffer(nil) 218 writePaths := iface.path(slot, "write") 219 emit := spec.AddUpdateNSf 220 if len(writePaths) > 0 { 221 fmt.Fprintf(contentSnippet, ` 222 # In addition to the bind mount, add any AppArmor rules so that 223 # snaps may directly access the slot implementation's files. Due 224 # to a limitation in the kernel's LSM hooks for AF_UNIX, these 225 # are needed for using named sockets within the exported 226 # directory. 227 `) 228 for i, w := range writePaths { 229 fmt.Fprintf(contentSnippet, "%s/** mrwklix,\n", 230 resolveSpecialVariable(w, slot.Snap())) 231 source, target := sourceTarget(plug, slot, w) 232 emit(" # Read-write content sharing %s -> %s (w#%d)\n", plug.Ref(), slot.Ref(), i) 233 emit(" mount options=(bind, rw) %s/ -> %s{,-[0-9]*}/,\n", source, target) 234 emit(" mount options=(rprivate) -> %s{,-[0-9]*}/,\n", target) 235 emit(" umount %s{,-[0-9]*}/,\n", target) 236 // TODO: The assumed prefix depth could be optimized to be more 237 // precise since content sharing can only take place in a fixed 238 // list of places with well-known paths (well, constrained set of 239 // paths). This can be done when the prefix is actually consumed. 240 apparmor.GenWritableProfile(emit, source, 1) 241 apparmor.GenWritableProfile(emit, target, 1) 242 apparmor.GenWritableProfile(emit, fmt.Sprintf("%s-[0-9]*", target), 1) 243 } 244 } 245 246 readPaths := iface.path(slot, "read") 247 if len(readPaths) > 0 { 248 fmt.Fprintf(contentSnippet, ` 249 # In addition to the bind mount, add any AppArmor rules so that 250 # snaps may directly access the slot implementation's files 251 # read-only. 252 `) 253 for i, r := range readPaths { 254 fmt.Fprintf(contentSnippet, "%s/** mrkix,\n", 255 resolveSpecialVariable(r, slot.Snap())) 256 257 source, target := sourceTarget(plug, slot, r) 258 emit(" # Read-only content sharing %s -> %s (r#%d)\n", plug.Ref(), slot.Ref(), i) 259 emit(" mount options=(bind) %s/ -> %s{,-[0-9]*}/,\n", source, target) 260 emit(" remount options=(bind, ro) %s{,-[0-9]*}/,\n", target) 261 emit(" mount options=(rprivate) -> %s{,-[0-9]*}/,\n", target) 262 emit(" umount %s{,-[0-9]*}/,\n", target) 263 // Look at the TODO comment above. 264 apparmor.GenWritableProfile(emit, source, 1) 265 apparmor.GenWritableProfile(emit, target, 1) 266 apparmor.GenWritableProfile(emit, fmt.Sprintf("%s-[0-9]*", target), 1) 267 } 268 } 269 270 spec.AddSnippet(contentSnippet.String()) 271 return nil 272 } 273 274 func (iface *contentInterface) AppArmorConnectedSlot(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { 275 contentSnippet := bytes.NewBuffer(nil) 276 writePaths := iface.path(slot, "write") 277 if len(writePaths) > 0 { 278 fmt.Fprintf(contentSnippet, ` 279 # When the content interface is writable, allow this slot 280 # implementation to access the slot's exported files at the plugging 281 # snap's mountpoint to accommodate software where the plugging app 282 # tells the slotting app about files to share. 283 `) 284 for _, w := range writePaths { 285 _, target := sourceTarget(plug, slot, w) 286 fmt.Fprintf(contentSnippet, "%s/** mrwklix,\n", 287 target) 288 } 289 } 290 291 spec.AddSnippet(contentSnippet.String()) 292 return nil 293 } 294 295 func (iface *contentInterface) AutoConnect(plug *snap.PlugInfo, slot *snap.SlotInfo) bool { 296 // allow what declarations allowed 297 return true 298 } 299 300 // Interactions with the mount backend. 301 302 func (iface *contentInterface) MountConnectedPlug(spec *mount.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { 303 for _, r := range iface.path(slot, "read") { 304 err := spec.AddMountEntry(mountEntry(plug, slot, r, "ro")) 305 if err != nil { 306 return err 307 } 308 } 309 for _, w := range iface.path(slot, "write") { 310 err := spec.AddMountEntry(mountEntry(plug, slot, w)) 311 if err != nil { 312 return err 313 } 314 } 315 return nil 316 } 317 318 func init() { 319 registerIface(&contentInterface{}) 320 }