Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Windows: Program a, a.<ext> should not resolve to current working directory #164

Closed
lread opened this issue Jul 22, 2024 · 3 comments · Fixed by #165
Closed

Windows: Program a, a.<ext> should not resolve to current working directory #164

lread opened this issue Jul 22, 2024 · 3 comments · Fixed by #165

Comments

@lread
Copy link
Contributor

lread commented Jul 22, 2024

Setup

On Windows, using clj-msi, from a CMD shell, from an empty dir, create a.bat with the following content in the current directory:

@echo off
echo hello

then launch a REPL:

> clj -Sdeps "{:deps {babashka/process {:mvn/version \"0.5.22\"}}}"
Clojure 1.11.3
user=> (require '[babashka.process :as p] '[babashka.fs :as fs])
nil

Base Repro

user=> (-> (p/shell {:out :string} "a.bat") :out)
"hello \r\n"

Expected Behaviour

Should throw an exception for file not found.

Unless the current working directory is on the PATH, invoking a.bat should require a path prefix, for example:

user=> (-> (p/shell {:out :string} ".\\a.bat") :out)
"hello \r\n"

Note that invoking a for this scenario throws as expected:

user=> (-> (p/shell {:out :string} "a") :out)
Execution error (IOException) at java.lang.ProcessImpl/create (ProcessImpl.java:-2).
CreateProcess error=2, The system cannot find the file specified

Repro Twist

If we add a.exe to the current working directory.

a.go source:

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

Built with:

GOOS=windows GOARCH=amd64 go build -o a.exe a.go

As with a.bat, a.exe errantly resolves:

user=> (-> (p/shell {:out :string} "a.exe") :out)
"Hello, World!\n"

But the twist is that a no longer throws and resolves to a.exe:

user=> (-> (p/shell {:out :string} "a") :out)
"Hello, World!\n"

Next Steps

I'll handle this with #163

@borkdude
Copy link
Contributor

How is the behavior of cmd.exe and powershell in this regard? If they do pick the cwd binary I'd say let's not break this behavior maybe since this may cause issues for existing users?

@lread
Copy link
Contributor Author

lread commented Jul 23, 2024

Thanks for the comment and concern, yes, let's explore it!

  • Cmd Shell - resolves from the cwd, then the PATH.
  • PowerShell - resolves from the PATH
  • ProcessBuilder - resolves from the cwd then the PATH, but a only resolves to a.exe (and never a.com, a.bat or a.cmd)

So why did Microsoft make this change in PowerShell?

TLDR: Security

If I try to run a from PowerShell, I get a somewhat helpful error:

a : The term 'a' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:1 char:1
+ a
+ ~
    + CategoryInfo          : ObjectNotFound: (a:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException


Suggestion [3,General]: The command a was not found, but does exist in the current location. Windows PowerShell does not load commands from the current location by default. If you trust this command, instead type: ".\a". See "get-help about_Command_Precedence" for more details.

Interesting part:

If you trust this command, instead type: ".\a". See "get-help about_Command_Precedence" for more details

If I run the suggested get-help about_Command_Precedence in the text is:

As a security feature, PowerShell doesn't run executable commands, including PowerShell
scripts and native commands, unless the command is located in a path listed
in the $env:Path environment variable.

So why does ProcessBuilder behave the way it does on Windows?

I have not read the JDK source, but I it is an abstraction over CreateProcess which is lower level.

Perhaps relevant:

  • ProcessBuilder was added to Java in v1.5 which was in 2004.
  • PowerShell was released in 2006.

What should babashka/process do on Windows?

It attempts to compensate for executable extension resolution (.i.e., it will resolve a to a.com, a.exe, a.bat, a.cmd instead of just a.exe).
I think this is a good thing to continue to do.

But for directory resolution, here are the options I currently see:

Option 1: Mimic CMD Shell and ProcessBuilder behaviour

Pro: consistent with CMD Shell
Con: could be considered less secure
Con: cross-platform behaviour is not consistent

Option 2: Mimic PowerShell (and macOS/Linux) behaviour

Pro: could be considered more secure
Pro: consistent behaviour across all platforms
Con: might surprise some users expecting CMD shell/ProcessBuilder-type behaviour

Proposal

Since babashka process does not handle resolving from the cwd consistently today and would require fixes to do so, and for the pros listed above, I would recommend Option 2.

If a user really wants a CMD Shell-type resolution, they can always provide their own :program-resolver.

This proposal includes clarifications of program resolution behaviour on Windows in README.

Whaddya think?

@borkdude
Copy link
Contributor

OK, option 2 it is!

lread added a commit to lread/process that referenced this issue Jul 25, 2024
After [some careful study](https://github.com/lread/info-process-builder/blob/main/README.adoc#program-resolution),
I now understand the underlying behaviour of ProcessBuilder on Windows.

It has some nuances:
- it resolves from the current dir before PATH (which CMD Shell does too -
but PowerShell opted not to do for security reasons)
- it only resolves `a` to `a.exe`
- and has what seem to be bugs wrt to resolving when a `directory` working dir
is specified

These oddities were sometimes leaking through to babashka process when
run from Windows.

We now entirely take over the program resolution for Windows in our default
`:program-resolver` which now matches the behaviour of PowerShell (with
the exception of launching `.ps1` files, we do not do that), macOS and
Linux.

Note that the default `:program-resolver` is also employed for `exec` on
all OSes. Because it matches macOS/Linux behaviour this does not
present any problems.

Tests now thoroughly exercise program resolution via explicitly
constructed scenarios instead of awkwardly relying on what happens to
be on the PATH. See new test-resources dir README for some details.

Bb tasks adjusted accordingly to setup the `PATH` for tests.
New `dev:bb` and `dev:jvm` tasks added to launch nREPLs with this same setup.

README is updated with new section on Program Resolution.

Closes babashka#163
Closes babashka#164
lread added a commit to lread/babashka that referenced this issue Jul 25, 2024
Default `BABASHKA_TEST_ENV` to `jvm` for libtests
CI already understands this need, but defaulting in-script avoids
having devs to understand/remember it too.

Alter PATH as required for new babashka.process tests.

In support of babashka/process#163, babashka/process#164
borkdude pushed a commit to babashka/babashka that referenced this issue Jul 26, 2024
Default `BABASHKA_TEST_ENV` to `jvm` for libtests
CI already understands this need, but defaulting in-script avoids
having devs to understand/remember it too.

Alter PATH as required for new babashka.process tests.

In support of babashka/process#163, babashka/process#164
borkdude added a commit that referenced this issue Jul 26, 2024

Co-authored-by: Michiel Borkent <michielborkent@gmail.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 a pull request may close this issue.

2 participants