nodejs / nodejs/node

Bug: Intermittent multi-block PBKDF2 corruption in async crypto/WebCrypto APIs on Windows

Đang mở
#65,696 1 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

needs more info
Ngôn ngữ chính
JavaScript
Star
122k
Fork
37.3k
Merge trung bình
4 ngày 2 giờ
Pull request đã merge (30 ngày)
283

Mô tả

Version

v24.x, v26.x, not v18

Platform
Microsoft Windows NT 10.0.26200.0 x64
Subsystem

crypto, webcrypto

What steps will reproduce the bug?

When invoking asynchronous multi-block PBKDF2 operations (requesting outputs greater than 32 bytes for SHA-256) with a high iteration count (1,000,000), the resulting buffer intermittently suffers from deterministic, block-level corruption.

Run the following test script multiple times on Windows. It processes identical jobs in parallel/sequential batches and asserts results against static, universally cross-verified values:

const { pbkdf2, pbkdf2Sync, webcrypto } = require("node:crypto");

const EXPECTED = Buffer.from(
    "f6de9e4c4c32a4d293b4415584ac9cca18d7314d01c424650d13e7a853e44d04" +
    "b32d335f56f0940d2cb44e89c94ffbcf15f4c86cdb3c83969077f55aa25176fc",
    "hex");

async function DeriveWebCrypto(password, salt) {
    const key = await webcrypto.subtle.importKey(
        "raw", new TextEncoder().encode(password), { name: "PBKDF2" }, false, ["deriveBits"]);
    const result = await webcrypto.subtle.deriveBits({
        name: "PBKDF2", salt, iterations: 1000000, hash: "SHA-256"
    }, key, 512);
    return new Uint8Array(result);
}

function DeriveNodePbkdf2(password, salt) {
    return new Promise((resolve, reject) => {
        pbkdf2(password, salt, 1000000, 64, "sha256", (error, actual) => {
            if(error) reject(error);
            else resolve(actual);
        });
    });
}

async function Main() {
    const sequential = process.argv.includes("--sequential");
    const isolated_salt = process.argv.includes("--isolated-salt");
    const node_pbkdf2 = process.argv.includes("--node-pbkdf2");
    const salt_bytes = Buffer.from("a4b770fbe99a32070d977a04c0ebfde8c4", "hex");
    const salt = isolated_salt
        ? new Uint8Array(new Uint8Array(salt_bytes).buffer.slice(0))
        : salt_bytes;
    const original_salt = Buffer.from(salt);
    const derive = node_pbkdf2 ? DeriveNodePbkdf2 : DeriveWebCrypto;
    const api_name = node_pbkdf2 ? "crypto.pbkdf2" : "WebCrypto";

    console.log(
        `Node ${process.version}, OpenSSL ${process.versions.openssl}, api=${api_name}, ` +
        `mode=${sequential ? "sequential" : "parallel"}, ` +
        `salt=${isolated_salt ? "isolated Uint8Array" : "Buffer"}`);

    for(let batch = 0; batch < 20; batch++) {
        const actual_sync = pbkdf2Sync("pass", salt, 1000000, 64, "sha256");
        if(!actual_sync.equals(EXPECTED)) {
            throw new Error(`pbkdf2Sync mismatch batch=${batch}`);
        }

        if(sequential) {
            for(let index = 0; index < 32; index++) {
                CheckResult(await derive("pass", salt), salt, batch, index, api_name);
            }
        } else {
            const jobs = [];
            for(let index = 0; index < 32; index++) {
                jobs.push(derive("pass", salt).then(actual =>
                    CheckResult(actual, salt, batch, index, api_name)));
            }
            await Promise.all(jobs);
        }
        console.log(`batch ${batch + 1}/20 passed`);
    }
}

function CheckResult(actual, salt, batch, index, api_name) {
    const actual_buffer = Buffer.from(actual.buffer, actual.byteOffset, actual.byteLength);
    if(!actual_buffer.equals(EXPECTED)) {
        throw new Error(
            `${api_name} mismatch batch=${batch} index=${index}\n` +
            `Expected: ${EXPECTED.toString("hex")}\n` +
            `Actual:   ${actual_buffer.toString("hex")}`
        );
    }
}

Main().catch(console.error);
How often does it reproduce? Is there a required condition?

Intermittently. It requires several continuous test script execution runs on Windows to trigger a race-condition threshold.

What is the expected behavior? Why is that the expected behavior?

Every single asynchronous multi-block calculation should consistently output identical 64-byte chunks matching pbkdf2Sync.

What do you see instead?

The script occasionally crashes with a mismatch exception explicitly targeting either block 1 or block 2. One 32-byte block segment matches perfectly while the remaining 32 bytes contain arbitrary memory context corruption.

Additional information
Key Isolation Characteristics Proven via Direct Testing:
  1. Not a JS Buffer Offset/Compaction Mismatch: The failure manifests identically when passing a strict, unpooled array backed by its own completely isolated ArrayBuffer (new Uint8Array(new ArrayBuffer(17))).
  2. Not a libuv Thread Concurrency Mismatch: The failure continues to reproduce sequentially (--sequential) even when constraining execution pool throughput down to a thread size limit of UV_THREADPOOL_SIZE=1.
  3. Affects Both Subsystems Globally: The regression is independent of the JS-facing abstraction; it triggers identically within crypto.subtle.deriveBits AND legacy asynchronous callback-based crypto.pbkdf2.
  4. Deterministic Base Execution: Synchronous computation (crypto.pbkdf2Sync) executes flawlessly every single time and never suffers from this block corruption.
Root Cause Analysis

Because the failure happens sequentially over isolated structures on async worker frames, it points directly to an OpenSSL thread state register or CPU context tracking leakage. During long-running asynchronous execution loops where native worker chunks pause and yield back to the Windows thread scheduler across macro-ticks, the underlying native C++ state bindings (PBKDF2Job / CryptoJob) fail to preserve the continuity of intermediate EVP_PKEY_CTX states or block indices. When a worker thread wakes up to compile the subsequent 32-byte block segment, it picks up dirty or improperly restored registers, altering the cryptographic block output.

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Chạy script tái hiện được cung cấp trên Windows với WebCrypto và crypto.pbkdf2, bao gồm các chế độ salt tuần tự và salt cô lập, rồi so sánh kết quả bất đồng bộ với pbkdf2Sync. Sau đó kiểm tra đường dẫn PBKDF2Job/CryptoJob bất đồng bộ được đề cập trong báo cáo. Được xem là hoàn tất khi các lần dẫn xuất 64 byte qua nhiều block lặp lại luôn khớp với EXPECTED mà không bị hỏng dữ liệu.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
javascript, node.js
Lĩnh vực
backend, security
Loại issue
Lỗi
Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức độ hoạt động
Sôi nổi
Độ rõ ràng
Khá rõ ràng
Mức phù hợp với người mới
45/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.