chromium/chrome/browser/resources/sandbox_internals/sandbox_internals_win.ts

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {assert} from 'chrome://resources/js/assert.js';
import {sendWithPromise} from 'chrome://resources/js/cr.js';
import {getRequiredElement} from 'chrome://resources/js/util.js';

interface SandboxFeature {
  name: string;
  enabled: boolean;
}

interface BrowserHostProcess {
  processId: number;
  processType: string;
  name: string;
  metricsName: string;
  sandboxType: string;
}

interface RendererHostProcess {
  processId: number;
}

/**
 * This may have additional fields displayed in the JSON output.
 * See //sandbox/win/src/sandbox_constants.cc for keys in policy.
 */
interface PolicyDiagnostic {
  appContainerCapabilities: string[];
  appContainerSid: string;
  componentFilters: string;
  desiredIntegrityLevel: string;
  lockdownLevel: string;
  lowboxSid: string;
  platformMitigations: string;
  processId: number;
}

interface SandboxDiagnostics {
  browser: BrowserHostProcess[];
  renderer: RendererHostProcess[];
  policies: PolicyDiagnostic[];
  features: SandboxFeature[];
}

/**
 * Represents a mitigation field from the PROCESS_CREATION_MITITAGION_POLICY*
 * series in Winbase.h.
 */
abstract class MitigationField {
  mitigation: string;
  value: number;
  mask: number;
  offset: number;

  /**
   * mask & value must be 0<=x<=255.
   * @param mitigation human name of mitigation.
   * @param value value to match within mask.
   * @param mask applied before matching.
   * @param offset within PC section.
   */
  constructor(mitigation: string, value: number, mask: number, offset: number) {
    this.mitigation = mitigation;
    this.value = value;
    this.mask = mask;
    this.offset = offset;
  }

  /**
   * Each PC field overrides this as they know where their data is.
   * @param bytes platform mitigations data.
   * @return chunk containing this field or null.
   */
  abstract getFieldData(bytes: Uint8Array): Uint8Array|null;

  /**
   * Are all the bits of this field set in the mitigations represented by
   * |bytes|.
   * @param bytes platform mitigations.
   */
  isFieldSet(bytes: Uint8Array): boolean {
    if (bytes.length !== 4 && bytes.length !== 8 && bytes.length !== 16) {
      throw new Error('Platform mitigations has unexpected size');
    }
    const subfield = this.getFieldData(bytes);
    if (subfield == null || this.offset > subfield.length * 8) {
      return false;
    }
    const idx = subfield.length - 1 - Math.floor(this.offset / 8);
    const ibit = this.offset % 8;
    return (subfield[idx]! & (this.mask << ibit)) === (this.value << ibit);
  }
}

/**
 * PROCESS_CREATION_MITIGATION_POLICY legacy bits.
 */
class Pc0Field extends MitigationField {
  /**
   * @param bytes platform mitigations data.
   * @return chunk containing this field or null.
   */
  getFieldData(bytes: Uint8Array): Uint8Array {
    if (bytes.length === 4) {
      // Win32 only 4 bytes of fields.
      return bytes;
    } else if (bytes.length === 8) {
      return bytes;
    } else {
      return bytes.slice(0, 8);
    }
  }
}

/**
 * PROCESS_CREATION_MITIGATION_POLICY_*
 */
class Pc1Field extends MitigationField {
  getFieldData(bytes: Uint8Array) {
    if (bytes.length === 8) {
      return bytes;
    } else if (bytes.length === 16) {
      return bytes.slice(0, 8);
    }
    return null;
  }
}

/**
 * PROCESS_CREATION_MITIGATION_POLICY2_*
 */
class Pc2Field extends MitigationField {
  getFieldData(bytes: Uint8Array) {
    if (bytes.length === 8) {
      return null;
    } else if (bytes.length === 16) {
      return bytes.slice(8, 16);
    }
    return null;
  }
}

/**
 * Helper to show enabled mitigations from a stringified hex
  representation of PROCESS_CREATION_MITIGATION_POLICY_* entries.
 */
class DecodeMitigations {
  fields: MitigationField[];

