Black Lives Matter. Support the Equal Justice Initiative.

The Go Blog

Contexts and structs

Jean de Klerk, Matt T. Proud
24 February 2021

Introduction

In many Go APIs, especially modern ones, the first argument to functions and methods is often context.Context. Context provides a means of transmitting deadlines, caller cancellations, and other request-scoped values across API boundaries and between processes. It is often used when a library interacts — directly or transitively — with remote servers, such as databases, APIs, and the like.

The documentation for context states:

Contexts should not be stored inside a struct type, but instead passed to each function that needs it.

This article expands on that advice with reasons and examples describing why it's important to pass Context rather than store it in another type. It also highlights a rare case where storing Context in a struct type may make sense, and how to do so safely.

Prefer contexts passed as arguments

To understand the advice to not store context in structs, let's consider the preferred context-as-argument approach:

type Worker struct { /* … */ }

type Work struct { /* … */ }

func New() *Worker {
  return &Worker{}
}

func (w *Worker) Fetch(ctx context.Context) (*Work, error) {
  _ = ctx // A per-call ctx is used for cancellation, deadlines, and metadata.
}

func (w *Worker) Process(ctx context.Context, w *Work) error {
  _ = ctx // A per-call ctx is used for cancellation, deadlines, and metadata.
}

Here, the (*Worker).Fetch and (*Worker).Process methods both accept a context directly. With this pass-as-argument design, users can set per-call deadlines, cancellation, and metadata. And, it's clear how the context.Context passed to each method will be used: there's no expectation that a context.Context passed to one method will be used by any other method. This is because the context is scoped to as small an operation as it needs to be, which greatly increases the utility and clarity of context in this package.

Storing context in structs leads to confusion

Let's inspect again the Worker example above with the disfavored context-in-struct approach. The problem with it is that when you store the context in a struct, you obscure lifetime to the callers, or worse intermingle two scopes together in unpredictable ways:

type Worker struct {
  ctx context.Context
}

func New(ctx context.Context) *Worker {
  return &Worker{ctx: ctx}
}

func (w *Worker) Fetch() (*Work, error) {
  _ = w.ctx // A shared w.ctx is used for cancellation, deadlines, and metadata.
}

func (w *Worker) Process(w *Work) error {
  _ = w.ctx // A shared w.ctx is used for cancellation, deadlines, and metadata.
}

The (*Worker).Fetch and (*Worker).Process method both use a context stored in Worker. This prevents the callers of Fetch and Process (which may themselves have different contexts) from specifying a deadline, requesting cancellation, and attaching metadata on a per-call basis. For example: the user is unable to provide a deadline just for (*Worker).Fetch, or cancel just the (*Worker).Process call. The caller's lifetime is intermingled with a shared context, and the context is scoped to the lifetime where the Worker is created.

The API is also much more confusing to users compared to the pass-as-argument approach. Users might ask themselves:

  • Since New takes a context.Context, is the constructor doing work that needs cancelation or deadlines?
  • Does the context.Context passed in to New apply to work in (*Worker).Fetch and (*Worker).Process? Neither? One but not the other?

The API would need a good deal of documentation to explicitly tell the user exactly what the context.Context is used for. The user might also have to read code rather than being able to rely on the structure of the API conveys.

And, finally, it can be quite dangerous to design a production-grade server whose requests don't each have a context and thus can't adequately honor cancellation. Without the ability to set per-call deadlines, your process could backlog and exhaust its resources (like memory)!

Exception to the rule: preserving backwards compatibility

When Go 1.7 — which introduced context.Context — was released, a large number of APIs had to add context support in backwards compatible ways. For example, net/http's Client methods, like Get and Do, were excellent candidates for context. Each external request sent with these methods would benefit from having the deadline, cancellation, and metadata support that came with context.Context.

There are two approaches for adding support for context.Context in backwards compatible ways: including a context in a struct, as we'll see in a moment, and duplicating functions, with duplicates accepting context.Context and having Context as their function name suffix. The duplicate approach should be preferred over the context-in-struct, and is further discussed in Keeping your modules compatible. However, in some cases it's impractical: for example, if your API exposes a large number of functions, then duplicating them all might be infeasible.

The net/http package chose the context-in-struct approach, which provides a useful case study. Let's look at net/http's Do. Prior to the introduction of context.Context, Do was defined as follows:

func (c *Client) Do(req *Request) (*Response, error)

After Go 1.7, Do might have looked like the following, if not for the fact that it would break backwards compatibility:

