Skip to content

proposal: runtime: remove unnecessary copies when converting the command line to os.Args under Windows #73507

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

Open
xformerfhs opened this issue Apr 26, 2025 · 7 comments
Labels
compiler/runtime Issues related to the Go compiler and/or runtime. NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. Proposal
Milestone

Comments

@xformerfhs
Copy link

Proposal Details

First, I have to explain the context in which this proposal came up:

A program that encrypts and decrypts data gets its passwords and keys from the command line.
I know there are better and preferable ways of storing secrets, like KMS and secrets managers.
However, in this special setting it is necessary that the passwords are given as command line parameters.

What can be done about this is that the command line parameters are wiped from memory after they have been converted to internal data structures and before the encryption/decryption is carried out.
To achieve this, one has to overwrite both the command line of the operating system and os.Args in the Go runtime.

It is easy to wipe the command line in the Linux environment.
It is more complicated to wipe the command line in Windows, but I successfully managed to do it, finding a bug in *NTUnicodeString.Slice() in package golang.org/x/sys/windows along the way.

Then came the part to clear os.Args.
This can be done easily with this function:

func wipeGoCommandLine() {
   // Clear all command line arguments in os.Args, except the program name.
   for i := 1; i < len(os.Args); i++ {
      clear(unsafe.Slice(unsafe.StringData(os.Args[i]), len(os.Args[i])))
   }

   // Make os.Args only contain the program name.
   os.Args = os.Args[:1]
}

It works perfectly under Linux.
However, under Windows I had a very strange phenomenon:

The above function worked, except when an os.Args string had a length of 1!
In that case the program crashed with an access violation fault.
All other lengths worked fine, only parameters of length 1 caused this.

I did quite a bit of research and pilfered through the Go runtime with source code and Ida Free debugging.
Finally, I found the cause of this strange phenomenon:

The Go runtime handles the command line under Windows quite differently from other OSes.
The command line under Windows is encoded in UTF-16, and so it has to be converted to UTF-8.
This makes a copy of the command line text in UTF-8.
Then this UTF-8 copy of the command line is converted to the os.Args slice in the function commandLineToArgv that is defined in os/exec_windows.go:

// commandLineToArgv splits a command line into individual argument
// strings, following the Windows conventions documented
// at http://daviddeley.com/autohotkey/parameters/parameters.htm#WINARGV
func commandLineToArgv(cmd string) []string {
	var args []string

	for len(cmd) > 0 {
		if cmd[0] == ' ' || cmd[0] == '\t' {
			cmd = cmd[1:]
			continue
		}
		var arg []byte
		arg, cmd = readNextArg(cmd)
		args = append(args, string(arg))
	}
	return args
}

At the end there is the statement args = append(args, string(arg)).
It converts the byte slice returned by the readNextArg into a string and appends it to the args slice.
The statement string(arg) creates a copy of the byte slice by calling the function runtime.slicebytetostring.

This runtime function has a strange quirk:
If the argument has a length of 1, arg is discarded and instead a pointer to a constant in the table runtime_staticuint64s is returned, that has the same value as arg.

It is this constant that causes the access violation fault.
The constant table is in a memory section marked as read-only.
It cannot be written to and when clear gets the address of this constant it tries to overwrite the read-only content and this leads to the access violation fault.

Contrast this with the way the command line is handled by the Go runtime on Linux:

The file runtime/runtime1.go contains the following function:

func goargs() {
   if GOOS == "windows" {
      return
   }
   argslice = make([]string, argc)
   for i := int32(0); i < argc; i++ {
      argslice[i] = gostringnocopy(argv_index(argv, i))		
   }
}

As one can see the arguments are converted to Go strings with the gostringnocopy function.
It does not copy the strings, but rather builds Go string structures that point to the bytes.

So, why are the arguments in the Windows case copied twice?
First, when converting from UTF-16 to UTF-8, which is necessary.
And then again when converting the byte slices to strings.
This second copy by the string conversion is not necessary.

TL;DR:

To make a long story short, the proposal is:
Change the function that converts the command line to os.Args on Windows to use gostringnocopy(arg) or unsafe.String(&arg[0], len(arg)), whichever is appropriate.

This saves unnecessary copy operations and brings the behavior of os.Args under Windows in line with the behavior on other platforms.

