Make mounts refuse what it could break: data the mesh keeps, a busy or non-empty mount point, its own share, and a mount that is not its source; say each node's wants so the server exports only to them (review of #136)

This commit is contained in:
jochen
2026-10-08 20:16:27 +02:00
parent 48fba44471
commit d5adc1d525
13 changed files with 747 additions and 31 deletions
+214
View File
@@ -0,0 +1,214 @@
package main
// What the module may not mount over (the review of 2026-10-08, findings 4a–4d): the mesh's own data on
// this machine, a mount point that something else uses, a folder that holds files, and a share this
// machine serves itself. And what the bundle leaves for the process so that it can tell: the paths the
// mesh keeps data in on this machine, from the controller's `data` verb over the bus.
import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// DataFile is what the bundle left from mesh-controller.data for this machine.
const DataFile = BusDir + "/data.json"
// DataPaths is where the mesh keeps data on this machine, as its controller says.
type DataPaths struct {
Machine string `json:"machine"`
Asked string `json:"asked"`
Paths []string `json:"paths"`
// Error is why the last ask got no answer; the paths are then the last answer's, or none.
Error string `json:"error,omitempty"`
Answered string `json:"answered,omitempty"`
}
// Want is one share this machine asks of one server, put on the bus so that the server exports the share
// to this machine and to nobody who does not ask (finding 5). The server decides the access it gives; this
// is only what is asked.
type Want struct {
Server string `json:"server"`
Share string `json:"share"`
Access string `json:"access"`
}
// Wants are what a config asks of its server, by share name.
func (c Config) Wants() []Want {
out := []Want{}
server := c.ServerNode()
if server == "" {
return out
}
for _, w := range c.Shares {
access := "rw"
if w.ReadOnly {
access = "ro"
}
out = append(out, Want{Server: server, Share: w.Name, Access: access})
}
sortWants(out)
return out
}
func sortWants(w []Want) {
for i := 1; i < len(w); i++ {
for j := i; j > 0 && w[j].Share < w[j-1].Share; j-- {
w[j], w[j-1] = w[j-1], w[j]
}
}
}
// DataFrom reads the paths out of mesh-controller.data's answer: `answer.data`, `data`, or the same as
// text in `output`. An answer with no data list is an error, never an empty list: an empty list would
// let every mount point through.
func DataFrom(raw json.RawMessage) ([]string, error) {
type item struct {
Path string `json:"path"`
}
var whole struct {
Answer *struct {
Data *[]item `json:"data"`
} `json:"answer"`
Data *[]item `json:"data"`
Output string `json:"output"`
}
if err := json.Unmarshal(raw, &whole); err != nil {
return nil, fmt.Errorf("the controller's data answer is not JSON: %v", err)
}
var items *[]item
switch {
case whole.Answer != nil && whole.Answer.Data != nil:
items = whole.Answer.Data
case whole.Data != nil:
items = whole.Data
case whole.Output != "":
var inner struct {
Data *[]item `json:"data"`
}
if err := json.Unmarshal([]byte(whole.Output), &inner); err == nil && inner.Data != nil {
items = inner.Data
}
}
if items == nil {
return nil, errors.New("the controller's data answer names no data list")
}
paths := []string{}
for _, it := range *items {
if strings.HasPrefix(it.Path, "/") {
paths = append(paths, filepath.Clean(it.Path))
}
}
return paths, nil
}
// nearData refuses a mount point at, under or above a path the mesh keeps data in here: at or under, the
// mount would hide the data; above, it would hide it too, and everything beside it.
func nearData(point string, paths []string) error {
for _, d := range paths {
switch {
case point == d:
return fmt.Errorf("the mount point %s is at %s, where the mesh keeps data on this machine; it is never mounted over", point, d)
case strings.HasPrefix(point, d+"/"):
return fmt.Errorf("the mount point %s is under %s, where the mesh keeps data on this machine; it is never mounted over", point, d)
case strings.HasPrefix(d, point+"/"):
return fmt.Errorf("the mount point %s is above %s, where the mesh keeps data on this machine; mounting there would hide it", point, d)
}
}
return nil
}
// sourceIs says whether what is mounted at a point is a pair's own source. For a network share the source
// is compared as the kernel writes it; for a disk, the device its by-uuid link resolves to.
func (m *Machine) sourceIs(kind, what string, mt Mounted) bool {
switch kind {
case "nfs":
return strings.HasPrefix(mt.Type, "nfs") && strings.TrimSuffix(mt.Source, "/") == strings.TrimSuffix(what, "/")
case "smb":
return (mt.Type == "cifs" || mt.Type == "smb3") && strings.EqualFold(strings.TrimSuffix(mt.Source, "/"), strings.TrimSuffix(what, "/"))
case "disk":
dev, err := filepath.EvalSymlinks(filepath.Join(m.Root, what))
if err != nil {
return false
}
src, err := filepath.EvalSymlinks(filepath.Join(m.Root, mt.Source))
return err == nil && src == dev
}
return false
}
// kindOf is a unit's kind from what it mounts.
func kindOf(what, fstype string) string {
switch {
case strings.HasPrefix(fstype, "nfs"):
return "nfs"
case fstype == "cifs" || strings.HasPrefix(what, "//"):
return "smb"
case strings.HasPrefix(what, "/dev/"):
return "disk"
}
return ""
}
// notOwnSource says why a mounted filesystem at a point is not the source a unit of the module mounts
// there, or nothing when it is, or nothing is mounted there but an automount.
func (m *Machine) notOwnSource(kind, what, point string, mounted map[string]Mounted) error {
mt, there := mounted[point]
if !there || mt.Type == "autofs" {
return nil
}
if kind != "" && m.sourceIs(kind, what, mt) {
return nil
}
return fmt.Errorf("%s is mounted at %s (%s), which is not what the module mounts there (%s)", mt.Source, point, mt.Type, what)
}
// mountpointFree refuses a mount point another mount uses, or a folder that holds files. A point in
// mountinfo is never opened, so a network server that is away cannot hold the look.
func (m *Machine) mountpointFree(p Pair, own map[string]bool, mounted map[string]Mounted) error {
ours := own[p.MountUnit] && own[p.AutoUnit]
if mt, there := mounted[p.Mountpoint]; there {
if mt.Type == "autofs" {
if ours {
return nil
}
return fmt.Errorf("%s is already an automount that is not the module's (%s)", p.Mountpoint, mt.Source)
}
if ours && m.sourceIs(p.Kind, p.What, mt) {
return nil
}
return fmt.Errorf("%s is already mounted: %s (%s) is there, so this share is not mounted over it", p.Mountpoint, mt.Source, mt.Type)
}
dir, err := os.Open(filepath.Join(m.Root, p.Mountpoint))
if err != nil {
return nil // missing: it is made
}
defer dir.Close()
info, err := dir.Stat()
if err != nil {
return fmt.Errorf("%s cannot be looked at: %v", p.Mountpoint, err)
}
if !info.IsDir() {
return fmt.Errorf("%s is a file, not a folder", p.Mountpoint)
}
if names, err := dir.Readdirnames(1); err == nil && len(names) > 0 {
return fmt.Errorf("%s is not empty (it holds %s); a mount would hide what is in it, so it is not mounted there", p.Mountpoint, names[0])
} else if err != nil && err != io.EOF {
return fmt.Errorf("%s cannot be read: %v", p.Mountpoint, err)
}
return nil
}
// unitField reads one Key= of a unit file.
func unitField(text, key string) string {
for _, line := range strings.Split(text, "\n") {
if v, ok := strings.CutPrefix(strings.TrimSpace(line), key+"="); ok {
return strings.TrimSpace(v)
}
}
return ""
}