summary refs log tree commit diff
path: root/pkgs/build-support/build-fhs-userenv/chroot-user.rb
blob: b7d6276ceab0e4de6ed97f080ea3ff3462c0ec6d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#!/usr/bin/env ruby

# Bind mounts hierarchy: [from, to (relative)]
# If 'to' is nil, path will be the same
mounts = [ ['/nix/store', nil],
           ['/dev', nil],
           ['/proc', nil],
           ['/sys', nil],
           ['/etc', 'host-etc'],
           ['/tmp', 'host-tmp'],
           ['/home', nil],
           ['/var', nil],
           ['/run', nil],
           ['/root', nil],
         ]

# Propagate environment variables
envvars = [ 'TERM',
            'DISPLAY',
            'HOME',
            'XDG_RUNTIME_DIR',
            'LANG',
            'SSL_CERT_FILE',
          ]

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
$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 swdir program args..." unless ARGV.length >= 2
swdir = Pathname.new ARGV[0]
execp = ARGV.drop 1

# Set destination paths for mounts
mounts.map! { |x| [x[0], x[1].nil? ? x[0].sub(/^\/*/, '') : x[1]] }

# 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
  # 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"

  # Do rbind mounts.
  mounts.each do |x|
    to = "#{root}/#{x[1]}"
    FileUtils.mkdir_p to
    $mount.call x[0], to, nil, MS_BIND | MS_REC, nil
  end

  # Chroot!
  Dir.chroot root
  Dir.chdir '/'

  # Symlink swdir hierarchy
  mount_dirs = Set.new mounts.map { |x| Pathname.new x[1] }
  link_swdir = lambda do |swdir, prefix|
    swdir.find do |path|
      rel = prefix.join path.relative_path_from(swdir)
      # Don't symlink anything in binded or symlinked directories
      Find.prune if mount_dirs.include? rel or rel.symlink?
      if not rel.directory?
        # File does not exist; make a symlink and bail out
        rel.make_symlink path
        Find.prune
      end
      # Recursively follow symlinks
      link_swdir.call path.readlink, rel if path.symlink?
    end
  end
  link_swdir.call swdir, Pathname.new('')

  # New environment
  ENV.replace(Hash[ envvars.map { |x| [x, ENV[x]] } ])

  # Finally, exec!
  exec *execp
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