  constructor() {
    this.fields = [
      // Defined in Windows.h from Winbase.h
      // basic (pc0) mitigations in {win7},{lsb of pc1}.
      new Pc0Field('DEP_ENABLE', 0x1, 0x01, 0),
      new Pc0Field('DEP_ATL_THUNK_ENABLE', 0x2, 0x02, 0),
      new Pc0Field('SEHOP_ENABLE', 0x4, 0x04, 0),

      // pc1 mitigations in {lsb of pc1}.
      new Pc1Field('FORCE_RELOCATE_IMAGES', 0x1, 0x03, 8),
      new Pc1Field('FORCE_RELOCATE_IMAGES_ALWAYS_OFF', 0x2, 0x03, 8),
      new Pc1Field('FORCE_RELOCATE_IMAGES_ALWAYS_ON_REQ_RELOCS', 0x3, 0x03, 8),
      new Pc1Field('HEAP_TERMINATE', 0x1, 0x03, 12),
      new Pc1Field('HEAP_TERMINATE_ALWAYS_OFF', 0x2, 0x03, 12),
      new Pc1Field('HEAP_TERMINATE_RESERVED', 0x3, 0x03, 12),
      new Pc1Field('BOTTOM_UP_ASLR', 0x1, 0x03, 16),
      new Pc1Field('BOTTOM_UP_ASLR_ALWAYS_OFF', 0x2, 0x03, 16),
      new Pc1Field('BOTTOM_UP_ASLR_RESERVED', 0x3, 0x03, 16),
      new Pc1Field('HIGH_ENTROPY_ASLR', 0x1, 0x03, 20),
      new Pc1Field('HIGH_ENTROPY_ASLR_ALWAYS_OFF', 0x2, 0x03, 20),
      new Pc1Field('HIGH_ENTROPY_ASLR_RESERVED', 0x3, 0x03, 20),
      new Pc1Field('STRICT_HANDLE_CHECKS', 0x1, 0x03, 24),
      new Pc1Field('STRICT_HANDLE_CHECKS_ALWAYS_OFF', 0x2, 0x03, 24),
      new Pc1Field('STRICT_HANDLE_CHECKS_RESERVED', 0x3, 0x03, 24),
      new Pc1Field('WIN32K_SYSTEM_CALL_DISABLE', 0x1, 0x03, 28),
      new Pc1Field('WIN32K_SYSTEM_CALL_DISABLE_ALWAYS_OFF', 0x2, 0x03, 28),
      new Pc1Field('WIN32K_SYSTEM_CALL_DISABLE_RESERVED', 0x3, 0x03, 28),
      new Pc1Field('EXTENSION_POINT_DISABLE', 0x1, 0x03, 32),
      new Pc1Field('EXTENSION_POINT_DISABLE_ALWAYS_OFF', 0x2, 0x03, 32),
      new Pc1Field('EXTENSION_POINT_DISABLE_RESERVED', 0x3, 0x03, 32),
      new Pc1Field('PROHIBIT_DYNAMIC_CODE', 0x1, 0x03, 36),
      new Pc1Field('PROHIBIT_DYNAMIC_CODE_ALWAYS_OFF', 0x2, 0x03, 36),
      new Pc1Field(
          'PROHIBIT_DYNAMIC_CODE_ALWAYS_ON_ALLOW_OPT_OUT', 0x3, 0x03, 36),
      new Pc1Field('CONTROL_FLOW_GUARD', 0x1, 0x03, 40),
      new Pc1Field('CONTROL_FLOW_GUARD_ALWAYS_OFF', 0x2, 0x03, 40),
      new Pc1Field('CONTROL_FLOW_GUARD_EXPORT_SUPPRESSION', 0x3, 0x03, 40),
      new Pc1Field('BLOCK_NON_MICROSOFT_BINARIES', 0x1, 0x03, 44),
      new Pc1Field('BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_OFF', 0x2, 0x03, 44),
      new Pc1Field('BLOCK_NON_MICROSOFT_BINARIES_ALLOW_STORE', 0x3, 0x03, 44),
      new Pc1Field('FONT_DISABLE', 0x1, 0x03, 48),
      new Pc1Field('FONT_DISABLE_ALWAYS_OFF', 0x2, 0x03, 48),
      new Pc1Field('AUDIT_NONSYSTEM_FONTS', 0x3, 0x03, 48),
      new Pc1Field('IMAGE_LOAD_NO_REMOTE', 0x1, 0x03, 52),
      new Pc1Field('IMAGE_LOAD_NO_REMOTE_ALWAYS_OFF', 0x2, 0x03, 52),
      new Pc1Field('IMAGE_LOAD_NO_REMOTE_RESERVED', 0x3, 0x03, 52),
      new Pc1Field('IMAGE_LOAD_NO_LOW_LABEL', 0x1, 0x03, 56),
      new Pc1Field('IMAGE_LOAD_NO_LOW_LABEL_ALWAYS_OFF', 0x2, 0x03, 56),
      new Pc1Field('IMAGE_LOAD_NO_LOW_LABEL_RESERVED', 0x3, 0x03, 56),
      new Pc1Field('IMAGE_LOAD_PREFER_SYSTEM32', 0x1, 0x03, 60),
      new Pc1Field('IMAGE_LOAD_PREFER_SYSTEM32_ALWAYS_OFF', 0x2, 0x03, 60),
      new Pc1Field('IMAGE_LOAD_PREFER_SYSTEM32_RESERVED', 0x3, 0x03, 60),

      // pc2: in second 64bit block only.
      new Pc2Field('LOADER_INTEGRITY_CONTINUITY', 0x1, 0x03, 4),
      new Pc2Field('LOADER_INTEGRITY_CONTINUITY_ALWAYS_OFF', 0x2, 0x03, 4),
      new Pc2Field('LOADER_INTEGRITY_CONTINUITY_AUDIT', 0x3, 0x03, 4),
      new Pc2Field('STRICT_CONTROL_FLOW_GUARD', 0x1, 0x03, 8),
      new Pc2Field('STRICT_CONTROL_FLOW_GUARD_ALWAYS_OFF', 0x2, 0x03, 8),
      new Pc2Field('STRICT_CONTROL_FLOW_GUARD_RESERVED', 0x3, 0x03, 8),
      new Pc2Field('MODULE_TAMPERING_PROTECTION', 0x1, 0x03, 12),
      new Pc2Field('MODULE_TAMPERING_PROTECTION_ALWAYS_OFF', 0x2, 0x03, 12),
      new Pc2Field('MODULE_TAMPERING_PROTECTION_NOINHERIT', 0x3, 0x03, 12),
      new Pc2Field('RESTRICT_INDIRECT_BRANCH_PREDICTION', 0x1, 0x03, 16),
      new Pc2Field(
          'RESTRICT_INDIRECT_BRANCH_PREDICTION_ALWAYS_OFF', 0x2, 0x03, 16),
      new Pc2Field(
          'RESTRICT_INDIRECT_BRANCH_PREDICTION_RESERVED', 0x3, 0x03, 16),
      new Pc2Field('ALLOW_DOWNGRADE_DYNAMIC_CODE_POLICY', 0x1, 0x03, 20),
      new Pc2Field(
          'ALLOW_DOWNGRADE_DYNAMIC_CODE_POLICY_ALWAYS_OFF', 0x2, 0x03, 20),
      new Pc2Field(
          'ALLOW_DOWNGRADE_DYNAMIC_CODE_POLICY_RESERVED', 0x3, 0x03, 20),
      new Pc2Field('SPECULATIVE_STORE_BYPASS_DISABLE', 0x1, 0x03, 24),
      new Pc2Field(
          'SPECULATIVE_STORE_BYPASS_DISABLE_ALWAYS_OFF', 0x2, 0x03, 24),
      new Pc2Field('SPECULATIVE_STORE_BYPASS_DISABLE_RESERVED', 0x3, 0x03, 24),
      new Pc2Field('CET_USER_SHADOW_STACKS', 0x1, 0x03, 28),
      new Pc2Field('CET_USER_SHADOW_STACKS_ALWAYS_OFF', 0x2, 0x03, 28),
      new Pc2Field('CET_USER_SHADOW_STACKS_STRICT_MODE', 0x3, 0x03, 28),
      new Pc2Field('USER_CET_SET_CONTEXT_IP_VALIDATION', 0x1, 0x03, 32),
      new Pc2Field(
          'USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF', 0x2, 0x03, 32),
      new Pc2Field(
          'USER_CET_SET_CONTEXT_IP_VALIDATION_RELAXED_MODE', 0x3, 0x03, 32),
      new Pc2Field('BLOCK_NON_CET_BINARIES', 0x1, 0x03, 36),
      new Pc2Field('BLOCK_NON_CET_BINARIES_ALWAYS_OFF', 0x2, 0x03, 36),
      new Pc2Field('BLOCK_NON_CET_BINARIES_NON_EHCONT', 0x3, 0x03, 36),
      new Pc2Field('XTENDED_CONTROL_FLOW_GUARD', 0x1, 0x03, 40),
      new Pc2Field('XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF', 0x2, 0x03, 40),
      new Pc2Field('XTENDED_CONTROL_FLOW_GUARD_RESERVED', 0x3, 0x03, 40),
      new Pc2Field('CET_DYNAMIC_APIS_OUT_OF_PROC_ONLY', 0x1, 0x03, 48),
      new Pc2Field(
          'CET_DYNAMIC_APIS_OUT_OF_PROC_ONLY_ALWAYS_OFF', 0x2, 0x03, 48),
      new Pc2Field('CET_DYNAMIC_APIS_OUT_OF_PROC_ONLY_RESERVED', 0x3, 0x03, 48),
      new Pc2Field('RESTRICT_CORE_SHARING', 0x1, 0x03, 52),
      new Pc2Field('RESTRICT_CORE_SHARING_ALWAYS_OFF', 0x2, 0x03, 52),
      new Pc2Field('RESTRICT_CORE_SHARING_RESERVED', 0x3, 0x03, 52),
      new Pc2Field('FSCTL_SYSTEM_CALL_DISABLE', 0x1, 0x03, 56),
      new Pc2Field('FSCTL_SYSTEM_CALL_DISABLE_ALWAYS_OFF', 0x2, 0x03, 56),
      new Pc2Field('FSCTL_SYSTEM_CALL_DISABLE_RESERVED', 0x3, 0x03, 56),
    ];
  }

