Skip to content

A malformed QUARTO_R crashes rBinaryPath instead of falling back, and the crash re-triggers inside the error reporter #14775

Description

@hvizzolini

I have:

  • searched the issue tracker for similar issues
  • installed the latest version of Quarto CLI
  • formatted my issue following the Bug Reports guide

Bug description

rBinaryPath() guards the QUARTO_R branch with existsSync(), but guards the R_HOME branch five lines later with safeExistsSync(). On Windows, Deno.statSync() throws ERROR_INVALID_NAME (os error 123) for a path the filesystem rejects outright — a control character in the string is enough — so existsSync() never returns false for such a value. It throws, and the whole render dies.

That defeats the function's own design. The else branch right there is:

warnOnce(`Specified QUARTO_R '${quartoR}' does not exist.`);

so the intent is clearly to warn and continue down the chain (QUARTO_RR_HOMEPATH → Windows Registry → PROGRAMFILES). For a malformed value that branch is unreachable.

There is a second, worse consequence. rBinaryPath is reached from the diagnostics path:

callR → printCallRDiagnostics → checkRBinary → rBinaryPath → existsSync → statSync → throw

So when callR fails for any reason, Quarto tries to explain why, and the explainer throws the same exception. The user never sees the underlying R error — only a stat failure on a path they never typed. This is what makes the bug expensive to diagnose: the visible message points nowhere near the cause.

Steps to reproduce

No R-IDE involvement needed — any invalid-name value reproduces it. On Windows:

$env:QUARTO_R = "C:\Program Files\R\R-4.6.1" + [char]8 + "ind"   # 0x08 = backspace
'---
title: repro
format: html
---

```{r}
1 + 1
```' | Set-Content repro.qmd

quarto render repro.qmd

Actual behavior

Actual behaviour

ERROR: The filename, directory name, or volume label syntax is incorrect.
(os error 123): stat 'C:\Program Files\R\R-4.6.ind'
ERROR: The filename, directory name, or volume label syntax is incorrect.
(os error 123): stat 'C:\Program Files\R\R-4.6.ind'

Stack trace:
    at Object.statSync (ext:deno_fs/30_fs.js:432:3)
    at existsSync2 (file:///C:/Users/.../Quarto/bin/quarto.js:2769:23)
    at rBinaryPath (file:///C:/Users/.../Quarto/bin/quarto.js:26710:9)
    at checkRBinary (file:///C:/Users/.../Quarto/bin/quarto.js:27673:22)
    at printCallRDiagnostics (file:///C:/Users/.../Quarto/bin/quarto.js:27916:22)
    at callR (file:///C:/Users/.../Quarto/bin/quarto.js:27900:13)
    at async Object.execute (file:///C:/Users/.../Quarto/bin/quarto.js:28203:29)
    at async renderExecute (file:///C:/Users/.../Quarto/bin/quarto.js:136835:25)
    at async renderFileInternal (file:///C:/Users/.../Quarto/bin/quarto.js:137094:35)
    at async renderFiles (file:///C:/Users/.../Quarto/bin/quarto.js:136884:9)

Two notes on that output. The path renders as R-4.6.ind or R-4.6.1ind depending on whether your terminal applies the backspace when printing — the underlying string is the same 30 bytes either way, and od -c shows the \b. And the message text above is the English equivalent; my system locale is Spanish, so I see The filename, directory name, or volume label syntax is incorrect..

Expected behavior

warnOnce("Specified QUARTO_R '…' does not exist."), then continue down the fallback chain and render normally. R is installed and discoverable here — with QUARTO_R unset, the same document renders and quarto check knitr passes:

Checking R installation...........OK
      Version: 4.6.1
      knitr: 1.51
      rmarkdown: 2.31
Checking Knitr engine render......OK

Confirming it is only the guard: rendering with QUARTO_R= (empty, so the branch is skipped as falsy) succeeds with the malformed value still exported in the environment.

Suggested fix
In src/core/resources.ts, rBinaryPath() — use the safe variant that the neighbouring branch already uses:

   if (quartoR) {
-    if (existsSync(quartoR)) {
+    if (safeExistsSync(quartoR)) {
       const rBinDir = Deno.statSync(quartoR).isDirectory ? quartoR : dirname(quartoR);

Worth noting that the Deno.statSync(quartoR) on the next line is also unguarded. It is unreachable once the guard is safe, but a TOCTOU-safe version would fold both into one try.

A follow-up worth considering separately: printCallRDiagnostics() re-entering the same discovery code means a throw anywhere in it masks the error it was called to report. Wrapping the diagnostics body in a try/catch would keep the original callR failure visible even if diagnostics fail.

How this shows up in practice

The malformed value is not something a user types. Positron's positron.positron-r extension registers an environment variable collection:

{ action: "replace", variable: "QUARTO_R", value: path.dirname(scriptpath) }

which on Windows is e.g. C:\Program Files\R\R-4.6.1\bin\x64. Somewhere in the collection's application that string is escape-processed once too many times: \b becomes U+0008 and \x64 becomes d, yielding C:\Program Files\R\R-4.6.1<BS>ind. That is arguably a Positron bug and I am reporting it there too — but Quarto is in a position to degrade gracefully, and the guard asymmetry means it currently cannot.

Note that any Windows R installation path trips this once the value is escape-processed, because the \bin component alone yields a control character. It is not specific to the x64 subdirectory or to one R version.

Because Positron re-applies the collection after shell profiles run, a plain export QUARTO_R=… in .bashrc or a PowerShell profile does not help. The workaround that does is a shell function that blanks the variable per invocation:

quarto() { QUARTO_R= command quarto "$@"; }

That only covers invocations that go through a shell, which is why the one-line change above matters.

Your environment

Quarto 1.10.18
Windows 11 Pro, 10.0.26200
R 4.6.1, knitr 1.51, rmarkdown 2.31
Positron 2026.09.0 (source of the malformed value; not required to reproduce)
Line numbers above are from the bundled quarto.js shipped with 1.10.18. Source-file attribution comes from the bundle's own origin comments: rBinaryPath under // core/resources.ts, checkRBinary under // core/knitr.ts, callR/printCallRDiagnostics under // execute/rmd.ts.

Quarto check output

Quarto 1.10.18
[>] Checking environment information...

[>] Checking versions of quarto binary dependencies...
Pandoc version 3.10.0: OK
Dart Sass version 1.101.0: OK
Deno version 2.7.14: OK
Typst version 0.15.1: OK
[>] Checking versions of quarto dependencies......OK
[>] Checking Quarto installation......OK
Version: 1.10.18

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions