package plugins
import (
"context"
"io/fs"
"path/filepath"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/tetratelabs/wazero"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/experimental/sysfs"
"github.com/tetratelabs/wazero/sys"
)
type jailedFS struct {
fs experimentalsys.FS
}
func escapes(path string) bool {
switch path {
case "", ".", "/":
return false
}
return !filepath.IsLocal(path)
}
func (jailedFS) Symlink(string, string) experimentalsys.Errno {
return experimentalsys.EPERM
}
func (j jailedFS) OpenFile(path string, flag experimentalsys.Oflag, perm fs.FileMode) (experimentalsys.File, experimentalsys.Errno) {
if escapes(path) {
return nil, experimentalsys.EPERM
}
return j.fs.OpenFile(path, flag, perm)
}
func (j jailedFS) Lstat(path string) (sys.Stat_t, experimentalsys.Errno) {
if escapes(path) {
return sys.Stat_t{}, experimentalsys.EPERM
}
return j.fs.Lstat(path)
}
func (j jailedFS) Stat(path string) (sys.Stat_t, experimentalsys.Errno) {
if escapes(path) {
return sys.Stat_t{}, experimentalsys.EPERM
}
return j.fs.Stat(path)
}
func (j jailedFS) Mkdir(path string, perm fs.FileMode) experimentalsys.Errno {
if escapes(path) {
return experimentalsys.EPERM
}
return j.fs.Mkdir(path, perm)
}
func (j jailedFS) Chmod(path string, perm fs.FileMode) experimentalsys.Errno {
if escapes(path) {
return experimentalsys.EPERM
}
return j.fs.Chmod(path, perm)
}
func (j jailedFS) Rename(from, to string) experimentalsys.Errno {
if escapes(from) || escapes(to) {
return experimentalsys.EPERM
}
return j.fs.Rename(from, to)
}
func (j jailedFS) Rmdir(path string) experimentalsys.Errno {
if escapes(path) {
return experimentalsys.EPERM
}
return j.fs.Rmdir(path)
}
func (j jailedFS) Unlink(path string) experimentalsys.Errno {
if escapes(path) {
return experimentalsys.EPERM
}
return j.fs.Unlink(path)
}
func (j jailedFS) Link(oldPath, newPath string) experimentalsys.Errno {
if escapes(oldPath) || escapes(newPath) {
return experimentalsys.EPERM
}
return j.fs.Link(oldPath, newPath)
}
func (j jailedFS) Readlink(path string) (string, experimentalsys.Errno) {
if escapes(path) {
return "", experimentalsys.EPERM
}
return j.fs.Readlink(path)
}
func (j jailedFS) Utimens(path string, atim, mtim int64) experimentalsys.Errno {
if escapes(path) {
return experimentalsys.EPERM
}
return j.fs.Utimens(path, atim, mtim)
}
type mount struct {
hostPath string
guestPath string
readOnly bool
}
func buildMounts(ctx context.Context, libraries model.Libraries, allowedLibraryIDs []int, allLibraries, allowWriteAccess bool) []mount {
allowedLibrarySet := make(map[int]struct{}, len(allowedLibraryIDs))
for _, id := range allowedLibraryIDs {
allowedLibrarySet[id] = struct{}{}
}
var mounts []mount
for _, lib := range libraries {
_, allowed := allowedLibrarySet[lib.ID]
if allLibraries || allowed {
m := mount{hostPath: lib.Path, guestPath: toPluginMountPoint(int32(lib.ID)), readOnly: !allowWriteAccess}
mounts = append(mounts, m)
log.Trace(ctx, "Added library to plugin mounts", "libraryID", lib.ID, "mountPoint", m.guestPath, "readOnly", m.readOnly, "hostPath", m.hostPath)
}
}
if allowWriteAccess {
log.Info(ctx, "Granting read-write filesystem access to libraries", "libraryCount", len(mounts), "allLibraries", allLibraries)
} else {
log.Debug(ctx, "Granting read-only filesystem access to libraries", "libraryCount", len(mounts), "allLibraries", allLibraries)
}
return mounts
}
func buildFSConfig(mounts []mount) wazero.FSConfig {
if len(mounts) == 0 {
return nil
}
cfg := wazero.NewFSConfig()
for _, m := range mounts {
var mounted experimentalsys.FS = jailedFS{fs: sysfs.DirFS(m.hostPath)}
if m.readOnly {
mounted = &sysfs.ReadFS{FS: mounted}
}
cfg = cfg.(sysfs.FSConfig).WithSysFSMount(mounted, m.guestPath)
}
return cfg
}