Skip to content

Fix env-var "a" collision in R front-end shell wrapper by avoiding -e in kernel launch - #756

Open
ajalon1 wants to merge 4 commits into
IRkernel:masterfrom
ajalon1:fix/env-var-a-collision-in-kernel-launch
Open

Fix env-var "a" collision in R front-end shell wrapper by avoiding -e in kernel launch#756
ajalon1 wants to merge 4 commits into
IRkernel:masterfrom
ajalon1:fix/env-var-a-collision-in-kernel-launch

Conversation

@ajalon1

@ajalon1 ajalon1 commented Aug 11, 2026

Copy link
Copy Markdown

Fixes #755.

Problem

On Unix-likes, R's own front-end ($(R RHOME)/bin/R) is a POSIX shell script. Its -e/-f/--file= argument handling reuses a plain, unexported local shell variable literally named a to hold the encoded expression text before it execs into the real R binary:

-e)
  a=`(echo "${2}" && echo) | sed -e 's/ /~+~/g' | ...`
  args="${args} -e $a"
  ;;
...
exec "${R_binary}" ${args} "${@}"

Because POSIX shells retain a variable's exported attribute once it's inherited from the environment, if the shell was launched with an env var already named a (e.g. a user-set container/notebook env var), this plain reassignment silently overwrites it with the encoded -e text — and the clobbered value is inherited by the real R process via the trailing exec, for the entire lifetime of that R session.

IRkernel's generated kernelspec launches R via ["R", "--slave", "-e", "IRkernel::main()", "--args", "{connection_file}"] — exactly this vulnerable code path. So any Jupyter user with an environment variable literally named a gets it permanently corrupted the moment their kernel starts: Sys.getenv("a") returns "IRkernel::main()" instead of the real value. Renaming to A, b, etc. is unaffected. See #755 for the full root-cause writeup and repro.

Fix

On Unix-likes, installspec() now generates a kernelspec that launches via a small sh -c wrapper piping IRkernel::main() into R's stdin, instead of passing it via -e:

echo 'IRkernel::main()' | exec /path/to/R --slave --args "$0"

This sidesteps the -e/-f/--file= code path in R's front-end entirely, so the a-variable collision never happens. Windows is untouched — R's front-end there is a compiled executable rather than a shell script, so it isn't subject to this bug, and there's no sh on a plain Windows R install to run the wrapper anyway.

Testing

Verified locally against a real R install (Alpine R=4.6.0-r0, though the front-end script logic is version/platform-independent for Unix-likes):

$ docker run --rm -e a=a -e A=A -e b=b r-base:latest \
    R --slave -e 'print(Sys.getenv("a")); print(Sys.getenv("A")); print(Sys.getenv("b"))' --args /dev/null
[1] "print(Sys.getenv(\"a\"));~+~print(Sys.getenv(\"A\"));~+~print(Sys.getenv(\"b\"))"   # <- bug
[1] "A"
[1] "b"

$ docker run --rm -e a=a -e A=A -e b=b r-base:latest sh -c \
    'echo "cat(Sys.getenv(\"a\"),Sys.getenv(\"A\"),Sys.getenv(\"b\"))" | exec R --slave --args /dev/null'
a A b   # <- fixed

Also confirmed the exact sh -c string this patch's sprintf()/shQuote() call generates for a real installspec() call, and ran it end-to-end with the same env vars set, with the same passing result.

I'm aware this project's had a quiet stretch, looks like three years or so. I'm happy to adjust the approach if maintainers have a preferred direction, or if there's a reason this hasn't been a priority -- already have a workaround in our own platform. But, I wanted to file this as a self-contained, backward-compatible fix in case it's useful.

🤖 PR description generated with Claude Code

ajalon1 and others added 4 commits August 11, 2026 09:45
…launch

On Unix-likes, R's own front-end (`$(R RHOME)/bin/R`) is a POSIX shell
script whose `-e`/`-f`/`--file=` argument handling reuses a plain,
unexported local shell variable literally named `a` to hold the encoded
expression text before it execs into the real R binary. Since POSIX
shells retain a variable's exported attribute once inherited from the
environment, this silently clobbers a pre-existing exported environment
variable named `a` for the entire lifetime of the R process.

IRkernel's generated kernelspec launches R via `-e "IRkernel::main()"`,
so any Jupyter user with an environment variable literally named `a`
had it permanently corrupted at kernel startup (e.g. Sys.getenv("a")
would return "IRkernel::main()" instead of the real value).

Fix: on Unix-likes, installspec() now generates a kernelspec that
launches via a small `sh -c` wrapper piping the startup expression into
R's stdin, instead of passing it via `-e`, which sidesteps the
vulnerable code path entirely. Windows is unaffected (R's front-end
there is a compiled executable, not a shell script) and keeps its
original launch mechanism.

Fixes IRkernel#755

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

Sys.getenv("a") returns the kernel's own -e argument text instead of the env var's value (R front-end wrapper "a" variable collision)

1 participant