Also, I expected the function that processes the command line to be a part of the runtime package, not os.

As a side note I really would like to know the rationale behind the special handling of one byte slices by runtime.slicebytetostring.

@gopherbot gopherbot added this to the Proposal milestone Apr 26, 2025
@gabyhelp gabyhelp added the LibraryProposal Issues describing a requested change to the Go standard library or x/ libraries, but not to a tool label Apr 26, 2025
@randall77
Copy link
Contributor

As a side note I really would like to know the rationale behind the special handling of one byte slices by runtime.slicebytetostring.

To avoid an allocation.

You should never write to the result of unsafe.StringData. It even says so right on the box.

I think it would be fine to avoid an extra copy, though.

I don't know why readNextArg returns a []byte instead of just a slice of the input string. I guess it has to deal with double slashes thing somehow, but maybe we could do allocations only on when double slashes appear.

Taking out of the proposal process. This is just an internal detail.

@randall77 randall77 removed Proposal LibraryProposal Issues describing a requested change to the Go standard library or x/ libraries, but not to a tool labels Apr 26, 2025
@prattmic prattmic changed the title proposal: runtime: Remove unnecessary copies when converting the command line to os.Args under Windows runtime: Remove unnecessary copies when converting the command line to os.Args under Windows Apr 26, 2025
@xformerfhs
Copy link
Author

xformerfhs commented Apr 26, 2025

@randall77, thanks for your answer.

To avoid an allocation.

Of course, now that you say it, it is obvious. Thank you.

You should never write to the result of unsafe.StringData. It even says so right on the box.

I know. This is why I did not propose to ensure the writeability. Here I am on my own and there is absolutely no guarantee that this will work in the future. And I will certainly not complain when the program behaves strange.

My intention was to point out the unnecessary copy and the differing behavior of the same object (os.Args) on the two platforms

@seankhliao seankhliao changed the title runtime: Remove unnecessary copies when converting the command line to os.Args under Windows runtime: remove unnecessary copies when converting the command line to os.Args under Windows Apr 29, 2025
@cagedmantis cagedmantis modified the milestones: Proposal, Backlog Apr 30, 2025
@cagedmantis cagedmantis added Performance compiler/runtime Issues related to the Go compiler and/or runtime. NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. labels Apr 30, 2025
@prattmic
Copy link
Member

prattmic commented May 7, 2025

This does sound like it would need sort of API to do safely, so adding the Proposal label back.

cc @golang/security

@prattmic prattmic changed the title runtime: remove unnecessary copies when converting the command line to os.Args under Windows proposal: runtime: remove unnecessary copies when converting the command line to os.Args under Windows May 7, 2025
@neild
Copy link
Contributor

neild commented May 7, 2025

Changing code to provide a property we don't guarantee is generally a bad idea. If we change Windows to construct os.Args in a way that permits users to use unsafe.StringData to write over the command-line arguments, is it a bug if we make a change in the future that breaks this use case?

We can say "there are no guarantees, and this might stop working". If that's true, what's the justification for making a change now to let this start working?

@xformerfhs
Copy link
Author

I would like to emphasize that the "proposal" is to remove the unnecessary memory allocation by the string conversion. The reason is efficiency, not the ability to overwrite a string.

I mentioned the original reason why I came across this little inefficiency to show the context. Maybe, it would have been better, if I just had stated that I found this unnecessary allocation. But I am pretty sure that then someone would have asked how I came up with this. I was also irritated by the inconsistent behavior of the same Go program under Windows and Linux. But, again, this one is about the unnecessary string allocation.

It may be worthwile to think about an API to delete the command line, but that should not rely on the Go programmer overwriting strings by using unsafe.StringData. Maybe this API could be a spin-off of this proposal.

@prattmic
Copy link
Member

prattmic commented May 8, 2025

Apologies for the misunderstanding.

For what it's worth, if this is just about the performance implications, I don't immediately see a few extra allocations that occur one time at start up as a major issue worth attention. But if someone sent a CL making this more efficient without hurting readability/maintainability too much I think that would be OK.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler/runtime Issues related to the Go compiler and/or runtime. NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. Proposal
Projects
Development

No branches or pull requests

7 participants