Change:
<del> #!/usr/bin/env ruby # Bind mounts hierarchy: from => to (relative) # If 'to' is nil, path will be the same mounts = { '/' => 'host', '/proc' => nil, '/sys' => nil, '/nix' => nil, '/tmp' => nil, '/var' => nil, '/run' => nil, '/dev' => nil, '/home' => nil, } # Propagate environment variables envvars = [ 'TERM', 'DISPLAY', 'XAUTHORITY', 'HOME', 'XDG_RUNTIME_DIR', 'LANG', 'SSL_CERT_FILE', 'DBUS_SESSION_BUS_ADDRESS', ] require 'tmpdir' require 'fileutils' require 'pathname' require 'set' require 'fiddle' def write_file(path, str) File.open(path, 'w') { |file| file.write str } end # Import C standard library and several needed calls $libc = Fiddle.dlopen nil def make_fcall(name, args, output) c = Fiddle::Function.new $libc[name], args, output lambda do |*args| ret = c.call *args raise SystemCallError.new Fiddle.last_error if ret < 0 return ret end end $fork = make_fcall 'fork', [], Fiddle::TYPE_INT CLONE_NEWNS   = 0x00020000 CLONE_NEWUSER = 0x10000000 $unshare = make_fcall 'unshare', [Fiddle::TYPE_INT], Fiddle::TYPE_INT MS_BIND = 0x1000 MS_REC  = 0x4000 MS_SLAVE  = 0x80000 $mount = make_fcall 'mount', [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG, Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT # Read command line args abort "Usage: chrootenv program args..." unless ARGV.length >= 1 execp = ARGV # Populate extra mounts if not ENV["CHROOTENV_EXTRA_BINDS"].nil? $stderr.puts "CHROOTENV_EXTRA_BINDS is discussed for deprecation." $stderr.puts "If you have a usecase, please drop a note in issue #16030." $stderr.puts "Notice that we now bind-mount host FS to '/host' and symlink all directories from it to '/' by default." for extra in ENV["CHROOTENV_EXTRA_BINDS"].split(':') paths = extra.split('=') if not paths.empty? if paths.size <= 2 mounts[paths[0]] = paths[1] else $stderr.puts "Ignoring invalid entry in CHROOTENV_EXTRA_BINDS: #{extra}" end end end end # Set destination paths for mounts mounts = mounts.map { |k, v| [k, v.nil? ? k.sub(/^/*/, '') : v] }.to_h # Create temporary directory for root and chdir root = Dir.mktmpdir 'chrootenv' # Fork process; we need this to do a proper cleanup because # child process will chroot into temporary directory. # We use imported 'fork' instead of native to overcome # CRuby's meddling with threads; this should be safe because # we don't use threads at all. $cpid = $fork.call if $cpid == 0 # If we are root, no need to create new user namespace. if Process.uid == 0 $unshare.call CLONE_NEWNS # Mark all mounted filesystems as slave so changes # don't propagate to the parent mount namespace. $mount.call nil, '/', nil, MS_REC | MS_SLAVE, nil else # Save user UID and GID uid = Process.uid gid = Process.gid # Create new mount and user namespaces # CLONE_NEWUSER requires a program to be non-threaded, hence # native fork above. $unshare.call CLONE_NEWNS | CLONE_NEWUSER # Map users and groups to the parent namespace begin # setgroups is only available since Linux 3.19 write_file '/proc/self/setgroups', 'deny' rescue end write_file '/proc/self/uid_map', "#{uid} #{uid} 1" write_file '/proc/self/gid_map', "#{gid} #{gid} 1" end # Do rbind mounts. mounts.each do |from, rto| to = "#{root}/#{rto}" FileUtils.mkdir_p to $mount.call from, to, nil, MS_BIND | MS_REC, nil end # Don't make root private so privilege drops inside chroot are possible File.chmod(0755, root) # Chroot! Dir.chroot root Dir.chdir '/' # New environment new_env = Hash[ envvars.map { |x| [x, ENV[x]] } ] # Finally, exec! exec(new_env, *execp, close_others: true, unsetenv_others: true) end # Wait for a child. If we catch a signal, resend it to child and continue # waiting. def wait_child begin Process.wait # Return child's exit code if $?.exited? exit $?.exitstatus else exit 1 end rescue SignalException => e Process.kill e.signo, $cpid wait_child end end begin wait_child ensure # Cleanup FileUtils.rm_rf root, secure: true end </del>

Discussion:
doesn't work inside sandbox. It can sometimes be useful to build, or unpack something in an environment that has a lot of assumptions (say, self-extracting checksummed executable, or build system that expects FHS-compliant paths). Given containing: Given : System: 18.03pre118631. (Impala) Nix version: 1.11.15 Nixpkgs version: Sandboxing enabled: yes /cc
I think at this point you would need to use the to do this. See e.g. .
I have noticed the same. I came up with this simpler FHS environment to run Altera Quartus installer on NixOS with sandbox build:
Thanks! This looks nice, and no Ruby dependency. BTW, just commenting out line 45 in script (one that raises the exception) solves the issue. Probably makes sense to mount and only if available.
Yes, +1 for working buildFHSUserEnv in sandboxed build.

consists mostly of syscalls (including ). There are even Ruby-specific workarounds. C/C++ would be a better language for the task. This should also reduce closure size and startup time of FHS-sandboxed apps (Ruby needs to load itself and its libraries from disk).
Can't we just use to setup the chroot?
I'm not sure if it's capable of doing all things that we need for this, but it should be worth checking. Can it run a container as a process (not as a systemd service), and can it be used not by a superuser?
needs root privileges. btw has C code to set them up, the rest is probably bash only?
D: A pity. Well, let's continue with the old plan then. Unfortunately we can't do much more in Bash than we already do because we need to do mounts in the same process we set the namespaces, in children necessary privileges are already gone. I'm not completely sure about this but I remember that when I tried to write this in Bash completely long time ago (using for setting up namespaces mounts and chroot were a problem.
How about using this one?
bind mounts, sysfs, tmpfs and procfs should be
I can confirm that these mounts are possible from using unprivileged lxc containers. But you cannot bind mount something from the host this way, because it needs to be done after . UPDATE: ok even that should be possible if the correct order is maintained: unshare -mount -chroot.
Oh, I should look at it! cc in case he's interested in this thread. I think that checks if the user is root -- that's why this doesn't work. This means that we need to call by ourselves... (I was going to hit "Comment" but you were faster ~_^) I expect that LXC containers do user maps such that you have UID 0 in new userns -- we, however, need to retain user uid.
why do we have to retain user id?
Multiple user apps use uid and username for their purposes -- we don't want to lie to them. I can't give a complete example (I remember that Steam breaks) but for example we have directories which apps can find based on uid.
I have another suggestion: the Go programming language.

Bear in mind that I'm generally not a super big fan of Go (in fact, while I use Go extensively at work, I generally use Haskell for my personal projects); however, I think that a higher level, simple (in terms of specification) language would be conducive to maintenance and further development. The binaries should be pretty small, and the closure should be pretty small, too. I might give that a go (no pun intended) and see what we think.
Several possible issues: need native and then before any calls to pthreads (or you'll get or something close); I remember that Go has a runtime so there may be problems; need several syscalls which may not be readily available, namely , and others. In Ruby I import them from libc dynamically and redefine constants from C by hand, but doing this (especially redefining constants) is one of the reasons I want to move from Ruby in the first place. However these are only potential problems -- I don't know if they actually exist! I'm actually interested in how this turns out.
You cannot use clone() properly because of the language runtime. That's why docker uses a little C for that job.
Ah, you must be referring to this from : I'll need to give that a thorough reading before I understand why the runtime gets in the way (unless, perhaps, you have an explanation of why/how pthreads mucks things up).
Oh, I'm guessing that's probably stuff like this: Though, if there are other details, I'd love to hear them :smile:.
If you refer to my remark about fork and pthreads, it's actually 's limitation. You can look at how I workaround this in Ruby by calling imported instead of Ruby's builtin. You will know if you encounter it -- will just fail. I think the best way to find out is with an experiment. (notice that I don't know about Go runtime and haven't even used Go before, so, as I underline again, those might be non-issues)
Interesting. I referred to the , but I don't see any mention of failure when used with pthreads. I don't doubt you; I'd just like to know how all this stuff works.
No problem even if these were doubts ~^.

From man page:
Ah, dunno why I didn't try reading the man page -- thanks for pointing that out :wink:.
Anybody done any more work on this? Also, this should probably be tagged with security, due to
a rewrite in a different language will not solve your grsecurity problems. I also do not see how the rewrite discussed here is related to security.
Anything related to grsecurity is security-related. A rewrite into a compiled language means that a setuid wrapper can be created, which means I don't have to run shit as root.
You can drop your privileges via right after you enter the environment as -- it's less convenient but you get roughly what you want.