Skip to content

pm view: report a matched version that is not an object instead of panicking - #43020

Closed
robobun wants to merge 1 commit into
mainfrom
robobun/17f331af/pm-view-non-object-version
Closed

robobun wants to merge 1 commit into
mainfrom
robobun/17f331af/pm-view-non-object-version

Conversation

@robobun

@robobun robobun commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • bun pm view and bun info abort with panic: internal error: entered unreachable code when the matched versions entry of the registry response is not an object, for example "versions": {"1.0.0": "x"}. A registry, or a host on the path of an http registry, can send that response.
  • The cause is view() (src/runtime/cli/pm_view_command.rs:291). It calls Expr::set on the matched entry. Expr::set (src/ast/expr.rs:496) is unreachable!() for a value that is not an object.

Fix

  • view() checks the matched entry before it uses it. If the entry is not an object, it prints error: registry manifest for "pkg" has a "1.0.0" version that is not an object and exits with code 1.
  • Correct because every later step reads fields of that entry. view() already exits with code 1 for a response that does not parse.
  • Verified: test/cli/install/bun-info.test.ts (6 new tests, canary c6b7fcb fails 6 of 6 with SIGABRT). The whole file passes on the debug build.

Background

  • A registry manifest is the JSON document that a registry serves for a package name. Its versions object maps each version string to the package.json of that version.
  • bun pm view parses the response two times. PackageManifest::parse resolves the requested tag or range, and it accepts an entry that is not an object. A generic JSON parse gives the Expr tree that the command prints.
  • Expr is the AST node type of the JS parser. Expr::set writes one property of an object Expr.
Notes

Repro (loopback registry, release canary 1.4.3-canary.1+c6b7fcb5b, Linux x64):

const packument = { name: "pkg", "dist-tags": { latest: "1.0.0" }, versions: { "1.0.0": "x" } };
const server = Bun.serve({ port: 0, fetch: () => Response.json(packument) });
// bunfig.toml: [install] registry = "http://localhost:<port>/"
// $ bun info pkg
// panic: internal error: entered unreachable code   (SIGABRT, exit 134)

A string, a number, null, true, [] and [{...}] all abort. bun pm view pkg, bun pm view pkg --json, bun pm view pkg version and bun info pkg all abort, because the Expr::set call runs before the output mode is read.

Shape matrix: 240 malformed manifests (each top-level field, each field of the version, dist, dist-tags, dependencies, maintainers, keywords, time, and the whole body, each replaced by 10 values of the wrong type) times 5 argument forms, 1,200 runs.

  • Release canary: 40 runs abort. All 40 are the entry that is not an object.
  • Debug build with this change: 0 of those 40 abort. 55 other runs abort in debug builds only. They are the debug assertions of PackageManifest::parse (src/install/npm.rs:2127 for a version key that is not semver, src/install/npm.rs:2697 for a dependency value that is not a string). Release builds skip both cases, and bun install shares them. install: don't debug_assert on untrusted registry packument version keys #34651 covered them and was closed as stale, not on its merits.

#42052 rewrites view() for npm parity and removes the Expr::set call as a side effect. It is a larger, separate change.

Expr::set has other callers in bun publish. They pass the local package.json after they validate it.

The error goes to stderr as plain text with --json too. The other malformed-response exits in view() (failed to parse response body as JSON, failed to parse package manifest) do the same.

`bun pm view` and `bun info` take the `versions` entry that matches the
requested version and call `Expr::set` on it. `Expr::set` is
`unreachable!()` for a value that is not an object, so a registry
response such as `"versions": {"1.0.0": "x"}` aborted the command with
`panic: internal error: entered unreachable code`.

Check the entry before use. Print an error and exit 1 when it is not an
object.
@coderabbitai

coderabbitai Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Warning

Review paused — included plan limit reached

Keep your review moving with free on-demand reviews.

  • Run this review for free

On-demand reviews are free for the next 3 days.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Promotion and pricing details

On-demand reviews are free for the next 3 days. After that, they cost $0.25 per reviewed file.

Review limit details

Or wait 1 minute for your next included review.