  /**
   * @param str Hex encoded data.
   * @return bytes Decoded bytes.
   */
  parseHexString(str: string): Uint8Array {
    assert((str.length % 2 === 0), 'str must have even length');
    const bytes = new Uint8Array(str.length / 2);
    for (let idx = 0; idx < str.length / 2; idx++) {
      bytes[idx] = parseInt(str.slice(idx * 2, idx * 2 + 2), 16);
    }
    return bytes;
  }

  /**
   * Return a list of platform mitigation which are set in |mitigations|.
   * Mitigations will be in the same order as Winbase.h.
   * @param mitigations Hex encoded process mitigation flags.
   * @return Matched mitigation values.
   */
  enabledMitigations(mitigations: string): string[] {
    const bytes = this.parseHexString(mitigations);
    const output = [];
    for (const item of this.fields) {
      if (item.isFieldSet(bytes)) {
        output.push(item.mitigation);
      }
    }
    return output;
  }
}

const DECODE_MITIGATIONS = new DecodeMitigations();

const WELL_KNOWN_SIDS: {[sid: string]: string} = {
  'S-1-15-3-1': 'InternetClient',
  'S-1-15-3-2': 'InternetClientServer',
  'S-1-15-3-3': 'PrivateNetworkClientServer',
  'S-1-15-3-4': 'PicturesLibrary',
  'S-1-15-3-5': 'VideosLibrary',
  'S-1-15-3-6': 'MusicLibrary',
  'S-1-15-3-7': 'DocumentsLibrary',
  'S-1-15-3-8': 'EnterpriseAuthentication',
  'S-1-15-3-9': 'SharedUserCertificates',
  'S-1-15-3-10': 'RemovableStorage',
  'S-1-15-3-11': 'Appointments',
  'S-1-15-3-12': 'Contacts',
  'S-1-15-3-1024-3424233489-972189580-2057154623-747635277-1604371224-316187997-3786583170-1043257646':
      'chromeInstallFiles',
  'S-1-15-3-1024-1502825166-1963708345-2616377461-2562897074-4192028372-3968301570-1997628692-1435953622':
      'lpacAppExperience',
  'S-1-15-3-1024-2302894289-466761758-1166120688-1039016420-2430351297-4240214049-4028510897-3317428798':
      'lpacChromeInstallFiles',
  'S-1-15-3-1024-2405443489-874036122-4286035555-1823921565-1746547431-2453885448-3625952902-991631256':
      'lpacCom',
  'S-1-15-3-1024-3203351429-2120443784-2872670797-1918958302-2829055647-4275794519-765664414-2751773334':
      'lpacCryptoServices',
  'S-1-15-3-1024-126078593-3658686728-1984883306-821399696-3684079960-564038680-3414880098-3435825201':
      'lpacEnterprisePolicyChangeNotifications',
  'S-1-15-3-1024-1788129303-2183208577-3999474272-3147359985-1757322193-3815756386-151582180-1888101193':
      'lpacIdentityServices',
  'S-1-15-3-1024-3153509613-960666767-3724611135-2725662640-12138253-543910227-1950414635-4190290187':
      'lpacInstrumentation',
  'S-1-15-3-1024-1692970155-4054893335-185714091-3362601943-3526593181-1159816984-2199008581-497492991':
      'lpacMedia',
  'S-1-15-3-1024-220022770-701261984-3991292956-4208751020-2918293058-3396419331-1700932348-2078364891':
      'lpacPnPNotifications',
  'S-1-15-3-1024-528118966-3876874398-709513571-1907873084-3598227634-3698730060-278077788-3990600205':
      'lpacServicesManagement',
  'S-1-15-3-1024-1864111754-776273317-3666925027-2523908081-3792458206-3582472437-4114419977-1582884857':
      'lpacSessionManagement',
  'S-1-15-3-1024-1065365936-1281604716-3511738428-1654721687-432734479-3232135806-4053264122-3456934681':
      'registryRead',
};

