browser_content_sandbox_utils.js (13578B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 "use strict"; 4 5 const uuidGenerator = Services.uuid; 6 7 /* 8 * Utility functions for the browser content sandbox tests. 9 */ 10 11 function sanityChecks() { 12 // This test is only relevant in e10s 13 if (!gMultiProcessBrowser) { 14 ok(false, "e10s is enabled"); 15 info("e10s is not enabled, exiting"); 16 return; 17 } 18 19 let level = 0; 20 let prefExists = true; 21 22 // Read the security.sandbox.content.level pref. 23 // eslint-disable-next-line mozilla/use-default-preference-values 24 try { 25 level = Services.prefs.getIntPref("security.sandbox.content.level"); 26 } catch (e) { 27 prefExists = false; 28 } 29 30 ok(prefExists, "pref security.sandbox.content.level exists"); 31 if (!prefExists) { 32 return; 33 } 34 35 info(`security.sandbox.content.level=${level}`); 36 Assert.greater(level, 0, "content sandbox is enabled."); 37 38 let isFileIOSandboxed = isContentFileIOSandboxed(level); 39 40 // Content sandbox enabled, but level doesn't include file I/O sandboxing. 41 ok(isFileIOSandboxed, "content file I/O sandboxing is enabled."); 42 if (!isFileIOSandboxed) { 43 info("content sandbox level too low for file I/O tests, exiting\n"); 44 } 45 } 46 47 function isXdgEnabled() { 48 try { 49 return Services.prefs.getBoolPref("widget.support-xdg-config"); 50 } catch (ex) { 51 // if the pref is not there it means we dont have XDG support 52 if (ex.name === "NS_ERROR_UNEXPECTED") { 53 return false; 54 } 55 throw ex; 56 } 57 } 58 59 // Creates file at |path| and returns a promise that resolves with an object 60 // with .ok boolean to indicate true if the file was successfully created, 61 // otherwise false. Include imports so this can be safely serialized and run 62 // remotely by ContentTask.spawn. 63 // 64 // Report the exception's error code in .code as well. 65 function createFile(path) { 66 const { FileUtils } = ChromeUtils.importESModule( 67 "resource://gre/modules/FileUtils.sys.mjs" 68 ); 69 70 try { 71 const fstream = Cc[ 72 "@mozilla.org/network/file-output-stream;1" 73 ].createInstance(Ci.nsIFileOutputStream); 74 75 fstream.init( 76 new FileUtils.File(path), 77 -1, // readonly mode 78 -1, // default permissions 79 0 80 ); // behaviour flags 81 82 const ostream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( 83 Ci.nsIBinaryOutputStream 84 ); 85 ostream.setOutputStream(fstream); 86 87 const data = "TEST FILE DUMMY DATA"; 88 ostream.writeBytes(data, data.length); 89 90 ostream.close(); 91 fstream.close(); 92 } catch (e) { 93 return { ok: false, code: e.result }; 94 } 95 96 return { ok: true }; 97 } 98 99 // Creates a symlink at |path| and returns a promise that resolves with an 100 // object with .ok boolean to indicate true if the symlink was successfully 101 // created, otherwise false. Include imports so this can be safely serialized 102 // and run remotely by ContentTask.spawn. 103 // 104 // Report the exception's error code in .code as well. 105 // Report errno in .code if syscall returns -1. 106 function createSymlink(path) { 107 const { ctypes } = ChromeUtils.importESModule( 108 "resource://gre/modules/ctypes.sys.mjs" 109 ); 110 111 try { 112 // Trying to open "libc.so" on linux will fail with invalid elf header error 113 // because it would be a linker script. Using libc.so.6 avoids that. 114 const libc = ctypes.open( 115 Services.appinfo.OS === "Darwin" ? "libSystem.B.dylib" : "libc.so.6" 116 ); 117 118 const symlink = libc.declare( 119 "symlink", 120 ctypes.default_abi, 121 ctypes.int, // return value 122 ctypes.char.ptr, // target 123 ctypes.char.ptr //linkpath 124 ); 125 126 ctypes.errno = 0; 127 const rv = symlink("/etc", path); 128 const _errno = ctypes.errno; 129 if (rv < 0) { 130 return { ok: false, code: _errno }; 131 } 132 } catch (e) { 133 return { ok: false, code: e.result }; 134 } 135 136 return { ok: true }; 137 } 138 139 // Deletes file at |path| and returns a promise that resolves with an object 140 // with .ok boolean to indicate true if the file was successfully deleted, 141 // otherwise false. Include imports so this can be safely serialized and run 142 // remotely by ContentTask.spawn. 143 // 144 // Report the exception's error code in .code as well. 145 function deleteFile(path) { 146 const { FileUtils } = ChromeUtils.importESModule( 147 "resource://gre/modules/FileUtils.sys.mjs" 148 ); 149 150 try { 151 const file = new FileUtils.File(path); 152 file.remove(false); 153 } catch (e) { 154 return { ok: false, code: e.result }; 155 } 156 157 return { ok: true }; 158 } 159 160 // Reads the directory at |path| and returns a promise that resolves when 161 // iteration over the directory finishes or encounters an error. The promise 162 // resolves with an object where .ok indicates success or failure and 163 // .numEntries is the number of directory entries found. 164 // 165 // Report the exception's error code in .code as well. 166 function readDir(path) { 167 const { FileUtils } = ChromeUtils.importESModule( 168 "resource://gre/modules/FileUtils.sys.mjs" 169 ); 170 171 let numEntries = 0; 172 173 try { 174 const file = new FileUtils.File(path); 175 const enumerator = file.directoryEntries; 176 177 while (enumerator.hasMoreElements()) { 178 void enumerator.nextFile; 179 numEntries++; 180 } 181 } catch (e) { 182 return { ok: false, numEntries, code: e.result }; 183 } 184 185 return { ok: true, numEntries }; 186 } 187 188 // Reads the file at |path| and returns a promise that resolves when 189 // reading is completed. Returned object has boolean .ok to indicate 190 // success or failure. 191 // 192 // Report the exception's error code in .code as well. 193 function readFile(path) { 194 const { FileUtils } = ChromeUtils.importESModule( 195 "resource://gre/modules/FileUtils.sys.mjs" 196 ); 197 198 try { 199 const file = new FileUtils.File(path); 200 201 const fstream = Cc[ 202 "@mozilla.org/network/file-input-stream;1" 203 ].createInstance(Ci.nsIFileInputStream); 204 fstream.init(file, -1, -1, 0); 205 206 const istream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( 207 Ci.nsIBinaryInputStream 208 ); 209 istream.setInputStream(fstream); 210 211 const available = istream.available(); 212 void istream.readBytes(available); 213 } catch (e) { 214 return { ok: false, code: e.result }; 215 } 216 217 return { ok: true }; 218 } 219 220 // Does a stat of |path| and returns a promise that resolves if the 221 // stat is successful. Returned object has boolean .ok to indicate 222 // success or failure. 223 // 224 // Report the exception's error code in .code as well. 225 function statPath(path) { 226 const { FileUtils } = ChromeUtils.importESModule( 227 "resource://gre/modules/FileUtils.sys.mjs" 228 ); 229 230 try { 231 const file = new FileUtils.File(path); 232 void file.lastModifiedTime; 233 } catch (e) { 234 return { ok: false, code: e.result }; 235 } 236 237 return { ok: true }; 238 } 239 240 // Returns true if the current content sandbox level, passed in 241 // the |level| argument, supports filesystem sandboxing. 242 function isContentFileIOSandboxed(level) { 243 let fileIOSandboxMinLevel = 0; 244 245 // Set fileIOSandboxMinLevel to the lowest level that has 246 // content filesystem sandboxing enabled. For now, this 247 // varies across Windows, Mac, Linux, other. 248 switch (Services.appinfo.OS) { 249 case "WINNT": 250 fileIOSandboxMinLevel = 1; 251 break; 252 case "Darwin": 253 fileIOSandboxMinLevel = 1; 254 break; 255 case "Linux": 256 fileIOSandboxMinLevel = 2; 257 break; 258 default: 259 Assert.ok(false, "Unknown OS"); 260 } 261 262 return level >= fileIOSandboxMinLevel; 263 } 264 265 // Returns the lowest sandbox level where blanket reading of the profile 266 // directory from the content process should be blocked by the sandbox. 267 function minProfileReadSandboxLevel() { 268 switch (Services.appinfo.OS) { 269 case "WINNT": 270 return 3; 271 case "Darwin": 272 return 2; 273 case "Linux": 274 return 3; 275 default: 276 Assert.ok(false, "Unknown OS"); 277 return 0; 278 } 279 } 280 281 // Returns the lowest sandbox level where blanket reading of the home 282 // directory from the content process should be blocked by the sandbox. 283 function minHomeReadSandboxLevel() { 284 switch (Services.appinfo.OS) { 285 case "WINNT": 286 return 3; 287 case "Darwin": 288 return 3; 289 case "Linux": 290 return 3; 291 default: 292 Assert.ok(false, "Unknown OS"); 293 return 0; 294 } 295 } 296 297 function isMac() { 298 return Services.appinfo.OS == "Darwin"; 299 } 300 function isWin() { 301 return Services.appinfo.OS == "WINNT"; 302 } 303 function isLinux() { 304 return Services.appinfo.OS == "Linux"; 305 } 306 307 function isNightly() { 308 let version = SpecialPowers.Services.appinfo.version; 309 return version.endsWith("a1"); 310 } 311 312 function uuid() { 313 return uuidGenerator.generateUUID().toString(); 314 } 315 316 // Returns a file object for a new file in the home dir ($HOME/<UUID>). 317 function fileInHomeDir() { 318 // get home directory, make sure it exists 319 let homeDir = Services.dirsvc.get("Home", Ci.nsIFile); 320 Assert.ok(homeDir.exists(), "Home dir exists"); 321 Assert.ok(homeDir.isDirectory(), "Home dir is a directory"); 322 323 // build a file object for a new file named $HOME/<UUID> 324 let homeFile = homeDir.clone(); 325 homeFile.appendRelativePath(uuid()); 326 Assert.ok(!homeFile.exists(), homeFile.path + " does not exist"); 327 return homeFile; 328 } 329 330 // Returns a file object for a new file in the content temp dir (.../<UUID>). 331 function fileInTempDir() { 332 let contentTempKey = "TmpD"; 333 334 // get the content temp dir, make sure it exists 335 let ctmp = Services.dirsvc.get(contentTempKey, Ci.nsIFile); 336 Assert.ok(ctmp.exists(), "Temp dir exists"); 337 Assert.ok(ctmp.isDirectory(), "Temp dir is a directory"); 338 339 // build a file object for a new file in content temp 340 let tempFile = ctmp.clone(); 341 tempFile.appendRelativePath(uuid()); 342 Assert.ok(!tempFile.exists(), tempFile.path + " does not exist"); 343 return tempFile; 344 } 345 346 function GetProfileDir() { 347 // get profile directory 348 let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); 349 return profileDir; 350 } 351 352 function GetHomeDir() { 353 // get home directory 354 let homeDir = Services.dirsvc.get("Home", Ci.nsIFile); 355 return homeDir; 356 } 357 358 function GetHomeSubdir(subdir) { 359 return GetSubdir(GetHomeDir(), subdir); 360 } 361 362 function GetHomeSubdirFile(subdir) { 363 return GetSubdirFile(GetHomeSubdir(subdir)); 364 } 365 366 function GetSubdir(dir, subdir) { 367 let newSubdir = dir.clone(); 368 newSubdir.appendRelativePath(subdir); 369 return newSubdir; 370 } 371 372 function GetSubdirFile(dir) { 373 let newFile = dir.clone(); 374 newFile.appendRelativePath(uuid()); 375 return newFile; 376 } 377 378 function GetPerUserExtensionDir() { 379 return Services.dirsvc.get("XREUSysExt", Ci.nsIFile); 380 } 381 382 // Returns a file object for the file or directory named |name| in the 383 // profile directory. 384 function GetProfileEntry(name) { 385 let entry = GetProfileDir(); 386 entry.append(name); 387 return entry; 388 } 389 390 function GetDir(path) { 391 info(`GetDir(${path})`); 392 let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); 393 dir.initWithPath(path); 394 Assert.ok(dir.isDirectory(), `${path} is a directory`); 395 return dir; 396 } 397 398 function GetDirFromEnvVariable(varName) { 399 return GetDir(Services.env.get(varName)); 400 } 401 402 function GetFile(path) { 403 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); 404 file.initWithPath(path); 405 return file; 406 } 407 408 function GetBrowserType(type) { 409 let browserType = undefined; 410 411 if (!GetBrowserType[type]) { 412 if (type === "web") { 413 GetBrowserType[type] = gBrowser.selectedBrowser; 414 } else { 415 // open a tab in a `type` content process 416 gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank", { 417 preferredRemoteType: type, 418 allowInheritPrincipal: true, 419 }); 420 // get the browser for the `type` process tab 421 GetBrowserType[type] = gBrowser.getBrowserForTab(gBrowser.selectedTab); 422 } 423 } 424 425 browserType = GetBrowserType[type]; 426 Assert.strictEqual( 427 browserType.remoteType, 428 type, 429 `GetBrowserType(${type}) returns a ${type} process` 430 ); 431 return browserType; 432 } 433 434 function GetWebBrowser() { 435 return GetBrowserType("web"); 436 } 437 438 function isFileContentProcessEnabled() { 439 // Ensure that the file content process is enabled. 440 let fileContentProcessEnabled = Services.prefs.getBoolPref( 441 "browser.tabs.remote.separateFileUriProcess" 442 ); 443 ok(fileContentProcessEnabled, "separate file content process is enabled"); 444 return fileContentProcessEnabled; 445 } 446 447 function GetFileBrowser() { 448 if (!isFileContentProcessEnabled()) { 449 return undefined; 450 } 451 return GetBrowserType("file"); 452 } 453 454 function GetSandboxLevel() { 455 // Current level 456 return Services.prefs.getIntPref("security.sandbox.content.level"); 457 } 458 459 async function runTestsList(tests) { 460 let level = GetSandboxLevel(); 461 462 // remove tests not enabled by the current sandbox level 463 tests = tests.filter(test => test.minLevel <= level); 464 465 for (let test of tests) { 466 let okString = test.ok ? "allowed" : "blocked"; 467 let processType = test.browser.remoteType; 468 469 // ensure the file/dir exists before we ask a content process to stat 470 // it so we know a failure is not due to a nonexistent file/dir 471 if (test.func === statPath) { 472 ok(test.file.exists(), `${test.file.path} exists`); 473 } 474 475 let result = await ContentTask.spawn( 476 test.browser, 477 test.file.path, 478 test.func 479 ); 480 481 Assert.equal( 482 result.ok, 483 test.ok, 484 `reading ${test.desc} from a ${processType} process ` + 485 `is ${okString} (${test.file.path})` 486 ); 487 488 // if the directory is not expected to be readable, 489 // ensure the listing has zero entries 490 if (test.func === readDir && !test.ok) { 491 Assert.equal( 492 result.numEntries, 493 0, 494 `directory list is empty (${test.file.path})` 495 ); 496 } 497 498 if (test.cleanup != undefined) { 499 await test.cleanup(test.file.path); 500 } 501 } 502 }