Check out review usage here.

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 2a59beb3-5475-42f9-ba8f-96c82b7f182b

📥 Commits

Reviewing files that changed from the base of the PR and between 630e921 and eed6447.

📒 Files selected for processing (2)
  • src/runtime/cli/pm_view_command.rs
  • test/cli/install/bun-info.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 12:52 AM PT - Sep 17th, 2026

@robobun, your commit eed6447 is building: #116907

@robobun

robobun commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: closed as a duplicate of #43019.

#43019 makes the same check at the same place in view() and was opened three minutes before this PR. The reproduction below applies to both.

// registry.js
const packument = { name: "pkg", "dist-tags": { latest: "1.0.0" }, versions: { "1.0.0": "x" } };
const server = Bun.serve({ port: 0, fetch: () => Response.json(packument) });
console.log(`[install]\nregistry = "http://localhost:${server.port}/"`); // put this in bunfig.toml
$ bun info pkg
panic: internal error: entered unreachable code
(SIGABRT, exit 134)

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — small, self-contained fix that turns a user-reachable unreachable!() panic into a clean error exit.

What was reviewed:

  • Confirmed the crash mechanism: Expr::set (src/ast/expr.rs:496-499) hits unreachable!() on a non-object, and the new check at src/runtime/cli/pm_view_command.rs:227 runs before the manifest.set call at line 299 on every code path.
  • Checked the --json path printing a plain-text stderr error: consistent with the sibling failed to parse response body as JSON / failed to parse package manifest exits in the same function, so not a new inconsistency.
  • Checked the test: hermetic local Bun.serve({ port: 0 }) registry, pipes drained concurrently, combined {stdout, stderr, exitCode} assertion, describe.concurrent; the expected stderr string matches err_generic's error: prefix with <b>/<r> stripped and bun_fmt::quote quoting.
  • Considered whether root-level lookups like bun pm view pkg dist-tags should still succeed on a non-object entry: they previously panicked at manifest.set anyway, so the new exit 1 is strictly an improvement, not a regression.
Extended reasoning...

Overview

The PR touches one function in src/runtime/cli/pm_view_command.rs (the view command backing bun info / bun pm view) and appends six concurrent tests to test/cli/install/bun-info.test.ts. The code change is eight lines: after the loop finds the versions entry matching the resolved semver, it checks that the entry's ExprData is EObject and, if not, prints an error via Output::err_generic and exits with code 1. Otherwise behavior is unchanged.

Security risks

None introduced. The change strictly narrows what a registry response can do: previously a malformed manifest (string/null/array under versions[<v>]) reached Expr::set, which is unreachable!() for non-objects — a panic on network-controlled input. The new check happens before any use of the value. No new allocation, parsing, or path handling is added.

Level of scrutiny

Low. The diff is small, the invariant being enforced (every later step reads fields of the matched entry, and manifest.set at line 299 requires an object) is verifiable by reading the surrounding ~80 lines, and the .expect("infallible: prop has value") it retains is the pre-existing one, relying on the JSON parser producing values for object properties. The error message follows repo voice (error: prefix, bun_fmt::quote, <b>/<r> markup, stderr). The --json mode emitting plain-text stderr matches the two adjacent parse-failure exits in the same function, so this is not a new inconsistency. No changed file is covered by .github/CODEOWNERS.

Other factors

The bug hunt exited on dry_streak with zero findings; the four ruled-out candidates (JSON-mode error shape, root-level property lookups, the retained expect, non-object document bodies) were each examined and do not represent regressions from the pre-PR state. The tests are hermetic (local Bun.serve, port: 0, proxy env cleared), use describe.concurrent, drain pipes with Promise.all, and assert a combined {stdout, stderr, exitCode} object, matching test/CLAUDE.md conventions. I did not run a debug build in this session; the PR author's claim that the tests fail on the unfixed canary is consistent with the unreachable!() I confirmed in Expr::set.

@robobun

robobun commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Closing: this is covered by #43019. It makes the same check at the same place in view(), with the same tests, and it was opened three minutes before this PR.

@robobun robobun closed this Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants