Skip to content

webcrypto: valid AES-GCM decrypt aborts (errors->Empty()) after tls.connect() with an undeserializable session (v24.x)Β #67203

Closed

Description

@webteamfrit

Version: v24.22.2 (also v16–v24 tested); not reproducible on v26.13.0
Platform: macOS arm64 (Darwin 25.6.0); OpenSSL 3.5.9

What steps will reproduce the bug?

// repro.mjs β€” run: node repro.mjs
import tls from 'node:tls';
import { webcrypto } from 'node:crypto';
const { subtle } = webcrypto;
const key = await subtle.importKey('raw', new Uint8Array(32), 'AES-GCM', false, ['encrypt', 'decrypt']);
const iv = new Uint8Array(12);
const ct = await subtle.encrypt({ name: 'AES-GCM', iv }, key, new Uint8Array([1, 2, 3]));
// A session buffer in the v24 serialized format whose DER payload is not a valid SSL_SESSION.
const session = Buffer.from(
  '006e6f64656a733a746c733a73657373696f6e3a31000014676174657761792e6578616d706c652e746573746d6f636b65642073657373696f6e',
  'hex',
);
const socket = tls.connect({ host: '127.0.0.1', port: 9, servername: 'gateway.example.test', session });
socket.on('error', () => {});
socket.destroy();
await subtle.decrypt({ name: 'AES-GCM', iv }, key, ct); // aborts here
console.log('ok');

What is the expected behavior? ok. A failed session deserialization should not affect an unrelated, valid WebCrypto operation.

What do you see instead?

# node[...]: v8::Maybe<bool> node::crypto::CipherJob<node::crypto::AESCipherTraits>::ToResult(...)
#  Assertion failed: errors->Empty()

The process is terminated with SIGABRT.

Additional information

  • TLSWrap::SetSession β†’ GetTLSSession β†’ d2i_SSL_SESSION fails and leaves the error on the JS thread's OpenSSL error queue. No ClearErrorOnReturn or MarkPopErrorOnReturn is used there (// TODO(tniessen): figure out error handling).
  • In v24.x, CipherJob::ToResult runs if (errors->Empty()) errors->Capture(); on the JS thread and then CHECK(errors->Empty()) on success. The stale error is captured into a successful job, and the process aborts.
  • In v26.x, ToResult no longer captures the queue, so the abort does not happen. Would that change be a candidate for a backport to v24.x (LTS)?
  • Separately, should SetSession clear or pop the error queue when deserialization fails?
  • In practice this was triggered by a test HTTP interceptor that emits fake TLS sessions, which undici then reuses (see the mswjs/interceptors report). Any stale OpenSSL error on the JS thread would have the same effect.

Activity

  1. webteamfrit commented on Nov 2, 2026

    @webteamfrit
    Author

    Related report on the interceptor side: mswjs/interceptors#892

  2. panva commented on Nov 2, 2026

    @panva
    Member

    The abort is handled by #66237 (crypto: isolate cipher job errors). CipherJob::ToResult() no longer captures unrelated errors from the main thread's OpenSSL error queue.

    It is already in v24.x-staging as ae35a8f slated for the next 24.x LTS release.

    The TLS session deserialization path should still clean up its OpenSSL errors separately. Feel free to open a separate issue for that.

  3. webteamfrit commented on Nov 2, 2026

    @webteamfrit
    Author

    Thanks @panva, confirmed: ae35a8f fixes the abort for us (verified on v26.13.0; we'll pick it up in the next 24.x as well). I opened #67204 for the TLS session deserialization error cleanup, as suggested. Closing this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions