Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

libuv assertion on Windows with Node.js 23.x #56645

Open
aduh95 opened this issue Jan 17, 2025 · 2 comments
Open

libuv assertion on Windows with Node.js 23.x #56645

aduh95 opened this issue Jan 17, 2025 · 2 comments
Labels
libuv Issues and PRs related to the libuv dependency or the uv binding. windows Issues and PRs related to the Windows platform.

Comments

@aduh95
Copy link
Contributor

aduh95 commented Jan 17, 2025

Version

23.x

Platform

Windows

Subsystem

No response

What steps will reproduce the bug?

Write the following file as registryServer.mjs:

registryServer.mjs

The original file is https://github.com/nodejs/corepack/blob/main/tests/_registryServer.mjs, I tried to trim the unrelated stuff but it's still a large file:

import { createHash, createSign, generateKeyPairSync } from "node:crypto";
import { once } from "node:events";
import { createServer } from "node:http";
import { gzipSync } from "node:zlib";

let privateKey, keyid;

({ privateKey } = generateKeyPairSync(`ec`, {
  namedCurve: `sect239k1`,
}));

const { privateKey: p, publicKey } = generateKeyPairSync(`ec`, {
  namedCurve: `sect239k1`,
  publicKeyEncoding: {
    type: `spki`,
    format: `pem`,
  },
});
privateKey ??= p;
keyid = `SHA256:${createHash(`SHA256`).end(publicKey).digest(`base64`)}`;
process.env.COREPACK_INTEGRITY_KEYS = JSON.stringify({
  npm: [
    {
      expires: null,
      keyid,
      keytype: `ecdsa-sha2-sect239k1`,
      scheme: `ecdsa-sha2-sect239k1`,
      key: publicKey.split(`\n`).slice(1, -2).join(``),
    },
  ],
});

function createSimpleTarArchive(fileName, fileContent, mode = 0o644) {
  const contentBuffer = Buffer.from(fileContent);

  const header = Buffer.alloc(512); // TAR headers are 512 bytes
  header.write(fileName);
  header.write(`100${mode.toString(8)} `, 100, 7, `utf-8`); // File mode (octal) followed by a space
  header.write(`0001750 `, 108, 8, `utf-8`); // Owner's numeric user ID (octal) followed by a space
  header.write(`0001750 `, 116, 8, `utf-8`); // Group's numeric user ID (octal) followed by a space
  header.write(`${contentBuffer.length.toString(8)} `, 124, 12, `utf-8`); // File size in bytes (octal) followed by a space
  header.write(
    `${Math.floor(new Date(2000, 1, 1) / 1000).toString(8)} `,
    136,
    12,
    `utf-8`
  ); // Last modification time in numeric Unix time format (octal) followed by a space
  header.fill(` `, 148, 156); // Fill checksum area with spaces for calculation
  header.write(`ustar  `, 257, 8, `utf-8`); // UStar indicator

  // Calculate and write the checksum. Note: This is a simplified calculation not recommended for production
  const checksum = header.reduce((sum, value) => sum + value, 0);
  header.write(`${checksum.toString(8)}\0 `, 148, 8, `utf-8`); // Write checksum in octal followed by null and space

  return Buffer.concat([
    header,
    contentBuffer,
    Buffer.alloc(512 - (contentBuffer.length % 512)),
  ]);
}

const mockPackageTarGz = gzipSync(
  Buffer.concat([
    createSimpleTarArchive(
      `package/bin/pnpm.js`,
      `#!/usr/bin/env node\nconsole.log("pnpm: Hello from custom registry");\n`,
      0o755
    ),
    createSimpleTarArchive(
      `package/package.json`,
      JSON.stringify({
        bin: {
          pnpm: `bin/pnpm.js`,
        },
      })
    ),
    Buffer.alloc(1024),
  ])
);
const shasum = createHash(`sha1`).update(mockPackageTarGz).digest(`hex`);
const integrity = `sha512-${createHash(`sha512`)
  .update(mockPackageTarGz)
  .digest(`base64`)}`;

const registry = {
  __proto__: null,
  pnpm: [`42.9998.9999`],
};

function generateSignature(packageName, version) {
  if (privateKey == null) return undefined;
  const sign = createSign(`SHA256`).end(
    `${packageName}@${version}:${integrity}`
  );
  return {
    integrity,
    signatures: [
      {
        keyid,
        sig: sign.sign(privateKey, `base64`),
      },
    ],
  };
}
function generateVersionMetadata(packageName, version) {
  return {
    name: packageName,
    version,
    bin: {
      [packageName]: `./bin/${packageName}.js`,
    },
    dist: {
      shasum,
      size: mockPackageTarGz.length,
      tarball: `https://registry.npmjs.org/${packageName}/-/${packageName}-${version}.tgz`,
      ...generateSignature(packageName, version),
    },
  };
}

const server = createServer((req, res) => {

  let slashPosition = req.url.indexOf(`/`, 1);
  if (req.url.charAt(1) === `@`)
    slashPosition = req.url.indexOf(`/`, slashPosition + 1);

  const packageName = req.url.slice(
    1,
    slashPosition === -1 ? undefined : slashPosition
  );
  if (packageName in registry) {
    if (req.url === `/${packageName}`) {
      // eslint-disable-next-line @typescript-eslint/naming-convention
      res.end(
        JSON.stringify({
          "dist-tags": {
            latest: registry[packageName].at(-1),
          },
          versions: Object.fromEntries(
            registry[packageName].map((version) => [
              version,
              generateVersionMetadata(packageName, version),
            ])
          ),
        })
      );
      return;
    }
    const isDownloadingRequest =
      req.url.slice(packageName.length + 1, packageName.length + 4) === `/-/`;
    let version;
    if (isDownloadingRequest) {
      const match = /^(.+)-(.+)\.tgz$/.exec(
        req.url.slice(packageName.length + 4)
      );
      if (match?.[1] === packageName) {
        version = match[2];
      }
    } else {
      version = req.url.slice(packageName.length + 2);
    }
    if (version === `latest`) version = registry[packageName].at(-1);
    if (registry[packageName].includes(version)) {
      res.end(
        isDownloadingRequest
          ? mockPackageTarGz
          : JSON.stringify(generateVersionMetadata(packageName, version))
      );
    } else {
      res.writeHead(404).end(`Not Found`);
      throw new Error(`unsupported request`, {
        cause: { url: req.url, packageName, version, isDownloadingRequest },
      });
    }
  } else {
    res.writeHead(500).end(`Internal Error`);
    throw new Error(`unsupported request`, {
      cause: { url: req.url, packageName },
    });
  }
});

server.listen(0, `localhost`);
await once(server, `listening`);

const { address, port } = server.address();

process.env.COREPACK_NPM_REGISTRY = `http://user:pass@${
  address.includes(`:`) ? `[${address}]` : address
}:${port}`;

server.unref();

Then run the following commands:

$env:COREPACK_ENABLE_PROJECT_SPEC=0
$env:NODE_OPTIONS="--import ./registryServer.mjs"
corepack pnpm@42.x --version

How often does it reproduce? Is there a required condition?

Always on Windows with Node.js 23.x, no required condition, tested with 23.0.0 (libuv 1.48.0), 23.4.0 (libuv 1.49.1), and 23.6.0 (libuv 1.49.2).

It does not reproduce on Linux nor macOS.

It does not reproduce on 22.13.2 (libuv 1.49.2), which makes me think it's not a libuv bug, but a Node.js one.

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

No assertions, the exit code should be 1

What do you see instead?

Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file c:\ws\deps\uv\src\win\async.c, line 76

The exit code is 3221226505.

Additional information

My initial thought was that it might be related to having an exception thrown while handling an HTTP request, but I wasn't able to reproduce with just that.

@aduh95 aduh95 added windows Issues and PRs related to the Windows platform. libuv Issues and PRs related to the libuv dependency or the uv binding. labels Jan 17, 2025
@aduh95
Copy link
Contributor Author

aduh95 commented Jan 17, 2025

/cc @nodejs/platform-windows @nodejs/libuv

@bnoordhuis
Copy link
Member

FWIW, I also agree it's not a libuv bug; it's libuv saying node is doing something wrong. Specifically, node calls uv_async_send() after uv_close().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
libuv Issues and PRs related to the libuv dependency or the uv binding. windows Issues and PRs related to the Windows platform.
Projects
None yet
Development

No branches or pull requests

2 participants