/**
 * Maps capabilities to well known values.
 */
function mapCapabilitySid(sid: string): string {
  if (WELL_KNOWN_SIDS[sid]) {
    return WELL_KNOWN_SIDS[sid]!;
  }
  return sid;
}

/**
 * Adds a row to the sandbox-status table.
 */
function addRow(args: Node[]) {
  const row = document.createElement('tr');
  for (const td of args) {
    row.appendChild(td);
  }
  getRequiredElement('sandbox-status').appendChild(row);
}

/**
 * Makes a <td> containing arg as textContent.
 */
function makeTextEntry(textContent: string): Node {
  const col = document.createElement('td');
  col.textContent = textContent;
  return col;
}

/**
 * Makes a <td> containing formatted component filter flags.
 */
function makeComponentFilterEntry(policy: PolicyDiagnostic): Node {
  const fixed = document.createElement('div');
  fixed.classList.add('mitigations');
  fixed.innerText = policy.componentFilters;
  const col = document.createElement('td');
  col.appendChild(fixed);
  return col;
}

/**
 * Makes an expandable <td> containing arg as textContent.
 */
function makeExpandableEntry(mainEntry: string, expandable: Expandable): Node {
  const button = document.createElement('div');
  const expand = document.createElement('div');
  button.innerText = '\u2795';  // (+)
  button.classList.add('expander');
  button.addEventListener('click', function() {
    if (expandable.onClick(expand)) {
      button.innerText = '\u2796';  // (-)
    } else {
      button.innerText = '\u2795';  // (+)
    }
  });
  const fixed = document.createElement('div');
  fixed.classList.add('mitigations');
  fixed.innerText = mainEntry;

  const col = document.createElement('td');
  col.appendChild(button);
  col.appendChild(fixed);
  col.appendChild(expand);
  return col;
}