func (c *Client) Do(ctx context.Context, req *Request) (*Response, error)

But, preserving the backwards compatibility and adhering to the Go 1 promise of compatibility is crucial for the standard library. So, instead, the maintainers chose to add a context.Context on the http.Request struct in order to allow support context.Context without breaking backwards compatibility:

type Request struct {
  ctx context.Context

  // ...
}

func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
  // Simplified for brevity of this article.
  return &Request{
    ctx: ctx,
    // ...
  }
}

func (c *Client) Do(req *Request) (*Response, error)

When retrofitting your API to support context, it may make sense to add a context.Context to a struct, as above. However, remember to first consider duplicating your functions, which allows retrofitting context.Context in a backwards compatibility without sacrificing utility and comprehension. For example:

func (c *Client) Call() error {
  return c.CallContext(context.Background())
}

func (c *Client) CallContext(ctx context.Context) error {
  // ...
}

Conclusion

Context makes it easy to propagate important cross-library and cross-API information down a calling stack. But, it must be used consistently and clearly in order to remain comprehensible, easy to debug, and effective.

When passed as the first argument in a method rather than stored in a struct type, users can take full advantage of its extensibility in order to build a powerful tree of cancelation, deadline, and metadata information through the call stack. And, best of all, its scope is clearly understood when it's passed in as an argument, leading to clear comprehension and debuggability up and down the stack.

When designing an API with context, remember the advice: pass context.Context in as an argument; don't store it in structs.

New module changes in Go 1.16

Jay Conrod
18 February 2021

We hope you're enjoying Go 1.16! This release has a lot of new features, especially for modules. The release notes describe these changes briefly, but let's explore a few of them in depth.

Modules on by default

The go command now builds packages in module-aware mode by default, even when no go.mod is present. This is a big step toward using modules in all projects.

It's still possible to build packages in GOPATH mode by setting the GO111MODULE environment variable to off. You can also set GO111MODULE to auto to enable module-aware mode only when a go.mod file is present in the current directory or any parent directory. This was previously the default. Note that you can set GO111MODULE and other variables permanently with go env -w:

go env -w GO111MODULE=auto

We plan to drop support for GOPATH mode in Go 1.17. In other words, Go 1.17 will ignore GO111MODULE. If you have projects that do not build in module-aware mode, now is the time to migrate. If there is a problem preventing you from migrating, please consider filing an issue or an experience report.

No automatic changes to go.mod and go.sum

Previously, when the go command found a problem with go.mod or go.sum like a missing require directive or a missing sum, it would attempt to fix the problem automatically. We received a lot of feedback that this behavior was surprising, especially for commands like go list that don't normally have side effects. The automatic fixes weren't always desirable: if an imported package wasn't provided by any required module, the go command would add a new dependency, possibly triggering upgrades of common dependencies. Even a misspelled import path would result in a (failed) network lookup.

In Go 1.16, module-aware commands report an error after discovering a problem in go.mod or go.sum instead of attempting to fix the problem automatically. In most cases, the error message recommends a command to fix the problem.

$ go build
example.go:3:8: no required module provides package golang.org/x/net/html; to add it:
    go get golang.org/x/net/html
$ go get golang.org/x/net/html
$ go build

As before, the go command may use the vendor directory if it's present (see Vendoring for details). Commands like go get and go mod tidy still modify go.mod and go.sum, since their main purpose is to manage dependencies.

Installing an executable at a specific version

The go install command can now install an executable at a specific version by specifying an @version suffix.

go install golang.org/x/tools/gopls@v0.6.5

When using this syntax, go install installs the command from that exact module version, ignoring any go.mod files in the current directory and parent directories. (Without the @version suffix, go install continues to operate as it always has, building the program using the version requirements and replacements listed in the current module’s go.mod.)

We used to recommend go get -u program to install an executable, but this use caused too much confusion with the meaning of go get for adding or changing module version requirements in go.mod. And to avoid accidentally modifying go.mod, people started suggesting more complex commands like:

cd $HOME; GO111MODULE=on go get program@latest

Now we can all use go install program@latest instead. See go install for details.

In order to eliminate ambiguity about which versions are used, there are several restrictions on what directives may be present in the program's go.mod file when using this install syntax. In particular, replace and exclude directives are not allowed, at least for now. In the long term, once the new go install program@version is working well for enough use cases, we plan to make go get stop installing command binaries. See issue 43684 for details.

Module retraction

