Compare commits
4 Commits
775a9f57c9
...
c9cd16fd2a
| Author | SHA1 | Date | |
|---|---|---|---|
|
c9cd16fd2a
|
|||
|
e42ea32dbe
|
|||
|
e7982b4ee9
|
|||
|
ef1ebf12d9
|
@@ -53,6 +53,9 @@ type (
|
||||
// Whether sharefs_init failed.
|
||||
initFailed bool
|
||||
|
||||
// Whether to create source directory as root.
|
||||
mkdir bool
|
||||
|
||||
// Open file descriptor to fuse.
|
||||
Fuse int
|
||||
|
||||
@@ -157,6 +160,9 @@ func parseOpts(args *fuseArgs, setup *setupState, log *log.Logger) (ok bool) {
|
||||
// Pathname to writable source directory.
|
||||
source *C.char
|
||||
|
||||
// Whether to create source directory as root.
|
||||
mkdir C.int
|
||||
|
||||
// Decimal string representation of uid to set when running as root.
|
||||
setuid *C.char
|
||||
// Decimal string representation of gid to set when running as root.
|
||||
@@ -169,6 +175,7 @@ func parseOpts(args *fuseArgs, setup *setupState, log *log.Logger) (ok bool) {
|
||||
|
||||
if C.fuse_opt_parse(args, unsafe.Pointer(&unsafeOpts), &[]C.struct_fuse_opt{
|
||||
{templ: C.CString("source=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.source)), value: 0},
|
||||
{templ: C.CString("mkdir"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.mkdir)), value: 1},
|
||||
{templ: C.CString("setuid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setuid)), value: 0},
|
||||
{templ: C.CString("setgid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setgid)), value: 0},
|
||||
|
||||
@@ -223,6 +230,7 @@ func parseOpts(args *fuseArgs, setup *setupState, log *log.Logger) (ok bool) {
|
||||
} else {
|
||||
setup.Source = a
|
||||
}
|
||||
setup.mkdir = unsafeOpts.mkdir != 0
|
||||
|
||||
if unsafeOpts.setuid == nil {
|
||||
setup.Setuid = -1
|
||||
@@ -314,7 +322,7 @@ func _main(s ...string) (exitCode int) {
|
||||
// hack to keep fuse_parse_cmdline happy in the container
|
||||
mountpoint := C.GoString(opts.mountpoint)
|
||||
pathnameArg := -1
|
||||
for i, arg := range os.Args {
|
||||
for i, arg := range s {
|
||||
if arg == mountpoint {
|
||||
pathnameArg = i
|
||||
break
|
||||
@@ -324,21 +332,43 @@ func _main(s ...string) (exitCode int) {
|
||||
log.Println("mountpoint must be absolute")
|
||||
return 2
|
||||
}
|
||||
os.Args[pathnameArg] = container.Nonexistent
|
||||
s[pathnameArg] = container.Nonexistent
|
||||
}
|
||||
|
||||
if !parseOpts(&args, &setup, msg.GetLogger()) {
|
||||
return 1
|
||||
}
|
||||
asRoot := os.Geteuid() == 0
|
||||
|
||||
if os.Geteuid() == 0 {
|
||||
if asRoot {
|
||||
if setup.Setuid <= 0 || setup.Setgid <= 0 {
|
||||
log.Println("setuid and setgid must not be 0")
|
||||
return 1
|
||||
}
|
||||
|
||||
if setup.Fuse >= 3 {
|
||||
log.Println("filesystem daemon must not run as root")
|
||||
return 1
|
||||
}
|
||||
|
||||
if setup.mkdir {
|
||||
if err := os.MkdirAll(setup.Source.String(), 0700); err != nil {
|
||||
if !errors.Is(err, os.ErrExist) {
|
||||
log.Println(err)
|
||||
return 1
|
||||
}
|
||||
// skip setup for existing source directory
|
||||
} else if err = os.Chown(setup.Source.String(), setup.Setuid, setup.Setgid); err != nil {
|
||||
log.Println(err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
} else if setup.Fuse < 3 && (setup.Setuid > 0 || setup.Setgid > 0) {
|
||||
log.Println("setuid and setgid has no effect when not starting as root")
|
||||
return 1
|
||||
} else if setup.mkdir {
|
||||
log.Println("mkdir has no effect when not starting as root")
|
||||
return 1
|
||||
}
|
||||
|
||||
op := C.struct_fuse_operations{
|
||||
@@ -382,11 +412,7 @@ func _main(s ...string) (exitCode int) {
|
||||
}
|
||||
}()
|
||||
|
||||
if os.Geteuid() == 0 {
|
||||
if setup.Setuid <= 0 || setup.Setgid <= 0 {
|
||||
log.Println("setuid and setgid must not be 0")
|
||||
return 5
|
||||
}
|
||||
if asRoot {
|
||||
if err := syscall.Setresgid(setup.Setgid, setup.Setgid, setup.Setgid); err != nil {
|
||||
log.Printf("cannot set gid: %v", err)
|
||||
return 5
|
||||
@@ -421,7 +447,7 @@ func _main(s ...string) (exitCode int) {
|
||||
} else {
|
||||
z.Path = a
|
||||
}
|
||||
z.Args = os.Args
|
||||
z.Args = s
|
||||
z.ForwardCancel = true
|
||||
z.SeccompPresets |= std.PresetStrict
|
||||
z.ParentPerm = 0700
|
||||
|
||||
@@ -3,14 +3,29 @@ package main
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// sharefsName is the prefix used by log.std in the sharefs process.
|
||||
const sharefsName = "sharefs"
|
||||
|
||||
// handleMountArgs returns an alternative, libfuse-compatible args slice for
|
||||
// args passed by mount -t fuse.sharefs [options] sharefs <mountpoint>.
|
||||
//
|
||||
// In this case, args always has a length of 5 with index 0 being what comes
|
||||
// after "fuse." in the filesystem type, 1 is the uninterpreted string passed
|
||||
// to mount (sharefsName is used as the magic string to enable this hack),
|
||||
// 2 is passed through to libfuse as mountpoint, and 3 is always "-o".
|
||||
func handleMountArgs(args []string) []string {
|
||||
if len(args) == 5 && args[1] == sharefsName && args[3] == "-o" {
|
||||
return []string{sharefsName, args[2], "-o", args[4]}
|
||||
}
|
||||
return slices.Clone(args)
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix(sharefsName + ": ")
|
||||
|
||||
os.Exit(_main(os.Args...))
|
||||
os.Exit(_main(handleMountArgs(os.Args)...))
|
||||
}
|
||||
|
||||
29
cmd/sharefs/main_test.go
Normal file
29
cmd/sharefs/main_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHandleMountArgs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
want []string
|
||||
}{
|
||||
{"nil", nil, nil},
|
||||
{"passthrough", []string{"sharefs", "-V"}, []string{"sharefs", "-V"}},
|
||||
{"replace", []string{"/sbin/sharefs", "sharefs", "/sdcard", "-o", "rw"}, []string{"sharefs", "/sdcard", "-o", "rw"}},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := handleMountArgs(tc.args); !slices.Equal(got, tc.want) {
|
||||
t.Errorf("handleMountArgs: %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{ pkgs, config, ... }:
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
users.users = {
|
||||
alice = {
|
||||
@@ -17,9 +17,6 @@
|
||||
environment = {
|
||||
# For benchmarking sharefs:
|
||||
systemPackages = [ pkgs.fsmark ];
|
||||
|
||||
# For sharefs option checks without adding to PATH:
|
||||
etc.sharefs.source = "${config.environment.hakurei.package}/libexec/sharefs";
|
||||
};
|
||||
|
||||
virtualisation = {
|
||||
|
||||
@@ -2,14 +2,14 @@ start_all()
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
# To check sharefs version:
|
||||
print(machine.succeed("/etc/sharefs -V"))
|
||||
print(machine.succeed("sharefs -V"))
|
||||
|
||||
# Make sure sharefs did not terminate:
|
||||
machine.wait_for_unit("sharefs.service")
|
||||
# Make sure sharefs started:
|
||||
machine.wait_for_unit("sdcard.mount")
|
||||
|
||||
machine.succeed("mkdir /mnt")
|
||||
def check_bad_opts_output(opts, want, source="/etc", privileged=False):
|
||||
output = machine.fail(("" if privileged else "sudo -u alice -i ") + f"/etc/sharefs -f -o source={source},{opts} /mnt 2>&1")
|
||||
output = machine.fail(("" if privileged else "sudo -u alice -i ") + f"sharefs -f -o source={source},{opts} /mnt 2>&1")
|
||||
if output != want:
|
||||
raise Exception(f"unexpected output: {output}")
|
||||
|
||||
@@ -27,6 +27,7 @@ check_bad_opts_output("setgid=-1", "sharefs: invalid value for option setgid\n")
|
||||
check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
|
||||
check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
|
||||
check_bad_opts_output("setuid=1023,setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
|
||||
check_bad_opts_output("mkdir", "sharefs: mkdir has no effect when not starting as root\n")
|
||||
|
||||
# Starting as root without setuid/setgid:
|
||||
check_bad_opts_output("allow_other", "sharefs: setuid and setgid must not be 0\n", privileged=True)
|
||||
@@ -37,6 +38,14 @@ check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid must not be 0\n
|
||||
machine.fail("umount /mnt")
|
||||
machine.succeed("rmdir /mnt")
|
||||
|
||||
# Unprivileged mount/unmount:
|
||||
machine.succeed("sudo -u alice -i mkdir /home/alice/{sdcard,persistent}")
|
||||
machine.succeed("sudo -u alice -i sharefs -o source=/home/alice/persistent /home/alice/sdcard")
|
||||
machine.succeed("sudo -u alice -i touch /home/alice/sdcard/check")
|
||||
machine.succeed("sudo -u alice -i umount /home/alice/sdcard")
|
||||
machine.succeed("sudo -u alice -i rm /home/alice/persistent/check")
|
||||
machine.succeed("sudo -u alice -i rmdir /home/alice/{sdcard,persistent}")
|
||||
|
||||
# Benchmark sharefs:
|
||||
machine.succeed("fs_mark -v -d /sdcard/fs_mark -l /tmp/fs_log.txt")
|
||||
machine.copy_from_vm("/tmp/fs_log.txt", "")
|
||||
|
||||
@@ -138,6 +138,10 @@
|
||||
;
|
||||
};
|
||||
hsu = pkgs.callPackage ./cmd/hsu/package.nix { inherit (self.packages.${system}) hakurei; };
|
||||
sharefs = pkgs.linkFarm "sharefs" {
|
||||
"bin/sharefs" = "${hakurei}/libexec/sharefs";
|
||||
"bin/mount.fuse.sharefs" = "${hakurei}/libexec/sharefs";
|
||||
};
|
||||
|
||||
dist = pkgs.runCommand "${hakurei.name}-dist" { buildInputs = hakurei.targetPkgs ++ [ pkgs.pkgsStatic.musl ]; } ''
|
||||
# go requires XDG_CACHE_HOME for the build cache
|
||||
|
||||
65
nixos.nix
65
nixos.nix
@@ -24,11 +24,38 @@ let
|
||||
getsubuid = userid: appid: userid * 100000 + 10000 + appid;
|
||||
getsubname = userid: appid: "u${toString userid}_a${toString appid}";
|
||||
getsubhome = userid: appid: "${cfg.stateDir}/u${toString userid}/a${toString appid}";
|
||||
|
||||
mountpoints = {
|
||||
${cfg.sharefs.name} = mkIf (cfg.sharefs.source != null) {
|
||||
depends = [ cfg.sharefs.source ];
|
||||
device = "sharefs";
|
||||
fsType = "fuse.sharefs";
|
||||
noCheck = true;
|
||||
options = [
|
||||
"rw"
|
||||
"noexec"
|
||||
"nosuid"
|
||||
"nodev"
|
||||
"noatime"
|
||||
"allow_other"
|
||||
"mkdir"
|
||||
"source=${cfg.sharefs.source}"
|
||||
"setuid=${toString config.users.users.${cfg.sharefs.user}.uid}"
|
||||
"setgid=${toString config.users.groups.${cfg.sharefs.group}.gid}"
|
||||
];
|
||||
};
|
||||
};
|
||||
in
|
||||
|
||||
{
|
||||
imports = [ (import ./options.nix packages) ];
|
||||
|
||||
options = {
|
||||
# Forward declare a dummy option for VM filesystems since the real one won't exist
|
||||
# unless the VM module is actually imported.
|
||||
virtualisation.fileSystems = lib.mkOption { };
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
assertions = [
|
||||
(
|
||||
@@ -66,41 +93,9 @@ in
|
||||
) "" cfg.users;
|
||||
};
|
||||
|
||||
systemd.services = {
|
||||
sharefs = mkIf (cfg.sharefs.source != null) {
|
||||
unitConfig.RequiresMountsFor = cfg.sharefs.source;
|
||||
serviceConfig = {
|
||||
NoNewPrivileges = true;
|
||||
};
|
||||
script = ''
|
||||
${pkgs.coreutils}/bin/install \
|
||||
-dm0700 \
|
||||
-o ${cfg.sharefs.user} \
|
||||
-g ${cfg.sharefs.group} \
|
||||
${cfg.sharefs.source} ${cfg.sharefs.name}
|
||||
|
||||
exec ${cfg.package}/libexec/sharefs -f \
|
||||
-o ${
|
||||
lib.join "," [
|
||||
"noexec"
|
||||
"nosuid"
|
||||
"nodev"
|
||||
"noatime"
|
||||
"auto_unmount"
|
||||
"allow_other"
|
||||
"setuid=$(id -u ${cfg.sharefs.user})"
|
||||
"setgid=$(id -g ${cfg.sharefs.group})"
|
||||
"source=${cfg.sharefs.source}"
|
||||
]
|
||||
} ${cfg.sharefs.name}
|
||||
'';
|
||||
|
||||
# do not unmount on configuration changes
|
||||
restartIfChanged = false;
|
||||
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
};
|
||||
};
|
||||
environment.systemPackages = optional (cfg.sharefs.source != null) cfg.sharefs.package;
|
||||
fileSystems = mountpoints;
|
||||
virtualisation.fileSystems = mountpoints;
|
||||
|
||||
home-manager =
|
||||
let
|
||||
|
||||
90
options.md
90
options.md
@@ -809,6 +809,96 @@ package
|
||||
|
||||
|
||||
|
||||
## environment\.hakurei\.sharefs\.package
|
||||
|
||||
|
||||
|
||||
The sharefs package to use\.
|
||||
|
||||
|
||||
|
||||
*Type:*
|
||||
package
|
||||
|
||||
|
||||
|
||||
*Default:*
|
||||
` <derivation sharefs> `
|
||||
|
||||
|
||||
|
||||
## environment\.hakurei\.sharefs\.group
|
||||
|
||||
|
||||
|
||||
Name of the group to run the sharefs daemon as\.
|
||||
|
||||
|
||||
|
||||
*Type:*
|
||||
string
|
||||
|
||||
|
||||
|
||||
*Default:*
|
||||
` "sharefs" `
|
||||
|
||||
|
||||
|
||||
## environment\.hakurei\.sharefs\.name
|
||||
|
||||
|
||||
|
||||
Host path to mount sharefs on\.
|
||||
|
||||
|
||||
|
||||
*Type:*
|
||||
string
|
||||
|
||||
|
||||
|
||||
*Default:*
|
||||
` "/sdcard" `
|
||||
|
||||
|
||||
|
||||
## environment\.hakurei\.sharefs\.source
|
||||
|
||||
|
||||
|
||||
Writable backing directory\. Setting this to null disables sharefs\.
|
||||
|
||||
|
||||
|
||||
*Type:*
|
||||
null or string
|
||||
|
||||
|
||||
|
||||
*Default:*
|
||||
` null `
|
||||
|
||||
|
||||
|
||||
## environment\.hakurei\.sharefs\.user
|
||||
|
||||
|
||||
|
||||
Name of the user to run the sharefs daemon as\.
|
||||
|
||||
|
||||
|
||||
*Type:*
|
||||
string
|
||||
|
||||
|
||||
|
||||
*Default:*
|
||||
` "sharefs" `
|
||||
|
||||
|
||||
|
||||
## environment\.hakurei\.shell
|
||||
|
||||
|
||||
|
||||
@@ -41,6 +41,12 @@ in
|
||||
};
|
||||
|
||||
sharefs = {
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = packages.${pkgs.stdenv.hostPlatform.system}.sharefs;
|
||||
description = "The sharefs package to use.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "sharefs";
|
||||
|
||||
Reference in New Issue
Block a user