abstract class Expandable {
  expanded: boolean = false;

  onClick(col: HTMLElement): boolean {
    this.expanded = !this.expanded;
    col.innerText = this.getText();
    return this.expanded;
  }

  abstract getText(): string;
}

class MitigationEntryExpandable extends Expandable {
  mitigations: string;

  constructor(mitigations: string) {
    super();
    this.mitigations = mitigations;
  }

  override getText(): string {
    if (this.expanded) {
      return DECODE_MITIGATIONS.enabledMitigations(this.mitigations).join('\n');
    } else {
      return '';
    }
  }
}

class AppContainerEntryExpandable extends Expandable {
  caps: string[];

  constructor(caps: string[]) {
    super();
    this.caps = caps;
  }

  override getText(): string {
    if (this.expanded) {
      return this.caps.map(mapCapabilitySid).sort().join('\n');
    } else {
      return '';
    }
  }
}

/**
 * Adds a mitigations entry that can expand to show friendly names of the
 * mitigations.
 */
function makeMitigationEntry(platformMitigations: string): Node {
  const expander = new MitigationEntryExpandable(platformMitigations);
  return makeExpandableEntry(platformMitigations, expander);
}

/**
 * Formats a lowbox sid or appcontainer configuration (policies can only
 * have one or the other).
 */