Have you ever accidentally published a module version before it was ready? Or have you discovered a problem right after a version was published that needed to be fixed quickly? Mistakes in published versions are difficult to correct. To keep module builds deterministic, a version cannot be modified after it is published. Even if you delete or change a version tag, proxy.golang.org and other proxies probably already have the original cached.

Module authors can now retract module versions using the retract directive in go.mod. A retracted version still exists and can be downloaded (so builds that depend on it won't break), but the go command won’t select it automatically when resolving versions like @latest. go get and go list -m -u will print warnings about existing uses.

For example, suppose the author of a popular library example.com/lib releases v1.0.5, then discovers a new security issue. They can add a directive to their go.mod file like the one below:

// Remote-triggered crash in package foo. See CVE-2021-01234.
retract v1.0.5

Next, the author can tag and push version v1.0.6, the new highest version. After this, users that already depend on v1.0.5 will be notified of the retraction when they check for updates or when they upgrade a dependent package. The notification message may include text from the comment above the retract directive.

$ go list -m -u all
example.com/lib v1.0.0 (retracted)
$ go get .
go: warning: example.com/lib@v1.0.5: retracted by module author:
    Remote-triggered crash in package foo. See CVE-2021-01234.
go: to switch to the latest unretracted version, run:
    go get example.com/lib@latest

For an interactive, browser-based guide, check out Retract Module Versions on play-with-go.dev. See the retract directive docs for syntax details.

Controlling version control tools with GOVCS

The go command can download module source code from a mirror like proxy.golang.org or directly from a version control repository using git, hg, svn, bzr, or fossil. Direct version control access is important, especially for private modules that aren't available on proxies, but it's also potentially a security problem: a bug in a version control tool may be exploited by a malicious server to run unintended code.

Go 1.16 introduces a new configuration variable, GOVCS, which lets the user specify which modules are allowed to use specific version control tools. GOVCS accepts a comma-separated list of pattern:vcslist rules. The pattern is a path.Match pattern matching one or more leading elements of a module path. The special patterns public and private match public and private modules (private is defined as modules matched by patterns in GOPRIVATE; public is everything else). The vcslist is a pipe-separated list of allowed version control commands or the keyword all or off.

For example:

GOVCS=github.com:git,evil.com:off,*:git|hg

With this setting, modules with paths on github.com can be downloaded using git; paths on evil.com cannot be downloaded using any version control command, and all other paths (* matches everything) can be downloaded using git or hg.

If GOVCS is not set, or if a module does not match any pattern, the go command uses this default: git and hg are allowed for public modules, and all tools are allowed for private modules. The rationale behind allowing only Git and Mercurial is that these two systems have had the most attention to issues of being run as clients of untrusted servers. In contrast, Bazaar, Fossil, and Subversion have primarily been used in trusted, authenticated environments and are not as well scrutinized as attack surfaces. That is, the default setting is:

GOVCS=public:git|hg,private:all

See Controlling version control tools with GOVCS for more details.

What's next?

We hope you find these features useful. We're already hard at work on the next set of module features for Go 1.17, particularly lazy module loading, which should make the module loading process faster and more stable. As always, if you run into new bugs, please let us know on the issue tracker. Happy coding!

Go 1.16 is released

Matt Pearring and Dmitri Shuralyov
16 February 2021

Today the Go team is very happy to announce the release of Go 1.16. You can get it from the download page.

The new embed package provides access to files embedded at compile time using the new //go:embed directive. Now it is easy to bundle supporting data files into your Go programs, making developing with Go even smoother. You can get started using the embed package documentation. Carl Johnson has also written a nice tutorial, “How to use Go embed”.

Go 1.16 also adds macOS ARM64 support (also known as Apple silicon). Since Apple’s announcement of their new arm64 architecture, we have been working closely with them to ensure Go is fully supported; see our blog post “Go on ARM and Beyond” for more.

Note that Go 1.16 requires use of Go modules by default, now that, according to our 2020 Go Developer Survey, 96% of Go developers have made the switch. We recently added official documentation for developing and publishing modules.

Finally, there are many other improvements and bug fixes, including builds that are up to 25% faster and use as much as 15% less memory. For the complete list of changes and more information about the improvements above, see the Go 1.16 release notes.

We want to thank everyone who contributed to this release by writing code filing bugs, providing feedback, and testing the beta and release candidate.

Your contributions and diligence helped to ensure that Go 1.16 is as stable as possible. That said, if you notice any problems, please file an issue.

We hope you enjoy the new release!

Gopls on by default in the VS Code Go extension

Go tools team
1 February 2021

We're happy to announce that the VS Code Go extension now enables the gopls language server by default, to deliver more robust IDE features and better support for Go modules.

(gopls provides IDE features, such as as intelligent autocompletion, signature help, refactoring, and workspace symbol search.)

When Go modules were released two years ago, they completely changed the landscape of Go developer tooling. Tools like goimports and godef previously depended on the fact that code was stored in your $GOPATH. When the Go team began rewriting these tools to work with modules, we immediately realized that we needed a more systematic approach to bridge the gap.

As a result, we began working on a single Go language server, gopls, which provides IDE features, such as autocompletion, formatting, and diagnostics to any compatible editor frontend. This persistent and unified server is a fundamental shift from the earlier collections of command-line tools.

In addition to working on gopls, we sought other ways of creating a stable ecosystem of editor tooling. Last year, the Go team took responsibility for the Go extension for VS Code. As part of this work, we smoothed the extension’s integration with the language server—automating gopls updates, rearranging and clarifying gopls settings, improving the troubleshooting workflow, and soliciting feedback through a survey. We’ve also continued to foster a community of active users and contributors who have helped us improve the stability, performance, and user experience of the Go extension.

Announcement

January 28 marked a major milestone in both the gopls and VS Code Go journeys, as gopls is now enabled by default in the Go extension for VS Code.

In advance of this switch we spent a long time iterating on the design, feature set, and user experience of gopls, focusing on improving performance and stability. For more than a year, gopls has been the default in most plugins for Vim, Emacs, and other editors. We’ve had 24 gopls releases, and we’re incredibly grateful to our users for consistently providing feedback and reporting issues on each and every one.

We’ve also dedicated time to smoothing the new user experience. We hope that VS Code Go with gopls will be intuitive with clear error messages, but if you have a question or need to adjust some configuration, you’ll be able to find answers in our updated documentation. We have also recorded a screencast to help you get started, as well as animations to show off some hard-to-find features.

Gopls is the best way of working with Go code, especially with Go modules. With the upcoming arrival of Go 1.16, in which modules are enabled by default, VS Code Go users will have the best possible experience out-of-the-box.

Still, this switch does not mean that gopls is complete. We will continue working on bug fixes, new features, and general stability. Our next area of focus will be improving the user experience when working with multiple modules. Feedback from our larger user base will help inform our next steps.

So, what should you do?

If you use VS Code, you don’t need to do anything. When you get the next VS Code Go update, gopls will be enabled automatically.

If you use another editor, you are likely using gopls already. If not, see the gopls user guide to learn how to enable gopls in your preferred editor. The Language Server Protocol ensures that gopls will continue to offer the same features to every editor.

If gopls is not working for you, please see our detailed troubleshooting guide and file an issue. If you need to, you can always disable gopls in VS Code.

Thank you

To our existing users, thank you for bearing with us as we rewrote our caching layer for the third time. To our new users, we look forward to hearing your experience reports and feedback.

Finally, no discussion of Go tooling is complete without mentioning the valuable contributions of the Go tools community. Thank you for the lengthy discussions, detailed bug reports, integration tests, and most importantly, thank you for the fantastic contributions. The most exciting gopls features come from our passionate open-source contributors, and we are appreciative of your hard work and dedication.

Learn more

Watch the screencast for a walk-through of how to get started with gopls and VS Code Go, and see the VS Code Go README for additional information.

If you’d like to read about gopls in more detail, see the gopls README.

Command PATH security in Go

Russ Cox
19 January 2021

Today’s Go security release fixes an issue involving PATH lookups in untrusted directories that can lead to remote execution during the go get command. We expect people to have questions about what exactly this means and whether they might have issues in their own programs. This post details the bug, the fixes we have applied, how to decide whether your own programs are vulnerable to similar problems, and what you can do if they are.

Go command & remote execution

One of the design goals for the go command is that most commands – including go build, go doc, go get, go install, and go list – do not run arbitrary code downloaded from the internet. There are a few obvious exceptions: clearly go run, go test, and go generate do run arbitrary code – that's their job. But the others must not, for a variety of reasons including reproducible builds and security. So when go get can be tricked into executing arbitrary code, we consider that a security bug.

If go get must not run arbitrary code, then unfortunately that means all the programs it invokes, such as compilers and version control systems, are also inside the security perimeter. For example, we've had issues in the past in which clever use of obscure compiler features or remote execution bugs in version control systems became remote execution bugs in Go. (On that note, Go 1.16 aims to improve the situation by introducing a GOVCS setting that allows configuration of exactly which version control systems are allowed and when.)

Today's bug, however, was entirely our fault, not a bug or obscure feature of gcc or git. The bug involves how Go and other programs find other executables, so we need to spend a little time looking at that before we can get to the details.

Commands and PATHs and Go

All operating systems have a concept of an executable path ($PATH on Unix, %PATH% on Windows; for simplicity, we'll just use the term PATH), which is a list of directories. When you type a command into a shell prompt, the shell looks in each of the listed directories, in turn, for an executable with the name you typed. It runs the first one it finds, or it prints a message like “command not found.”

On Unix, this idea first appeared in Seventh Edition Unix's Bourne shell (1979). The manual explained:

The shell parameter $PATH defines the search path for the directory containing the command. Each alternative directory name is separated by a colon (:). The default path is :/bin:/usr/bin. If the command name contains a / then the search path is not used. Otherwise, each directory in the path is searched for an executable file.

Note the default: the current directory (denoted here by an empty string, but let's call it “dot”) is listed ahead of /bin and /usr/bin. MS-DOS and then Windows chose to hard-code that behavior: on those systems, dot is always searched first, automatically, before considering any directories listed in %PATH%.

As Grampp and Morris pointed out in their classic paper “UNIX Operating System Security” (1984), placing dot ahead of system directories in the PATH means that if you cd into a directory and run ls, you might get a malicious copy from that directory instead of the system utility. And if you can trick a system administrator to run ls in your home directory while logged in as root, then you can run any code you want. Because of this problem and others like it, essentially all modern Unix distributions set a new user's default PATH to exclude dot. But Windows systems continue to search dot first, no matter what PATH says.

For example, when you type the command

go version

on a typically-configured Unix, the shell runs a go executable from a system directory in your PATH. But when you type that command on Windows, cmd.exe checks dot first. If .\go.exe (or .\go.bat or many other choices) exists, cmd.exe runs that executable, not one from your PATH.

For Go, PATH searches are handled by exec.LookPath, called automatically by exec.Command. And to fit well into the host system, Go's exec.LookPath implements the Unix rules on Unix and the Windows rules on Windows. For example, this command

out, err := exec.Command("go", "version").CombinedOutput()

behaves the same as typing go version into the operating system shell. On Windows, it runs .\go.exe when that exists.

(It is worth noting that Windows PowerShell changed this behavior, dropping the implicit search of dot, but cmd.exe and the Windows C library SearchPath function continue to behave as they always have. Go continues to match cmd.exe.)

The Bug

When go get downloads and builds a package that contains import "C", it runs a program called cgo to prepare the Go equivalent of the relevant C code. The go command runs cgo in the directory containing the package sources. Once cgo has generated its Go output files, the go command itself invokes the Go compiler on the generated Go files and the host C compiler (gcc or clang) to build any C sources included with the package. All this works well. But where does the go command find the host C compiler? It looks in the PATH, of course. Luckily, while it runs the C compiler in the package source directory, it does the PATH lookup from the original directory where the go command was invoked:

cmd := exec.Command("gcc", "file.c")
cmd.Dir = "badpkg"
cmd.Run()

So even if badpkg\gcc.exe exists on a Windows system, this code snippet will not find it. The lookup that happens in exec.Command does not know about the badpkg directory.

The go command uses similar code to invoke cgo, and in that case there's not even a path lookup, because cgo always comes from GOROOT:

cmd := exec.Command(GOROOT+"/pkg/tool/"+GOOS_GOARCH+"/cgo", "file.go")
cmd.Dir = "badpkg"
cmd.Run()

This is even safer than the previous snippet: there's no chance of running any bad cgo.exe that may exist.

But it turns out that cgo itself also invokes the host C compiler, on some temporary files it creates, meaning it executes this code itself:

// running in cgo in badpkg dir
cmd := exec.Command("gcc", "tmpfile.c")
cmd.Run()

Now, because cgo itself is running in badpkg, not in the directory where the go command was run, it will run badpkg\gcc.exe if that file exists, instead of finding the system gcc.

So an attacker can create a malicious package that uses cgo and includes a gcc.exe, and then any Windows user that runs go get to download and build the attacker's package will run the attacker-supplied gcc.exe in preference to any gcc in the system path.

Unix systems avoid the problem first because dot is typically not in the PATH and second because module unpacking does not set execute bits on the files it writes. But Unix users who have dot ahead of system directories in their PATH and are using GOPATH mode would be as susceptible as Windows users. (If that describes you, today is a good day to remove dot from your path and to start using Go modules.)

(Thanks to RyotaK for reporting this issue to us.)

The Fixes

It's obviously unacceptable for the go get command to download and run a malicious gcc.exe. But what's the actual mistake that allows that? And then what's the fix?

One possible answer is that the mistake is that cgo does the search for the host C compiler in the untrusted source directory instead of in the directory where the go command was invoked. If that's the mistake, then the fix is to change the go command to pass cgo the full path to the host C compiler, so that cgo need not do a PATH lookup in to the untrusted directory.

Another possible answer is that the mistake is to look in dot during PATH lookups, whether happens automatically on Windows or because of an explicit PATH entry on a Unix system. A user may want to look in dot to find a command they typed in a console or shell window, but it's unlikely they also want to look there to find a subprocess of a subprocess of a typed command. If that's the mistake, then the fix is to change the cgo command not to look in dot during a PATH lookup.

We decided both were mistakes, so we applied both fixes. The go command now passes the full host C compiler path to cgo. On top of that, cgo, go, and every other command in the Go distribution now use a variant of the os/exec package that reports an error if it would have previously used an executable from dot. The packages go/build and go/import use the same policy for their invocation of the go command and other tools. This should shut the door on any similar security problems that may be lurking.

Out of an abundance of caution, we also made a similar fix in commands like goimports and gopls, as well as the libraries golang.org/x/tools/go/analysis and golang.org/x/tools/go/packages, which invoke the go command as a subprocess. If you run these programs in untrusted directories – for example, if you git checkout untrusted repositories and cd into them and then run programs like these, and you use Windows or use Unix with dot in your PATH – then you should update your copies of these commands too. If the only untrusted directories on your computer are the ones in the module cache managed by go get, then you only need the new Go release.

After updating to the new Go release, you can update to the latest gopls by using:

GO111MODULE=on \
go get golang.org/x/tools/gopls@v0.6.4

and you can update to the latest goimports or other tools by using:

GO111MODULE=on \
go get golang.org/x/tools/cmd/goimports@v0.1.0

You can update programs that depend on golang.org/x/tools/go/packages, even before their authors do, by adding an explicit upgrade of the dependency during go get:

GO111MODULE=on \
go get example.com/cmd/thecmd golang.org/x/tools@v0.1.0

For programs that use go/build, it is sufficient for you to recompile them using the updated Go release.

Again, you only need to update these other programs if you are a Windows user or a Unix user with dot in the PATH and you run these programs in source directories you do not trust that may contain malicious programs.

Are your own programs affected?

If you use exec.LookPath or exec.Command in your own programs, you only need to be concerned if you (or your users) run your program in a directory with untrusted contents. If so, then a subprocess could be started using an executable from dot instead of from a system directory. (Again, using an executable from dot happens always on Windows and only with uncommon PATH settings on Unix.)

If you are concerned, then we've published the more restricted variant of os/exec as golang.org/x/sys/execabs. You can use it in your program by simply replacing

import "os/exec"

with

import exec "golang.org/x/sys/execabs"

and recompiling.

Securing os/exec by default

We have been discussing on golang.org/issue/38736 whether the Windows behavior of always preferring the current directory in PATH lookups (during exec.Command and exec.LookPath) should be changed. The argument in favor of the change is that it closes the kinds of security problems discussed in this blog post. A supporting argument is that although the Windows SearchPath API and cmd.exe still always search the current directory, PowerShell, the successor to cmd.exe, does not, an apparent recognition that the original behavior was a mistake. The argument against the change is that it could break existing Windows programs that intend to find programs in the current directory. We don’t know how many such programs exist, but they would get unexplained failures if the PATH lookups started skipping the current directory entirely.

The approach we have taken in golang.org/x/sys/execabs may be a reasonable middle ground. It finds the result of the old PATH lookup and then returns a clear error rather than use a result from the current directory. The error returned from exec.Command("prog") when prog.exe exists looks like:

prog resolves to executable in current directory (.\prog.exe)

For programs that do change behavior, this error should make very clear what has happened. Programs that intend to run a program from the current directory can use exec.Command("./prog") instead (that syntax works on all systems, even Windows).

We have filed this idea as a new proposal, golang.org/issue/43724.

See the index for more articles.