SMB shares and NFS exports may now point to any existing directory (USB disks under /mnt or /media, /srv, home folders...) instead of only ZFS dataset mountpoints. System trees (/etc, /usr, /var, /boot, /proc, /root, ...) are refused, also after resolving symlinks. The path suggestions list mounted data filesystems from /proc/self/mounts in addition to ZFS mountpoints. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
package shares
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// pseudoFS are filesystem types that never hold user data.
|
||||
var pseudoFS = map[string]bool{
|
||||
"proc": true, "sysfs": true, "tmpfs": true, "devtmpfs": true, "devpts": true, "cgroup": true, "cgroup2": true,
|
||||
"securityfs": true, "pstore": true, "bpf": true, "debugfs": true, "tracefs": true, "configfs": true,
|
||||
"fusectl": true, "mqueue": true, "hugetlbfs": true, "autofs": true, "binfmt_misc": true, "nsfs": true,
|
||||
"overlay": true, "squashfs": true, "efivarfs": true, "rpc_pipefs": true, "nfsd": true, "ramfs": true,
|
||||
}
|
||||
|
||||
// ParseMounts extracts shareable mountpoints from /proc/self/mounts content:
|
||||
// real filesystems outside system directories, sorted and de-duplicated.
|
||||
func ParseMounts(data string) []string {
|
||||
seen := map[string]bool{}
|
||||
var out []string
|
||||
for _, line := range strings.Split(data, "\n") {
|
||||
f := strings.Fields(line)
|
||||
if len(f) < 3 || pseudoFS[f[2]] {
|
||||
continue
|
||||
}
|
||||
mp := unescapeMount(f[1])
|
||||
if !strings.HasPrefix(mp, "/") || ValidateNotSystem(mp) != nil || seen[mp] {
|
||||
continue
|
||||
}
|
||||
seen[mp] = true
|
||||
out = append(out, mp)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// unescapeMount decodes the octal escapes (\040 etc.) the kernel uses in mounts.
|
||||
func unescapeMount(s string) string {
|
||||
if !strings.Contains(s, `\`) {
|
||||
return s
|
||||
}
|
||||
var b strings.Builder
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '\\' && i+4 <= len(s) {
|
||||
if n, err := strconv.ParseUint(s[i+1:i+4], 8, 8); err == nil {
|
||||
b.WriteByte(byte(n))
|
||||
i += 3
|
||||
continue
|
||||
}
|
||||
}
|
||||
b.WriteByte(s[i])
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
@@ -102,7 +102,7 @@ func RenderExports(c Config, uid, gid string) string {
|
||||
default:
|
||||
opts = append(opts, "root_squash", "anonuid="+uid, "anongid="+gid)
|
||||
}
|
||||
// crossmnt: child ZFS datasets are separate mounts and must be reachable
|
||||
// crossmnt: child mounts (ZFS datasets or other filesystems) must be reachable
|
||||
opts = append(opts, "crossmnt", "no_subtree_check")
|
||||
var parts []string
|
||||
for _, cl := range e.Clients {
|
||||
|
||||
@@ -70,12 +70,13 @@ func TestValidation(t *testing.T) {
|
||||
t.Errorf("%q accepted", n)
|
||||
}
|
||||
}
|
||||
for _, p := range []string{"/nas/x", "/tmp_pool"} {
|
||||
for _, p := range []string{"/nas/x", "/tmp_pool", "/mnt/usb", "/srv/media", "/home/alice/docs", "/media/disk", "/variable", "/etcetera"} {
|
||||
if err := ValidatePath(p); err != nil {
|
||||
t.Errorf("%q rejected: %v", p, err)
|
||||
}
|
||||
}
|
||||
for _, p := range []string{"", "/", "nas/x", "/nas/../etc", "/nas/", "/nas/x\"y", "/nas/x\n"} {
|
||||
for _, p := range []string{"", "/", "nas/x", "/nas/../etc", "/nas/", "/nas/x\"y", "/nas/x\n",
|
||||
"/etc", "/etc/samba", "/var/lib/docker", "/usr", "/proc/1", "/root", "/boot/efi", "/run/user/1000"} {
|
||||
if ValidatePath(p) == nil {
|
||||
t.Errorf("%q accepted", p)
|
||||
}
|
||||
@@ -336,3 +337,27 @@ func TestUsers(t *testing.T) {
|
||||
t.Errorf("owner account deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMounts(t *testing.T) {
|
||||
in := `proc /proc proc rw,nosuid 0 0
|
||||
tmpfs /run tmpfs rw 0 0
|
||||
/dev/sda2 / ext4 rw 0 0
|
||||
/dev/sda1 /boot/efi vfat rw 0 0
|
||||
tank /tank zfs rw,xattr 0 0
|
||||
tank /tank zfs rw,xattr 0 0
|
||||
/dev/sdc1 /mnt/usb\040disk ext4 rw 0 0
|
||||
overlay /srv/docker/merged overlay rw 0 0
|
||||
`
|
||||
got := strings.Join(ParseMounts(in), "|")
|
||||
if got != "/mnt/usb disk|/tank" {
|
||||
t.Errorf("ParseMounts = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigRefusesSystemPath(t *testing.T) {
|
||||
c := Defaults()
|
||||
c.SMB.Shares = []SMBShare{{Name: "etc", Path: "/etc", Guest: true}}
|
||||
if c.Validate() == nil {
|
||||
t.Errorf("share on /etc accepted")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,8 +54,23 @@ func ValidateComment(s string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePath accepts an absolute, clean path without control characters.
|
||||
// Whether it lies under a pool is checked by the caller (needs live state).
|
||||
// systemRoots are OS trees that must never be shared.
|
||||
var systemRoots = []string{"/etc", "/proc", "/sys", "/dev", "/boot", "/run", "/usr", "/bin", "/sbin",
|
||||
"/lib", "/lib32", "/lib64", "/libx32", "/var", "/root", "/snap", "/lost+found"}
|
||||
|
||||
// ValidateNotSystem refuses the root filesystem and system directories.
|
||||
func ValidateNotSystem(p string) error {
|
||||
if p == "/" {
|
||||
return errors.New("refusing to share the root filesystem")
|
||||
}
|
||||
if UnderRoots(p, systemRoots) {
|
||||
return fmt.Errorf("%s is a system directory and cannot be shared", p)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePath accepts an absolute, clean, non-system path without control
|
||||
// characters. Existence and symlinks are checked by the caller.
|
||||
func ValidatePath(p string) error {
|
||||
if p == "" {
|
||||
return errors.New("path is required")
|
||||
@@ -63,10 +78,7 @@ func ValidatePath(p string) error {
|
||||
if !strings.HasPrefix(p, "/") || path.Clean(p) != p || strings.ContainsAny(p, "\"\\\r\n\t") {
|
||||
return errors.New("path must be absolute and normalised (no .., trailing / or quotes)")
|
||||
}
|
||||
if p == "/" {
|
||||
return errors.New("refusing to share the root filesystem")
|
||||
}
|
||||
return nil
|
||||
return ValidateNotSystem(p)
|
||||
}
|
||||
|
||||
// UnderRoots reports whether p equals or lies below one of the roots.
|
||||
|
||||
Reference in New Issue
Block a user