function makeLowboxAcEntry(policy: PolicyDiagnostic): Node {
  if (policy.lowboxSid) {
    // Lowbox token does not have capabilities but should match AC entries.
    const fixed = document.createElement('div');
    fixed.classList.add('mitigations');
    fixed.innerText = policy.lowboxSid;
    const col = document.createElement('td');
    col.appendChild(fixed);
    return col;
  }
  if (policy.appContainerSid) {
    // AC has identifying SID plus lockdown capabilities.
    const expander =
        new AppContainerEntryExpandable(policy.appContainerCapabilities);
    return makeExpandableEntry(policy.appContainerSid, expander);
  }
  return makeTextEntry('');
}

/**
 * Adds policy information for a process to the sandbox-status table.
 */
function addRowForProcess(
    pid: number, type: string, name: string, sandbox: string,
    policy: PolicyDiagnostic) {
  if (policy) {
    // Text-only items.
    const entries = [
      String(pid),
      type,
      name,
      sandbox,
      policy.lockdownLevel,
      policy.desiredIntegrityLevel,
    ].map(makeTextEntry);
    entries.push(makeMitigationEntry(policy.platformMitigations));
    entries.push(makeComponentFilterEntry(policy));
    entries.push(makeLowboxAcEntry(policy));
    addRow(entries);
  } else {
    addRow([String(pid), type, name, 'Not Sandboxed', '', '', '', '', ''].map(
        makeTextEntry));
  }
}

function onGetSandboxDiagnostics(results: SandboxDiagnostics) {
  // Make it easy to look up policies.
  const policies: Map<number, PolicyDiagnostic> = new Map();
  for (const policy of results.policies) {
    policies.set(policy.processId, policy);
  }

  // Titles.
  addRow([
    'Process',
    'Type',
    'Name',
    'Sandbox',
    'Lockdown',
    'Integrity',
    'Mitigations',
    'Component Filter',
    'Lowbox/AppContainer',
  ].map(makeTextEntry));

  // Browser Processes.
  for (const process of results.browser) {
    const pid = process.processId;
    const name = process.name || process.metricsName;
    addRowForProcess(
        pid,
        process.processType,
        name,
        process.sandboxType,
        policies.get(pid)!,
    );
  }

  // Renderer Processes.
  for (const process of results.renderer) {
    const pid = process.processId;
    addRowForProcess(pid, 'Renderer', '', 'Renderer', policies.get(pid)!);
  }

  // Raw Diagnostics.
  getRequiredElement('raw-info').textContent =
      'features: ' + JSON.stringify(results.features, null, 2) + '\n' +
      'policies: ' + JSON.stringify(results.policies, null, 2);
}

document.addEventListener('DOMContentLoaded', () => {
  sendWithPromise('requestSandboxDiagnostics').then(onGetSandboxDiagnostics);
});