I have:
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_R → R_HOME → PATH → 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
I have:
Bug description
rBinaryPath()guards theQUARTO_Rbranch withexistsSync(), but guards theR_HOMEbranch five lines later withsafeExistsSync(). On Windows,Deno.statSync()throwsERROR_INVALID_NAME(os error 123) for a path the filesystem rejects outright — a control character in the string is enough — soexistsSync()never returnsfalsefor such a value. It throws, and the whole render dies.That defeats the function's own design. The
elsebranch right there is:so the intent is clearly to warn and continue down the chain (
QUARTO_R→R_HOME→PATH→ Windows Registry →PROGRAMFILES). For a malformed value that branch is unreachable.There is a second, worse consequence.
rBinaryPathis reached from the diagnostics path:So when
callRfails for any reason, Quarto tries to explain why, and the explainer throws the same exception. The user never sees the underlying R error — only astatfailure 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:
Actual behavior
Actual behaviour
Two notes on that output. The path renders as
R-4.6.indorR-4.6.1inddepending on whether your terminal applies the backspace when printing — the underlying string is the same 30 bytes either way, andod -cshows the\b. And the message text above is the English equivalent; my system locale is Spanish, so I seeThe 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 — withQUARTO_Runset, the same document renders andquarto check knitrpasses: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: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 onetry.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 atry/catchwould keep the originalcallRfailure 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:
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:\bbecomes U+0008 and\x64becomesd, yieldingC:\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
\bincomponent alone yields a control character. It is not specific to thex64subdirectory or to one R version.Because Positron re-applies the collection after shell profiles run, a plain
export QUARTO_R=…in.bashrcor a PowerShell profile does not help. The workaround that does is a shell function that blanks the variable per invocation: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.jsshipped with 1.10.18. Source-file attribution comes from the bundle's own origin comments:rBinaryPathunder// core/resources.ts,checkRBinaryunder// core/knitr.ts,callR/printCallRDiagnosticsunder //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