current commit
260618.22 · child_process: serialize advanced IPC messages natively
// Flags: --expose-internals
'use strict';

// Regression tests for the native `advanced` IPC codec (see PR #63933).
// Host-object classification and tag validation must match the previous
// JavaScript (v8.DefaultSerializer-based) behavior exactly.
const common = require('../common');
const assert = require('assert');
const { fork } = require('child_process');
const v8 = require('v8');
const { MessageChannel } = require('worker_threads');
const { internalBinding } = require('internal/test/binding');

if (process.argv[2] === 'inspect') {
  process.on('message', (value) => {
    process.send({
      isBuffer: Buffer.isBuffer(value),
      keys: Object.keys(value),
      visible: value?.visible,
    });
  });
  return;
}

function inspect(value) {
  return new Promise((resolve, reject) => {
    const child = fork(__filename, ['inspect'], {
      serialization: 'advanced',
      stdio: ['ignore', 'ignore', 'inherit', 'ipc'],
    });
    child.once('message', (message) => {
      child.disconnect();
      resolve(message);
    });
    try {
      child.send(value);
    } catch (err) {
      child.disconnect();
      reject(err);
    }
  });
}

async function main() {
  // 1) Spreading a non-ArrayBufferView host object must copy own properties
  //    with [[DefineOwnProperty]] semantics, i.e. without invoking inherited
  //    setters (the JS codec used `{ ...object }`).
  {
    const { port1, port2 } = new MessageChannel();
    port1.visible = 1;
    Object.defineProperty(Object.prototype, 'visible', {
      configurable: true,
      get() { return undefined; },
      set() { throw new Error('setter called'); },
    });
    let message;
    try {
      message = await inspect(port1);
    } finally {
      delete Object.prototype.visible;
      port1.close();
      port2.close();
    }
    assert.strictEqual(message.visible, 1);
    assert.strictEqual(message.keys[0], 'visible');
  }

  // 2) A Buffer whose `.constructor` is reassigned is classified by its
  //    constructor, exactly like v8.DefaultSerializer: it is no longer a
  //    Buffer on the receiving side.
  {
    const buf = Buffer.from('abc');
    buf.constructor = Uint8Array;
    const message = await inspect(buf);
    assert.strictEqual(message.isBuffer, false);
  }

  // 3) Conversely, a Uint8Array with `.constructor === Buffer` round-trips as
  //    a Buffer.
  {
    const uint8 = new Uint8Array([1, 2, 3]);
    uint8.constructor = Buffer;
    const message = await inspect(uint8);
    assert.strictEqual(message.isBuffer, true);
  }

  // 4) Tampering with Buffer.prototype.constructor must not fool the
  //    classifier: a genuine Buffer is then classified by its (now non-Buffer)
  //    constructor, matching v8.DefaultSerializer, which compares against a
  //    stable Buffer reference rather than re-reading Buffer.prototype.
  {
    const original = Buffer.prototype.constructor;
    Buffer.prototype.constructor = Uint8Array;
    let message;
    try {
      message = await inspect(Buffer.from('abc'));
    } finally {
      Buffer.prototype.constructor = original;
    }
    assert.strictEqual(message.isBuffer, false);
  }

  // 5) A host-object tag other than the two the codec emits (0 and 1) must be
  //    rejected rather than silently accepted.
  {
    const { deserialize } = internalBinding('ipc_serdes');

    class BadTagSerializer extends v8.DefaultSerializer {
      _writeHostObject(value) {
        this.writeUint32(2); // Valid tags are only 0 or 1
        return super._writeHostObject(value);
      }
    }
    const ser = new BadTagSerializer();
    ser.writeHeader();
    ser.writeValue(Buffer.from('x'));
    const payload = ser.releaseBuffer();

    assert.throws(() => deserialize(payload), { code: 'ERR_INVALID_STATE' });
  }
}

main().then(common.mustCall());