1#!/usr/bin/python3 2# 3# Copyright (C) 2022 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17import json 18import os 19import textwrap 20 21 22def _bool(value): 23 """Convert string to one of None, True, False.""" 24 if isinstance(value, bool): 25 return value 26 if value is None: 27 return None 28 if value.to_lower() in ('', 'none', 'null'): 29 return None 30 if value in ('False', 'false'): 31 return False 32 return bool(value) 33 34 35class Envar(): 36 """Environment variables.""" 37 38 def __init__(self, *args, name=None, value=None): 39 assert not args, "Envar only accepts kwargs" 40 self.name = name 41 self.value = value 42 43 def __str__(self): 44 if self.value is None: 45 # Use the current value of the variable. 46 return f'envar: "{self.name}"\n' 47 return f'envar: "{self.name}={self.value}"\n' 48 49 50class MountPt(object): 51 def __init__(self, 52 _kw_only=(), 53 src="", 54 prefix_src_env="", 55 src_content="", 56 dst="", 57 prefix_dst_env="", 58 fstype="", 59 options="", 60 is_bind=None, 61 rw=None, 62 is_dir=None, 63 mandatory=None, 64 is_symlink=None, 65 nosuid=None, 66 nodev=None, 67 noexec=None): 68 assert _kw_only == (), "MountPt only accepts kwargs" 69 # These asserts may need to be revisited if we ever use prefix_src_env 70 # or prefix_dst_env. 71 assert not src or os.path.abspath(src) == src, "Paths must be absolute" 72 assert not dst or os.path.abspath(dst) == dst, "Paths must be absolute" 73 self.src = src 74 self.prefix_src_env = prefix_src_env 75 self.src_content = src_content 76 self.dst = dst 77 self.prefix_dst_env = prefix_dst_env 78 self.fstype = fstype 79 self.options = options 80 self.is_bind = _bool(is_bind) 81 self.rw = _bool(rw) 82 self.is_dir = _bool(is_dir) 83 self.mandatory = _bool(mandatory) 84 self.is_symlink = _bool(is_symlink) 85 self.nosuid = _bool(nosuid) 86 self.nodev = _bool(nodev) 87 self.noexec = _bool(noexec) 88 89 def __str__(self): 90 ret = "mount {\n" 91 if self.src: 92 ret += f" src: {json.dumps(self.src)}\n" 93 if self.prefix_src_env: 94 ret += f" prefix_src_env: {json.dumps(self.prefix_src_env)}\n" 95 if self.src_content: 96 ret += f" src_content: {json.dumps(self.src_content)}\n" 97 if self.dst: 98 ret += f" dst: {json.dumps(self.dst)}\n" 99 if self.prefix_dst_env: 100 ret += f" prefix_dst_env: {json.dumps(self.prefix_dst_env)}\n" 101 if self.fstype: 102 ret += f" fstype: {json.dumps(self.fstype)}\n" 103 if self.options: 104 ret += f" options: {json.dumps(self.options)}\n" 105 if self.is_bind is not None: 106 ret += f" is_bind: {json.dumps(self.is_bind)}\n" 107 if self.rw is not None: 108 ret += f" rw: {json.dumps(self.rw)}\n" 109 if self.is_dir is not None: 110 ret += f" is_dir: {json.dumps(self.is_dir)}\n" 111 if self.mandatory is not None: 112 ret += f" mandatory: {json.dumps(self.mandatory)}\n" 113 if self.is_symlink is not None: 114 ret += f" is_symlink: {json.dumps(self.is_symlink)}\n" 115 if self.nosuid is not None: 116 ret += f" nosuid: {json.dumps(self.nosuid)}\n" 117 if self.nodev is not None: 118 ret += f" nodev: {json.dumps(self.nodev)}\n" 119 if self.noexec is not None: 120 ret += f" noexec: {json.dumps(self.noexec)}\n" 121 ret += "}\n\n" 122 return ret 123 124 def __eq__(self, other): 125 return (isinstance(other, MountPt) and self.src == other.src 126 and self.prefix_src_env == other.prefix_src_env 127 and self.src_content == other.src_content 128 and self.dst == other.dst 129 and self.prefix_dst_env == other.prefix_dst_env 130 and self.fstype == other.fstype 131 and self.options == other.options 132 and self.is_bind == other.is_bind and self.rw == other.rw 133 and self.is_dir == other.is_dir 134 and self.mandatory == other.mandatory 135 and self.is_symlink == other.is_symlink 136 and self.nosuid == other.nosuid and self.nodev == other.nodev 137 and self.noexec == other.noexec) 138 139 def copy(self): 140 return MountPt(src=self.src, 141 prefix_src_env=self.prefix_src_env, 142 src_content=self.src_content, 143 dst=self.dst, 144 prefix_dst_env=self.prefix_dst_env, 145 fstype=self.fstype, 146 options=self.options, 147 is_bind=self.is_bind, 148 rw=self.rw, 149 is_dir=self.is_dir, 150 mandatory=self.mandatory, 151 is_symlink=self.is_symlink, 152 nosuid=self.nosuid, 153 nodev=self.nodev, 154 noexec=self.noexec) 155 156 157class NsjailConfigOption(object): 158 """Options for nsjail configuration.""" 159 160 def __init__(self, name, value, comment=""): 161 self.name = name 162 self.value = value 163 self.comment = comment 164 165 def __str__(self): 166 return f"{self.comment}\n{self.name}: {self.value}" 167 168 169class Nsjail(object): 170 def __init__(self, cwd, verbose=False): 171 self.cwd = cwd 172 self.verbose = verbose 173 # Add the mount points that we always need. 174 self.mounts = [ 175 # Mount proc so that the PID namespace can be shared between the 176 # parent and child process. 177 MountPt(src="/proc", 178 dst="/proc", 179 is_bind=True, 180 rw=True, 181 mandatory=True), 182 # Some commands may need /etc/alternatives to reach the correct 183 # binary. 184 MountPt(src="/etc/alternatives", 185 dst="/etc/alternatives", 186 is_bind=True, 187 rw=False, 188 mandatory=False), 189 190 # TODO: we may need to use something other than tmpfs for this, 191 # because of some tests, etc. 192 MountPt(dst="/tmp", 193 fstype="tmpfs", 194 rw=True, 195 is_bind=False, 196 noexec=False, 197 nodev=True, 198 nosuid=True), 199 200 # Some tools need /dev/shm to created a named semaphore. Use a new 201 # tmpfs to limit access to the external environment. 202 MountPt(dst="/dev/shm", fstype="tmpfs", rw=True, is_bind=False), 203 204 # Add the expected tty devices. 205 MountPt(src="/dev/tty", dst="/dev/tty", rw=True, is_bind=True), 206 # These are symlinks to /proc/self/fd/{0,1,2}. 207 MountPt(src="/proc/self/fd/0", dst="/dev/stdin", is_symlink=True), 208 MountPt(src="/proc/self/fd/1", dst="/dev/stdout", is_symlink=True), 209 MountPt(src="/proc/self/fd/2", dst="/dev/stderr", is_symlink=True), 210 211 # Map the working User ID to a username 212 # Some tools like Java need a valid username 213 # Inner trees building with Soong also expect the nobody UID to be 214 # available to setup its own nsjail. 215 MountPt( 216 src_content="user:x:999999:65533:user:/tmp:/bin/bash\n" 217 "nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin\n", 218 dst="/etc/passwd", 219 mandatory=False), 220 221 # Define default group 222 MountPt(src_content="group::65533:user\n" 223 "nogroup::65534:nobody\n", 224 dst="/etc/group", 225 mandatory=False), 226 227 # Empty mtab file needed for some build scripts that check for 228 # images being mounted 229 MountPt(src_content="\n", dst="/etc/mtab", mandatory=False), 230 231 # Explicitly mount required device file nodes 232 # 233 # This will enable a chroot based NsJail sandbox. A chroot does not 234 # provide device file nodes. So just mount the required device file 235 # nodes directly from the host. 236 # 237 # Note that this has no effect in a docker container, since in that 238 # case NsJail will just mount the container device nodes. When we 239 # use NsJail in a docker container we mount the full file system 240 # root. So the container device nodes were already mounted in the 241 # NsJail. 242 243 # Some tools (like llvm-link) look for file descriptors in /dev/fd 244 MountPt(src="/proc/self/fd", 245 dst="/dev/fd", 246 is_symlink=True, 247 mandatory=False), 248 249 # /dev/null is a very commonly used for silencing output 250 MountPt(src="/dev/null", dst="/dev/null", rw=True, is_bind=True), 251 252 # /dev/urandom used during the creation of system.img 253 MountPt(src="/dev/urandom", 254 dst="/dev/urandom", 255 rw=False, 256 is_bind=True), 257 258 # /dev/random used by test scripts 259 MountPt(src="/dev/random", 260 dst="/dev/random", 261 rw=False, 262 is_bind=True), 263 264 # /dev/zero is required to make vendor-qemu.img 265 MountPt(src="/dev/zero", dst="/dev/zero", is_bind=True), 266 MountPt(src="/lib", dst="/lib", is_bind=True, rw=False), 267 MountPt(src="/bin", dst="/bin", is_bind=True, rw=False), 268 MountPt(src="/sbin", dst="/sbin", is_bind=True, rw=False), 269 MountPt(src="/usr", dst="/usr", is_bind=True, rw=False), 270 MountPt(src="/lib64", 271 dst="/lib64", 272 is_bind=True, 273 rw=False, 274 mandatory=False), 275 MountPt(src="/lib32", 276 dst="/lib32", 277 is_bind=True, 278 rw=False, 279 mandatory=False), 280 ] 281 282 self.envars = [ 283 # Some tools in the build toolchain expect a $HOME to be set Point 284 # $HOME to /tmp in case the toolchain needs to write something out 285 # there 286 Envar(name="HOME", value="/tmp"), 287 # By default nsjail does not propagate the environment into the 288 # jail. We need the path to be set up. There are a few ways to solve 289 # this problem, but to avoid an undocumented dependency we are 290 # explicit about the path we inject. 291 Envar(name="PATH", value="/usr/bin:/usr/sbin:/bin:/sbin"), 292 ] 293 self.options = [] 294 295 def make_cwd_writable(self): 296 """Mark things under cwd writable.""" 297 for mount in self.mounts: 298 if mount.dst.startswith(self.cwd) and not mount.rw: 299 if self.verbose: 300 print(f"Marking {mount.dst} r/w") 301 mount.rw = True 302 303 def add_mountpt(self, **kwargs): 304 """Add a mountpoint to the config.""" 305 self.mounts.append(MountPt(**kwargs)) 306 307 def add_envar(self, **kwargs): 308 """Add an envar to the config.""" 309 self.envars.append(Envar(**kwargs)) 310 311 def add_option(self, **kwargs): 312 """Add an option to the njsail config.""" 313 self.options.append(NsjailConfigOption(**kwargs)) 314 315 def copy(self): 316 """Return a copy of ourselves.""" 317 ret = Nsjail(self.cwd, verbose=self.verbose) 318 ret.mounts = [x.copy() for x in self.mounts()] 319 return ret 320 321 @property 322 def mount_points(self): 323 """Return the list of mount points. 324 325 Returns a list of mount points (destinations) for the nsjail. 326 """ 327 return (x.dst for x in self.mounts) 328 329 def add_nsjail(self, other): 330 """Add another Nsjail object to this one.""" 331 # WARNING: clone_newnet option of inner tree should not be merged into 332 # the combined nsjsail.cfg. 333 assert other.cwd.startswith(self.cwd), "Must be a subdir" 334 our_mounts = {x.dst: x for x in self.mounts} 335 for mount in other.mounts: 336 if mount.dst not in our_mounts: 337 self.mounts.append(mount) 338 else: 339 assert mount == our_mounts[mount.dst] 340 341 def generate_config(self, fn): 342 """Generate the nsjail config file. 343 344 Args: 345 fn: (str) The name of the file to write, or None to not create. 346 347 Returns: 348 (str) The configuration written. 349 """ 350 data = textwrap.dedent(f"""\ 351 name: "android-build-sandbox" 352 description: "Sandboxed Android Platform Build." 353 description: "No network access and a limited access to local host resources." 354 355 log_level: {"INFO" if self.verbose else "WARNING"} 356 # All configuration options are described in 357 # https://github.com/google/nsjail/blob/master/config.proto 358 359 # Run once then exit 360 mode: ONCE 361 362 # No time limit 363 time_limit: 0 364 365 # Limits memory usage 366 rlimit_as_type: SOFT 367 # Maximum size of core dump files 368 rlimit_core_type: SOFT 369 # Limits use of CPU time 370 rlimit_cpu_type: SOFT 371 # Maximum file size 372 rlimit_fsize_type: SOFT 373 # Maximum number of file descriptors opened 374 rlimit_nofile_type: SOFT 375 # Maximum stack size 376 rlimit_stack_type: SOFT 377 # Maximum number of threads 378 rlimit_nproc_type: SOFT 379 380 # Allow terminal control 381 # This let's users cancel jobs with CTRL-C without exiting the 382 # jail. 383 skip_setsid: true 384 385 # Below are all the host paths that shall be mounted 386 # to the sandbox 387 388 # TODO: Determine if we need to have /proc from outside of the jail. 389 mount_proc: false 390 391 # The user must mount the source to /src using --bindmount 392 # It will be set as the initial working directory 393 cwd: "{self.cwd}" 394 395 # The sandbox User ID was chosen arbitrarily 396 uidmap {{ 397 inside_id: "999999" 398 outside_id: "" 399 count: 1 400 }} 401 402 # The sandbox Group ID was chosen arbitrarily 403 gidmap {{ 404 inside_id: "65533" 405 outside_id: "" 406 count: 1 407 }} 408 409 # Share PID namespace between parent and child process. 410 # Sharing the PID namespace ensures that the Bazel daemon does not 411 # get killed after every invocation. 412 clone_newpid: false 413 414 """) 415 for envar in self.envars: 416 data += f'{envar}' 417 data += '\n' 418 for mount in self.mounts: 419 data += f'{mount}' 420 data += '\n' 421 for option in self.options: 422 data += f"{option}" 423 424 if fn: 425 os.makedirs(os.path.dirname(fn), exist_ok=True) 426 with open(fn, "w", encoding="iso-8859-1") as f: 427 f.write(data) 428 return data 429