Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

syscall: Unshare with CLONE_NEWUSER fails #22283

Closed
rvolosatovs opened this issue Oct 15, 2017 · 9 comments
Closed

syscall: Unshare with CLONE_NEWUSER fails #22283

rvolosatovs opened this issue Oct 15, 2017 · 9 comments

Comments

@rvolosatovs
Copy link

rvolosatovs commented Oct 15, 2017

What version of Go are you using (go version)?

go version go1.9.1 linux/amd64

Does this issue reproduce with the latest release?

yes

What operating system and processor architecture are you using (go env)?

GOARCH="amd64"
GOBIN="/home/rvolosatovs/.local/bin.go"
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"

What did you do?

main.c

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("%d\n", unshare(CLONE_NEWUSER));
    return 0;
}

main.go:

package main

import (
	"fmt"
	"os"
	"syscall"
)

func main() {
	fmt.Printf("%d\n", syscall.Unshare(syscall.CLONE_NEWUSER))
	os.Exit(0)
}

main_cgo.go:

package main

// #define _GNU_SOURCE
// #include <sched.h>
// #include <stdio.h>
import "C"

import (
	"fmt"
	"os"
)

func main() {
	fmt.Printf("%d\n", C.unshare(C.CLONE_NEWUSER))
	os.Exit(0)
}

compile and run each of those with:

unshare -U echo 0 &&\
gcc -o main main.c && ./main &&\
go build -o main main.go && ./main &&\
go build -o main main_cgo.go && ./main

What did you expect to see?

0
0

0

0

What did you see instead?

0
0


22


-1

The errno 22 returned by main.go implementation corresponds to syscall.EINVAL, "invalid argument"

@rvolosatovs rvolosatovs changed the title Unshare with CLONE_NEWUSER fails syscall.Unshare with CLONE_NEWUSER fails Oct 15, 2017
@ianlancetaylor ianlancetaylor changed the title syscall.Unshare with CLONE_NEWUSER fails syscall: Unshare with CLONE_NEWUSER fails Oct 15, 2017
@ianlancetaylor
Copy link
Contributor

My understanding is that the unshare system call is basically impossible to use correctly in a multi-threaded programs, and Go programs are always multi-threaded. So unfortunately I don't think there is any safe way to use syscall.Unshare.

One thing that you can safely do is use the os/exec package to start a new program (or to re-exec the same program), setting `SysProcAttr.Unshareflags to the unshare flags you want to use for the new program.

I'm going to close this since I don't think there is any possible resolution. Please comment if you disagree.

@rvolosatovs
Copy link
Author

I see your point, but in my particular use case I need to bind mount a directory in the file system root as a non-root user and afaik there's no way to do so using "just" os/exec and so I wanted to use the unshare for this specific goal.

Excerpt from the program:

	if err = syscall.Unshare(unshareFlags); err != nil {
		log.Fatal(errors.Wrap(err, "Failed to unshare()"))
	}

	f, err := os.Open(hostRoot)
	if err != nil {
		log.Fatal(errors.Wrapf(err, "Failed to open %s", hostRoot))
	}

	names, err := f.Readdirnames(-1)
	if err != nil {
		log.Fatal(errors.Wrapf(err, "Failed to read directory names at %s", hostRoot))
	}

	m := make(map[string]string, len(names)+1)
	for _, name := range names {
		if name == "nix" {
			continue
		}
		m[filepath.Join(hostRoot, name)] = filepath.Join(rootPath, name)
	}
	m[nixPath] = filepath.Join(rootPath, "nix")

	for hostPath, guestPath := range m {
		fi, err := os.Stat(hostPath)
		if err != nil {
			log.Fatal(errors.Wrapf(err, "Failed to stat() %s", hostPath))
		}

		mode := fi.Mode() &^ syscall.S_IFMT
		if err = os.Mkdir(guestPath, mode); err != nil {
			log.Fatal(errors.Wrapf(err, "Failed to create new directory at %s with mode %s", guestPath, mode))
		}

		if err := syscall.Mount(hostPath, guestPath, "none", syscall.MS_BIND|syscall.MS_REC, ""); err != nil {
			log.Fatal(errors.Wrapf(err, "Failed to bind mount %s to %s", hostRoot, guestPath))
		}
	}

I need to setup a chroot environment as a non-root user.

Any suggestions how I could do that, or is it not possible to do so with Go?

@ianlancetaylor
Copy link
Contributor

Is that the same as #12125?

@rvolosatovs
Copy link
Author

rvolosatovs commented Oct 15, 2017

#12125 is a proposal for a feature, but this is a bugreport. The essential idea is the same though.

Regardless, solution from #12125 is pretty much exactly what I tried to do and it fails with syscall.EINVAL, errno 22 (same as in my first post), which should not happen.
Also, you can see that it works both in C implementation and using the unshare directly from shell.

@ianlancetaylor
Copy link
Contributor

The C program is a single-threaded program. The Go program is a multi-threaded program (all Go programs are multi-threaded programs). You can't reliably use unshare in a multi-threaded program.

@rvolosatovs
Copy link
Author

Okay, so it's not possible to do bind mounts as a non-root user in Go, right?
Is #12125 (comment) (which you suggested) the only solution to the problem then?
because #12125 (comment) fails with the same "invalid argument" error.

@myitcv
Copy link
Member

myitcv commented Oct 16, 2017

@rvolosatovs I'm doing something similar; I have a simple Go program https://github.com/myitcv/g/blob/master/cmd/unsharemounts/main.go (could just as easily be C code as @ianlancetaylor points out) that runs setuid as root which performs the intermediate CLONE and then exec's a child process having reduced privilege escalation.

The code is old, probably could be done a better way... but it works for my requirements.

The background to my requirement is http://blog.myitcv.io/2014/03/18/using-process-namespaces-to-implement-variant-symlinks.html

@ianlancetaylor
Copy link
Contributor

@rvolosatovs I do not know enough about this to usefully comment. The point of #12125, which is still open, is to figure out some useful way to do bind mounts in Go. Any comment on that topic should go onto that issue, not this one. Thanks.

@Omnifarious
Copy link

@rvolosatovs - I don't understand why execing yourself wouldn't work in this case. It seems simple enough. Just exec yourself with a special parameter (or even environment variable) set to tell the new version of yourself not to do it again. Heck, you could even test whether or not a file previously owned by root is now owned by nobody or any number of other things.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

5 participants