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