tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

decompression-corrupt-input.tentative.any.js (11405B)


      1 // META: global=window,worker
      2 
      3 "use strict";
      4 
      5 // The zstd-compressed bytes for the string "expected output"
      6 // where the optional checksum value has been included in the data.
      7 //
      8 // Each section of the data is labeled according to RFC 8878.
      9 // https://datatracker.ietf.org/doc/html/rfc8878
     10 const compressedZstdBytesWithChecksum = new Uint8Array([
     11  // Section 3.1.1 - Magic Number (4 bytes)
     12  0x28, 0xb5, 0x2f, 0xfd,
     13  // Section 3.1.1.1 - Frame Header (2 to 14 bytes)
     14  0x04,       // Section 3.1.1.1.1 - Frame Header Descriptor
     15  0x58,       // Section 3.1.1.1.2 - Window Descriptor
     16  0x79, 0x00, // Section 3.1.1.1.3 - Dictionary ID
     17  // Section 3.1.1.2 - Block Data (16 bytes)
     18  0x00, 0x65, 0x78, 0x70,
     19  0x65, 0x63, 0x74, 0x65,
     20  0x64, 0x20, 0x6f, 0x75,
     21  0x74, 0x70, 0x75, 0x74,
     22  // Section 3.1.1 - Content Checksum (4 bytes)
     23  0x5b, 0x11, 0xc6, 0x85,
     24 ]);
     25 
     26 // The zstd-compressed bytes for the string "expected output",
     27 // where the optional checksum value has been omitted from the data.
     28 //
     29 // Each section of the data is labeled according to RFC 8878.
     30 // https://datatracker.ietf.org/doc/html/rfc8878
     31 const compressedZstdBytesNoChecksum = new Uint8Array([
     32  // Section 3.1.1 - Magic Number (4 bytes)
     33  0x28, 0xb5, 0x2f, 0xfd,
     34  // Section 3.1.1.1 - Frame Header (2 to 14 bytes)
     35  0x00,       // Section 3.1.1.1.1 - Frame Header Descriptor
     36  0x58,       // Section 3.1.1.1.2 - Window Descriptor
     37  0x79, 0x00, // Section 3.1.1.1.3 - Dictionary ID
     38  // Section 3.1.1.2 - Block Data (16 bytes)
     39  0x00, 0x65, 0x78, 0x70,
     40  0x65, 0x63, 0x74, 0x65,
     41  0x64, 0x20, 0x6f, 0x75,
     42  0x74, 0x70, 0x75, 0x74,
     43 ]);
     44 
     45 // We define all tests with fields:
     46 //
     47 //  - name: descriptive string
     48 //  - type: "unchanged", "extendStart", "extendEnd", "truncateStart", "truncateEnd", or "field"
     49 //  - removeBytes / extraBytes / offset / length / value as needed
     50 //
     51 //  - expectedResultWithChecksum: "success" | "error" | "corrupt"
     52 //  - expectedResultNoChecksum: "success" | "error" | "corrupt"
     53 //    (if omitted/undefined, the no-checksum path won't be tested)
     54 //
     55 const expectations = [
     56  {
     57    name: "Unchanged",
     58    type: "unchanged",
     59    expectedResultWithChecksum: "success",
     60    expectedResultNoChecksum: "success",
     61    compressedZstdBytesWithChecksum,
     62    compressedZstdBytesNoChecksum,
     63  },
     64  {
     65    name: "Truncate 1 byte from start",
     66    type: "truncateStart",
     67    removeBytes: 1,
     68    expectedResultWithChecksum: "error",
     69    expectedResultNoChecksum: "error",
     70    compressedZstdBytesWithChecksum,
     71    compressedZstdBytesNoChecksum,
     72  },
     73  {
     74    name: "Truncate 1 byte from end",
     75    type: "truncateEnd",
     76    removeBytes: 1,
     77    expectedResultWithChecksum: "error",
     78    expectedResultNoChecksum: "error",
     79    compressedZstdBytesWithChecksum,
     80    compressedZstdBytesNoChecksum,
     81  },
     82  {
     83    name: "Truncate 2 bytes from start",
     84    type: "truncateStart",
     85    removeBytes: 2,
     86    expectedResultWithChecksum: "error",
     87    expectedResultNoChecksum: "error",
     88    compressedZstdBytesWithChecksum,
     89    compressedZstdBytesNoChecksum,
     90  },
     91  {
     92    name: "Truncate 2 bytes from end",
     93    type: "truncateEnd",
     94    removeBytes: 2,
     95    expectedResultWithChecksum: "error",
     96    expectedResultNoChecksum: "error",
     97    compressedZstdBytesWithChecksum,
     98    compressedZstdBytesNoChecksum,
     99  },
    100  {
    101    name: "Extend from start with [0x00]",
    102    type: "extendStart",
    103    extraBytes: [0x00],
    104    expectedResultWithChecksum: "error",
    105    expectedResultNoChecksum: "error",
    106    compressedZstdBytesWithChecksum,
    107    compressedZstdBytesNoChecksum,
    108  },
    109  {
    110    name: "Extend from end with [0x00]",
    111    type: "extendEnd",
    112    extraBytes: [0x00],
    113    expectedResultWithChecksum: "error",
    114    expectedResultNoChecksum: "error",
    115    compressedZstdBytesWithChecksum,
    116    compressedZstdBytesNoChecksum,
    117  },
    118  {
    119    name: "Extend from end [0xff]",
    120    type: "extendEnd",
    121    extraBytes: [0xff],
    122    expectedResultWithChecksum: "error",
    123    expectedResultNoChecksum: "error",
    124    compressedZstdBytesWithChecksum,
    125    compressedZstdBytesNoChecksum,
    126  },
    127  {
    128    name: "Extend from start with [0xff]",
    129    type: "extendStart",
    130    extraBytes: [0xff],
    131    expectedResultWithChecksum: "error",
    132    expectedResultNoChecksum: "error",
    133    compressedZstdBytesWithChecksum,
    134    compressedZstdBytesNoChecksum,
    135  },
    136  {
    137    name: "Extend from end with [0xff]",
    138    type: "extendEnd",
    139    extraBytes: [0xff],
    140    expectedResultWithChecksum: "error",
    141    expectedResultNoChecksum: "error",
    142    compressedZstdBytesWithChecksum,
    143    compressedZstdBytesNoChecksum,
    144  },
    145  {
    146    name: "Extend from start with [0x00, 0x00]",
    147    type: "extendStart",
    148    extraBytes: [0x00, 0x00],
    149    expectedResultWithChecksum: "error",
    150    expectedResultNoChecksum: "error",
    151    compressedZstdBytesWithChecksum,
    152    compressedZstdBytesNoChecksum,
    153  },
    154  {
    155    name: "Extend from end with [0x00, 0x00]",
    156    type: "extendEnd",
    157    extraBytes: [0x00, 0x00],
    158    expectedResultWithChecksum: "error",
    159    expectedResultNoChecksum: "error",
    160    compressedZstdBytesWithChecksum,
    161    compressedZstdBytesNoChecksum,
    162  },
    163  {
    164    name: "Magic Number: offset=0 replace with 0xff",
    165    type: "field",
    166    offset: 0,
    167    length: 1,
    168    value: 0xff,
    169    expectedResultWithChecksum: "error",
    170    expectedResultNoChecksum: "error",
    171    compressedZstdBytesWithChecksum,
    172    compressedZstdBytesNoChecksum,
    173  },
    174  {
    175    name: "Magic Number: offset=1 replace with 0xff",
    176    type: "field",
    177    offset: 1,
    178    length: 1,
    179    value: 0xff,
    180    expectedResultWithChecksum: "error",
    181    expectedResultNoChecksum: "error",
    182    compressedZstdBytesWithChecksum,
    183    compressedZstdBytesNoChecksum,
    184  },
    185  {
    186    name: "Magic Number: offset=2 replace with 0xff",
    187    type: "field",
    188    offset: 2,
    189    length: 1,
    190    value: 0xff,
    191    expectedResultWithChecksum: "error",
    192    expectedResultNoChecksum: "error",
    193    compressedZstdBytesWithChecksum,
    194    compressedZstdBytesNoChecksum,
    195  },
    196  {
    197    name: "Magic Number: offset=3 replace with 0xff",
    198    type: "field",
    199    offset: 3,
    200    length: 1,
    201    value: 0xff,
    202    expectedResultWithChecksum: "error",
    203    expectedResultNoChecksum: "error",
    204    compressedZstdBytesWithChecksum,
    205    compressedZstdBytesNoChecksum,
    206  },
    207  {
    208    name: "Frame Header Descriptor: offset=4 replace with 0x05",
    209    type: "field",
    210    offset: 4,
    211    length: 1,
    212    value: 0x05,
    213    expectedResultWithChecksum: "error",
    214    expectedResultNoChecksum: "error",
    215    compressedZstdBytesWithChecksum,
    216    compressedZstdBytesNoChecksum,
    217  },
    218  {
    219    name: "Window Descriptor: offset=5 replace with 0xff",
    220    type: "field",
    221    offset: 5,
    222    length: 1,
    223    value: 0xff,
    224    expectedResultWithChecksum: "error",
    225    expectedResultNoChecksum: "error",
    226    compressedZstdBytesWithChecksum,
    227    compressedZstdBytesNoChecksum,
    228  },
    229  {
    230    name: "Dictionary ID: offset=6 length=2 replace with 0x00",
    231    type: "field",
    232    offset: 6,
    233    length: 2,
    234    value: 0x00,
    235    expectedResultWithChecksum: "error",
    236    expectedResultNoChecksum: "error",
    237    compressedZstdBytesWithChecksum,
    238    compressedZstdBytesNoChecksum,
    239  },
    240  {
    241    name: "Block Data: offset=18 replace with 0x64",
    242    type: "field",
    243    offset: 18,
    244    length: 1,
    245    value: 0x64,
    246    expectedResultWithChecksum: "error",
    247    expectedResultNoChecksum: "corrupt",
    248    compressedZstdBytesWithChecksum,
    249    compressedZstdBytesNoChecksum,
    250  },
    251  {
    252    name: "Block Data: offset=23 replace with 0x73",
    253    type: "field",
    254    offset: 23,
    255    length: 1,
    256    value: 0x73,
    257    expectedResultWithChecksum: "error",
    258    expectedResultNoChecksum: "corrupt",
    259    compressedZstdBytesWithChecksum,
    260    compressedZstdBytesNoChecksum,
    261  },
    262  // The following test cases mutate the checksum itself, so there are no expected
    263  // results for the no-checksum scenario.
    264  {
    265    name: "Content Checksum: offset=-4 replace with 0x00",
    266    type: "field",
    267    offset: -4,
    268    length: 1,
    269    value: 0x00,
    270    expectedResultWithChecksum: "error",
    271    compressedZstdBytesWithChecksum,
    272  },
    273  {
    274    name: "Content Checksum: offset=-3 replace with 0xff",
    275    type: "field",
    276    offset: -3,
    277    length: 1,
    278    value: 0xff,
    279    expectedResultWithChecksum: "error",
    280    compressedZstdBytesWithChecksum,
    281  },
    282  {
    283    name: "Content Checksum: offset=-2 replace with 0x00",
    284    type: "field",
    285    offset: -2,
    286    length: 1,
    287    value: 0x00,
    288    expectedResultWithChecksum: "error",
    289    compressedZstdBytesWithChecksum,
    290  },
    291  {
    292    name: "Content Checksum: offset=-1 replace with 0xff",
    293    type: "field",
    294    offset: -1,
    295    length: 1,
    296    value: 0xff,
    297    expectedResultWithChecksum: "error",
    298    compressedZstdBytesWithChecksum,
    299  },
    300 ];
    301 
    302 async function tryDecompress(input) {
    303  const ds = new DecompressionStream("zstd");
    304  const reader = ds.readable.getReader();
    305  const writer = ds.writable.getWriter();
    306 
    307  const writePromise = writer.write(input).catch(() => {});
    308  const writerClosePromise = writer.close().catch(() => {});
    309 
    310  let out = [];
    311  while (true) {
    312    try {
    313      const { value, done } = await reader.read();
    314      if (done) {
    315        break;
    316      }
    317      out = out.concat(Array.from(value));
    318    } catch (e) {
    319      if (e.name === "TypeError") {
    320        return { result: "error" };
    321      }
    322      return { result: e.name };
    323    }
    324  }
    325 
    326  await writePromise;
    327  await writerClosePromise;
    328 
    329  const textDecoder = new TextDecoder();
    330  const text = textDecoder.decode(new Uint8Array(out));
    331  if (text !== "expected output") {
    332    return { result: "corrupt" };
    333  }
    334  return { result: "success" };
    335 }
    336 
    337 function produceTestInput(testCase, compressedBytes) {
    338  switch (testCase.type) {
    339    case "unchanged":
    340      return compressedBytes;
    341 
    342    case "truncateStart": {
    343      return compressedBytes.slice(testCase.removeBytes);
    344    }
    345    case "truncateEnd": {
    346      return compressedBytes.slice(
    347        0,
    348        compressedBytes.length - testCase.removeBytes
    349      );
    350    }
    351    case "extendStart": {
    352      return new Uint8Array([...testCase.extraBytes, ...compressedBytes]);
    353    }
    354    case "extendEnd": {
    355      return new Uint8Array([...compressedBytes, ...testCase.extraBytes]);
    356    }
    357    case "field": {
    358      const output = new Uint8Array(compressedBytes);
    359      let realOffset = testCase.offset;
    360      if (realOffset < 0) {
    361        realOffset = output.length + realOffset;
    362      }
    363      for (let i = 0; i < testCase.length; i++) {
    364        output[realOffset + i] = testCase.value;
    365      }
    366      return output;
    367    }
    368    default: {
    369      throw new Error(`Unknown testCase type: ${testCase.type}`);
    370    }
    371  }
    372 }
    373 
    374 for (const testCase of expectations) {
    375  promise_test(async t => {
    376    const inputWithChecksum = produceTestInput(
    377      testCase,
    378      testCase.compressedZstdBytesWithChecksum
    379    );
    380    const { result: resultWithChecksum } =
    381      await tryDecompress(inputWithChecksum);
    382 
    383    assert_equals(
    384      resultWithChecksum,
    385      testCase.expectedResultWithChecksum,
    386      `${testCase.name} (with checksum)`
    387    );
    388 
    389    let resultNoChecksum;
    390    if (testCase.expectedResultNoChecksum !== undefined) {
    391      const inputNoChecksum = produceTestInput(
    392        testCase,
    393        testCase.compressedZstdBytesNoChecksum
    394      );
    395      const { result } = await tryDecompress(inputNoChecksum);
    396      resultNoChecksum = result;
    397    }
    398 
    399    if (testCase.expectedResultNoChecksum !== undefined) {
    400      assert_equals(
    401        resultNoChecksum,
    402        testCase.expectedResultNoChecksum,
    403        `${testCase.name} (no checksum)`
    404      );
    405    }
    406  }, testCase.name);
    407 }