Skip to content

fix: parse dvm's key=value format in .dvmrc - #133

Open
somaz94 wants to merge 2 commits into
denoland:mainfrom
somaz94:fix/dvmrc-key-value
Open

somaz94 wants to merge 2 commits into
denoland:mainfrom
somaz94:fix/dvmrc-key-value

Conversation

@somaz94

@somaz94 somaz94 commented Aug 4, 2026

Copy link
Copy Markdown

The README documents that deno-version-file can read dvm's .dvmrc, but the parser only accepts a bare version string. dvm writes the file as key=value (configrc.rs serializes with format!("{}={}", k, v)), so deno_version=1.43.1 falls through to the bare-version branch and fails with The passed version range is not valid.

.dvmrc is a general key/value store rather than a single line, and dvm also records registry_binary / registry_version, so the version is not necessarily on the first line. The new pattern scans every line for the deno_version key. It runs only after the existing .tool-versions match, and the two patterns are disjoint, so bare-version and .tool-versions files behave exactly as before.

Also tolerates spaces around = (dvm's own reader trims both sides) and a v prefix, consistent with the existing .tool-versions handling.

Validation:

  • deno lint, deno fmt --check, deno check src/main.ts all pass locally
  • deno run -A scripts/build.ts rebuilt dist/, and a second build is byte-identical, so build-diff stays clean
  • New test-version-file-dvm job writes a dvm-style .dvmrc with deno_version on the second line, so it covers the multi-key case rather than just a prefix strip
  • Exercised the parser locally over 13 cases: dvm single-key / multi-key in both orders / spaces around = / v prefix / CRLF, plus regressions for bare version, bare range (~1.32), .tool-versions, and a my_deno_versionx= string that must not match

related: #121

@somaz94

somaz94 commented Aug 4, 2026

Copy link
Copy Markdown
Author

Heads up on the red CI, both failures are pre-existing on main and not from this change.

lint fails at deno check src/main.ts because canary now declares ImportMeta.dirname/filename as string, which conflicts with the optional re-declaration at src/main.ts:13-14. I reproduced the same 4 errors on an unmodified main checkout with the same canary build.

test (ubuntu-latest, 4e8b2f46...) fails with a 404 from dl.deno.land for that pinned canary hash, so the artifact looks expired.

The jobs this PR affects are green: build-diff, test-version-file (.dvmrc and .tool-versions), and the new test-version-file-dvm.

Happy to fix either one in a separate PR if that helps.

@somaz94
somaz94 marked this pull request as ready for review August 4, 2026 02:43
somaz94 added a commit to somaz94/somaz94 that referenced this pull request Aug 4, 2026
@somaz94

somaz94 commented Aug 5, 2026

Copy link
Copy Markdown
Author

The red CI here is pre-existing and unrelated to this PR — I reproduced it on unmodified main. Details, since the check summary is misleading:

Only one check actually failed: lint. The other 24 reds are test (...) matrix legs that were cancelled by fail-fast, and GitHub reports cancelled as failed. (Spot-check: job 91876092624, conclusion: cancelled, log body is just ##[error]The operation was canceled. — the 4–9s durations are the tell.)

The lint failure reproduces on main with no changes at all:

$ git clone https://github.com/denoland/setup-deno && cd setup-deno   # 22d081f, "2.0.5 (#131)"
$ deno check src/main.ts
TS2687 [ERROR]: All declarations of 'dirname' must have identical modifiers.
TS2717 [ERROR]: Property 'dirname' must be of type 'string', but here has type 'string | undefined'.
TS2687 [ERROR]: All declarations of 'filename' must have identical modifiers.
TS2717 [ERROR]: Property 'filename' must be of type 'string', but here has type 'string | undefined'.
Found 4 errors.

The cause is the ImportMeta augmentation at src/main.ts:11-16:

declare global {
  interface ImportMeta {
    dirname?: string;
    filename?: string;
  }
}

Deno now ships import.meta.dirname/filename natively as required string, so declaring them again as optional collides on both the modifier and the type. Deleting that block makes deno check src/main.ts pass clean; import.meta.dirname at src/main.ts:46 then resolves to the built-in type. The lint job pins deno-version: canary, so this started failing on its own — the last green run on main was 2026-07-03 and mine is the first run since.

I've deliberately left that out of this PR since it's unrelated to .dvmrc parsing — happy to either fold it in here or send it as a separate PR, whichever you prefer.

For what it's worth, this PR's own tests do pass: test-version-file-dvm, test-version-file (.dvmrc), test-version-file (.tool-versions), and build-diff.

@LitoMore

Copy link
Copy Markdown

I’m concerned that this change allows an attacker to specify an attacker-controlled URL in the .dvmrc file, potentially causing CI to download and execute an untrusted binary.

A version file is repo content, so it must not be able to choose what gets
downloaded. Neither dvm's registry_binary key nor a URL in deno_version is
accepted as a version: both fail parseVersionRange before any download.
Cover both with a job that expects the action to fail.

Signed-off-by: somaz <genius5711@gmail.com>
@somaz94

somaz94 commented Sep 18, 2026 •

Copy link
Copy Markdown
Author

Good question. I traced it, and the file contents can't reach a download URL.

getDenoVersionFromFile only extracts a version string, and that string has to survive parseVersionRange, which accepts exactly four literals (canary, rc, latest, lts), a 40-hex git hash, or a valid semver range. Anything else returns null and main exits with "The passed version range is not valid." The download host is hardcoded in every branch of install(), and for a stable range the version that ends up in the URL is chosen by semver.maxSatisfying from dl.deno.land/versions.json, not taken from the file.

Two things specific to this PR:

  • dvm's registry_binary key is deliberately not read: the regex matches deno_version only. Before this PR a dvm-style .dvmrc fell through to contents.trim(), so the whole registry_binary=... line became the "version". This narrows what's accepted rather than widening it.
  • A URL in deno_version is simply an invalid version and fails the same gate.

Checked against the real functions:

registry key only     parsed="registry_binary=https://example.invalid"  range=null
URL as deno_version   parsed="https://example.invalid/deno.zip"         range=null
path traversal        parsed="../../../evil"                            range=null
both keys             parsed="1.43.1"                                   range={"range":"1.43.1","kind":"stable"}

I've added a test-version-file-rejects-url job asserting both hostile files fail the action.

For completeness: the one path where file-derived bytes do reach a URL is canary with a 40-char git hash (dl.deno.land/canary/<hash>/), constrained to [0-9a-fA-F]{40} on the hardcoded host. That predates this PR and is the same for the deno-version input.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants