browser_content_sandbox_syscalls.js (11210B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 /* import-globals-from browser_content_sandbox_utils.js */ 4 "use strict"; 5 6 Services.scriptloader.loadSubScript( 7 "chrome://mochitests/content/browser/" + 8 "security/sandbox/test/browser_content_sandbox_utils.js", 9 this 10 ); 11 12 const lazy = {}; 13 14 /* getLibcConstants is only present on *nix */ 15 ChromeUtils.defineLazyGetter(lazy, "LIBC", () => 16 ChromeUtils.getLibcConstants() 17 ); 18 19 /* 20 * This test is for executing system calls in content processes to validate 21 * that calls that are meant to be blocked by content sandboxing are blocked. 22 * We use the term system calls loosely so that any OS API call such as 23 * fopen could be included. 24 */ 25 26 // Calls the native execv library function. Include imports so this can be 27 // safely serialized and run remotely by ContentTask.spawn. 28 function callExec(args) { 29 const { ctypes } = ChromeUtils.importESModule( 30 "resource://gre/modules/ctypes.sys.mjs" 31 ); 32 let { lib, cmd } = args; 33 let libc = ctypes.open(lib); 34 let exec = libc.declare( 35 "execv", 36 ctypes.default_abi, 37 ctypes.int, 38 ctypes.char.ptr 39 ); 40 let rv = exec(cmd); 41 libc.close(); 42 return rv; 43 } 44 45 // Calls the native fork syscall. 46 function callFork(args) { 47 const { ctypes } = ChromeUtils.importESModule( 48 "resource://gre/modules/ctypes.sys.mjs" 49 ); 50 let { lib } = args; 51 let libc = ctypes.open(lib); 52 let fork = libc.declare("fork", ctypes.default_abi, ctypes.int); 53 let rv = fork(); 54 libc.close(); 55 return rv; 56 } 57 58 // Calls the native sysctl syscall. 59 function callSysctl(args) { 60 const { ctypes } = ChromeUtils.importESModule( 61 "resource://gre/modules/ctypes.sys.mjs" 62 ); 63 let { lib, name } = args; 64 let libc = ctypes.open(lib); 65 let sysctlbyname = libc.declare( 66 "sysctlbyname", 67 ctypes.default_abi, 68 ctypes.int, 69 ctypes.char.ptr, 70 ctypes.voidptr_t, 71 ctypes.size_t.ptr, 72 ctypes.voidptr_t, 73 ctypes.size_t.ptr 74 ); 75 let rv = sysctlbyname(name, null, null, null, null); 76 libc.close(); 77 return rv; 78 } 79 80 function callPrctl(args) { 81 const { ctypes } = ChromeUtils.importESModule( 82 "resource://gre/modules/ctypes.sys.mjs" 83 ); 84 let { lib, option } = args; 85 let libc = ctypes.open(lib); 86 let prctl = libc.declare( 87 "prctl", 88 ctypes.default_abi, 89 ctypes.int, 90 ctypes.int, // option 91 ctypes.unsigned_long, // arg2 92 ctypes.unsigned_long, // arg3 93 ctypes.unsigned_long, // arg4 94 ctypes.unsigned_long // arg5 95 ); 96 let rv = prctl(option, 0, 0, 0, 0); 97 if (rv == -1) { 98 rv = ctypes.errno; 99 } 100 libc.close(); 101 return rv; 102 } 103 104 // Calls the native open/close syscalls. 105 function callOpen(args) { 106 const { ctypes } = ChromeUtils.importESModule( 107 "resource://gre/modules/ctypes.sys.mjs" 108 ); 109 let { lib, path, flags } = args; 110 let libc = ctypes.open(lib); 111 let open = libc.declare( 112 "open", 113 ctypes.default_abi, 114 ctypes.int, 115 ctypes.char.ptr, 116 ctypes.int 117 ); 118 let close = libc.declare("close", ctypes.default_abi, ctypes.int, ctypes.int); 119 let fd = open(path, flags); 120 close(fd); 121 libc.close(); 122 return fd; 123 } 124 125 // Verify faccessat2 126 function callFaccessat2(args) { 127 const { ctypes } = ChromeUtils.importESModule( 128 "resource://gre/modules/ctypes.sys.mjs" 129 ); 130 let { lib, dirfd, path, mode, flag } = args; 131 let libc = ctypes.open(lib); 132 let faccessat = libc.declare( 133 "faccessat", 134 ctypes.default_abi, 135 ctypes.int, 136 ctypes.int, // dirfd 137 ctypes.char.ptr, // path 138 ctypes.int, // mode 139 ctypes.int // flag 140 ); 141 let rv = faccessat(dirfd, path, mode, flag); 142 if (rv == -1) { 143 rv = ctypes.errno; 144 } 145 libc.close(); 146 return rv; 147 } 148 149 // Returns the name of the native library needed for native syscalls 150 function getOSLib() { 151 switch (Services.appinfo.OS) { 152 case "WINNT": 153 return "kernel32.dll"; 154 case "Darwin": 155 return "libc.dylib"; 156 case "Linux": 157 return "libc.so.6"; 158 default: 159 Assert.ok(false, "Unknown OS"); 160 return 0; 161 } 162 } 163 164 // Reading a header might be weird, but the alternatives to read a stable 165 // version number we can easily check against are not much more fun 166 async function getKernelVersion() { 167 let header = await IOUtils.readUTF8("/usr/include/linux/version.h"); 168 let hr = header.split("\n"); 169 for (let line in hr) { 170 let hrs = hr[line].split(" "); 171 if (hrs[0] === "#define" && hrs[1] === "LINUX_VERSION_CODE") { 172 return Number(hrs[2]); 173 } 174 } 175 throw Error("No LINUX_VERSION_CODE"); 176 } 177 178 // This is how it is done in /usr/include/linux/version.h 179 function computeKernelVersion(major, minor, dot) { 180 return (major << 16) + (minor << 8) + dot; 181 } 182 183 function getGlibcVersion() { 184 const { ctypes } = ChromeUtils.importESModule( 185 "resource://gre/modules/ctypes.sys.mjs" 186 ); 187 let libc = ctypes.open(getOSLib()); 188 let gnu_get_libc_version = libc.declare( 189 "gnu_get_libc_version", 190 ctypes.default_abi, 191 ctypes.char.ptr 192 ); 193 let rv = gnu_get_libc_version().readString(); 194 libc.close(); 195 let ar = rv.split("."); 196 // return a number made of MAJORMINOR 197 return Number(ar[0] + ar[1]); 198 } 199 200 // Returns a harmless command to execute with execv 201 function getOSExecCmd() { 202 Assert.ok(!isWin()); 203 return "/bin/cat"; 204 } 205 206 // Returns true if the current content sandbox level, passed in 207 // the |level| argument, supports syscall sandboxing. 208 function areContentSyscallsSandboxed(level) { 209 let syscallsSandboxMinLevel = 0; 210 211 // Set syscallsSandboxMinLevel to the lowest level that has 212 // syscall sandboxing enabled. For now, this varies across 213 // Windows, Mac, Linux, other. 214 switch (Services.appinfo.OS) { 215 case "WINNT": 216 syscallsSandboxMinLevel = 1; 217 break; 218 case "Darwin": 219 syscallsSandboxMinLevel = 1; 220 break; 221 case "Linux": 222 syscallsSandboxMinLevel = 1; 223 break; 224 default: 225 Assert.ok(false, "Unknown OS"); 226 } 227 228 return level >= syscallsSandboxMinLevel; 229 } 230 231 // 232 // Drive tests for a single content process. 233 // 234 // Tests executing OS API calls in the content process. Limited to Mac 235 // and Linux calls for now. 236 // 237 add_task(async function () { 238 // This test is only relevant in e10s 239 if (!gMultiProcessBrowser) { 240 ok(false, "e10s is enabled"); 241 info("e10s is not enabled, exiting"); 242 return; 243 } 244 245 let level = 0; 246 let prefExists = true; 247 248 // Read the security.sandbox.content.level pref. 249 // If the pref isn't set and we're running on Linux on !isNightly(), 250 // exit without failing. The Linux content sandbox is only enabled 251 // on Nightly at this time. 252 // eslint-disable-next-line mozilla/use-default-preference-values 253 try { 254 level = Services.prefs.getIntPref("security.sandbox.content.level"); 255 } catch (e) { 256 prefExists = false; 257 } 258 259 ok(prefExists, "pref security.sandbox.content.level exists"); 260 if (!prefExists) { 261 return; 262 } 263 264 info(`security.sandbox.content.level=${level}`); 265 Assert.greater(level, 0, "content sandbox is enabled."); 266 267 let areSyscallsSandboxed = areContentSyscallsSandboxed(level); 268 269 // Content sandbox enabled, but level doesn't include syscall sandboxing. 270 ok(areSyscallsSandboxed, "content syscall sandboxing is enabled."); 271 if (!areSyscallsSandboxed) { 272 info("content sandbox level too low for syscall tests, exiting\n"); 273 return; 274 } 275 276 let browser = gBrowser.selectedBrowser; 277 let lib = getOSLib(); 278 279 // use execv syscall 280 // (causes content process to be killed on Linux) 281 if (isMac()) { 282 // exec something harmless, this should fail 283 let cmd = getOSExecCmd(); 284 let rv = await SpecialPowers.spawn(browser, [{ lib, cmd }], callExec); 285 Assert.equal(rv, -1, `exec(${cmd}) is not permitted`); 286 } 287 288 // use open syscall 289 if (isLinux() || isMac()) { 290 // open a file for writing in $HOME, this should fail 291 let path = fileInHomeDir().path; 292 let flags = lazy.LIBC.O_CREAT | lazy.LIBC.O_WRONLY; 293 let fd = await SpecialPowers.spawn( 294 browser, 295 [{ lib, path, flags }], 296 callOpen 297 ); 298 Assert.less(fd, 0, "opening a file for writing in home is not permitted"); 299 } 300 301 // use open syscall 302 if (isLinux() || isMac()) { 303 // open a file for writing in the content temp dir, this should fail on 304 // macOS and Linux. The open handler in the content process closes the file 305 // for us 306 let path = fileInTempDir().path; 307 let flags = lazy.LIBC.O_CREAT | lazy.LIBC.O_WRONLY; 308 let fd = await SpecialPowers.spawn( 309 browser, 310 [{ lib, path, flags }], 311 callOpen 312 ); 313 Assert.strictEqual( 314 fd, 315 -1, 316 "opening a file for writing in content temp is not permitted" 317 ); 318 } 319 320 // use fork syscall 321 if (isLinux() || isMac()) { 322 let rv = await SpecialPowers.spawn(browser, [{ lib }], callFork); 323 Assert.equal(rv, -1, "calling fork is not permitted"); 324 } 325 326 // On macOS before 10.10 the |sysctl-name| predicate didn't exist for 327 // filtering |sysctl| access. Check the Darwin version before running the 328 // tests (Darwin 14.0.0 is macOS 10.10). This branch can be removed when we 329 // remove support for macOS 10.9. 330 if (isMac() && Services.sysinfo.getProperty("version") >= "14.0.0") { 331 let rv = await SpecialPowers.spawn( 332 browser, 333 [{ lib, name: "kern.boottime" }], 334 callSysctl 335 ); 336 Assert.equal(rv, -1, "calling sysctl('kern.boottime') is not permitted"); 337 338 rv = await SpecialPowers.spawn( 339 browser, 340 [{ lib, name: "net.inet.ip.ttl" }], 341 callSysctl 342 ); 343 Assert.equal(rv, -1, "calling sysctl('net.inet.ip.ttl') is not permitted"); 344 345 rv = await SpecialPowers.spawn( 346 browser, 347 [{ lib, name: "hw.ncpu" }], 348 callSysctl 349 ); 350 Assert.equal(rv, 0, "calling sysctl('hw.ncpu') is permitted"); 351 } 352 353 if (isLinux()) { 354 // These constants are not portable. 355 356 // verify we block PR_CAPBSET_READ with EINVAL 357 let option = lazy.LIBC.PR_CAPBSET_READ; 358 let rv = await SpecialPowers.spawn(browser, [{ lib, option }], callPrctl); 359 Assert.strictEqual( 360 rv, 361 lazy.LIBC.EINVAL, 362 "prctl(PR_CAPBSET_READ) is blocked" 363 ); 364 365 const kernelVersion = await getKernelVersion(); 366 const glibcVersion = getGlibcVersion(); 367 // faccessat2 is only used with kernel 5.8+ by glibc 2.33+ 368 if (glibcVersion >= 233 && kernelVersion >= computeKernelVersion(5, 8, 0)) { 369 info("Linux v5.8+, glibc 2.33+, checking faccessat2"); 370 const dirfd = 0; 371 const path = "/"; 372 const mode = 0; 373 // the value 0x01 is just one we know should get rejected 374 let rv = await SpecialPowers.spawn( 375 browser, 376 [{ lib, dirfd, path, mode, flag: 0x01 }], 377 callFaccessat2 378 ); 379 Assert.strictEqual( 380 rv, 381 lazy.LIBC.ENOSYS, 382 "faccessat2 (flag=0x01) was blocked with ENOSYS" 383 ); 384 385 rv = await SpecialPowers.spawn( 386 browser, 387 [{ lib, dirfd, path, mode, flag: lazy.LIBC.AT_EACCESS }], 388 callFaccessat2 389 ); 390 Assert.strictEqual( 391 rv, 392 lazy.LIBC.EACCES, 393 "faccessat2 (flag=0x200) was allowed, errno=EACCES" 394 ); 395 } else { 396 info( 397 "Unsupported kernel (" + 398 kernelVersion + 399 " )/glibc (" + 400 glibcVersion + 401 "), skipping faccessat2" 402 ); 403 } 404 } 405 });