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

proposal: add GOEXPERIMENT=checkptr #22218

Closed
amscanne opened this issue Oct 11, 2017 · 34 comments
Closed

proposal: add GOEXPERIMENT=checkptr #22218

amscanne opened this issue Oct 11, 2017 · 34 comments

Comments

@amscanne
Copy link
Contributor

This is a proposal for this change, and see if there's any interest in having it move forward.

Background

During stack growth, pointers that have values between 0 and minLegalPointer will cause a "invalid pointer found on stack" exception to be thrown by the runtime. This suggests that storing these pointers on the stack is not a valid operation, i.e. the following code may cause a runtime exception if the stack happens to grow.

p := unsafe.Pointer(uintptr(1))

Since this is not a permitted state and will cause a runtime exception, it is preferable for the user to receive a recoverable error at the time the state is introduced. Even if this results in the program crashing, the full stack trace will be available making the cause of the problem much easier to diagnose.

Proposal

I propose to add code that causes a recoverable panic at the time the conversion is made. This conversion adds a small cost to each unsafe.Pointer cast. I believe that these conversions in user code should be relatively rare (usually associated with some other costly event, such as allocating a new object), but there may be cases where it's common (e.g. some CGo usage?). The original change did not apply these checks to code in the runtime package itself, due to its presumed safety and the fact that the conversions were common for the implementation of core types (e.g. map, channels, etc.).

Alternate Proposal

As noted in the original change, this behavior may be surprising. Users may be encoding some CGo void* equivalents as pointers in Go, which could contain "illegal" values. The constraint imposed by the stack growth code is non-obvious and undocumented.

Currently, debug.invalidptr is used in both heapBitsForObject and stack adjustment and defaults to on. I propose that a new debug variable is introduced, debug.stackptr, that toggles the behavior of the check during stack growth. I would advocate that this check should default to off, but regardless it would allow just that check to be disabled if it proves problematic.

@gopherbot gopherbot added this to the Proposal milestone Oct 11, 2017
@cznic
Copy link
Contributor

cznic commented Oct 11, 2017

I believe that these conversions in user code should be relatively rare (usually associated with some other costly event, such as allocating a new object), but there may be cases where it's common (e.g. some CGo usage?).

Virtual machines are perhaps rare, but they may use code making such conversions every few nanoseconds and in that case the performance would be hurt substantially.

@bcmills
Copy link
Contributor

bcmills commented Oct 12, 2017

the following code may cause a runtime exception if the stack happens to grow.

The example you give is one that could be caught at compile-time (e.g. by vet). Do you have more realistic examples to demonstrate the problem?

@bcmills
Copy link
Contributor

bcmills commented Oct 12, 2017

Users may be encoding some CGo void* equivalents as pointers in Go, which could contain "illegal" values.

Note that in C, converting an arbitrary integer to void* produces “implementation-defined” behavior. Portable C code cannot convert arbitrary integers to void*, just as portable Go code cannot convert arbitrary integers to unsafe.Pointer.

@cznic
Copy link
Contributor

cznic commented Oct 12, 2017

Portable C code cannot convert arbitrary integers to void*, ...

True, yet unfortunately some well known and widely used C code bases do that (and not only to void*). For example SQLite, including magic-valued function pointers, IIRC.

@bcmills
Copy link
Contributor

bcmills commented Oct 12, 2017

See also #19928, #21897, and #21730.

@amscanne
Copy link
Contributor Author

amscanne commented Oct 13, 2017

Virtual machines are perhaps rare, but they may use code making such conversions every few nanoseconds and in that case the performance would be hurt substantially.

I assume you're talking about language-level virtual machines here, where the implementation is in Go. Why would they require constant conversions between uintptr and unsafe.Pointer? I don't follow. It seems like their internal implementations would aim to be well-typed, just like everything else. Do you have an example?

The example you give is one that could be caught at compile-time (e.g. by vet). Do you have more realistic examples to demonstrate the problem?

Sure. Suppose you have something like:

   // C.create_foo => unsafe.Pointer(), maybe encoding errors as well
   foo := create_foo()
   // ... runtime can die starting here.
   if is_foo_okay(foo) {
      ...
   }

You might argue that the create_foo() interface is bad. Agreed, but it's still common. It's even used within the runtime itself for e.g. mmap return values within mem_linux.go:

func sysAlloc(n uintptr, sysStat *uint64) unsafe.Pointer {
  	p := mmap(nil, n, _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_PRIVATE, -1, 0)
  	if uintptr(p) < 4096 {
  		if uintptr(p) == _EACCES {
  			print("runtime: mmap: access denied\n")
  			exit(2)
  		}
  		if uintptr(p) == _EAGAIN {
  			print("runtime: mmap: too much locked memory (check 'ulimit -l').\n")
  			exit(2)
  		}
  		return nil
  	}
  	mSysStatInc(sysStat, n)
  	return p
  }

The above function uses "invalid" behavior per #21897. If it were not marked with go:nosplit, and happened to grow while p was on the stack, then it would panic the runtime.

I also encountered this issue in a specific context as well that I will follow up with you about separately.

Note that in C, converting an arbitrary integer to void* produces “implementation-defined” behavior. Portable C code cannot convert arbitrary integers to void*, just as portable Go code cannot convert arbitrary integers to unsafe.Pointer.

I'm with you here, but it still happens. The go runtime itself is guilty of this.

@bcmills
Copy link
Contributor

bcmills commented Oct 13, 2017

You might argue that the create_foo() interface is bad. Agreed, but it's still common.

Isn't that exactly the use-case that the Go uintptr type (and the corresponding C99 uintptr_t type) is meant to address? (Why return void* / unsafe.Pointer when the caller can always do that conversion explicitly once they have verified that the value is, in fact, a valid pointer?)

It's even used within the runtime itself

The runtime can rely upon whatever implementation details it likes. (Runtime code is not user code!)

That said, arguably we should fix runtime.mmap to return a uintptr so that it sets a better example.

@cznic
Copy link
Contributor

cznic commented Oct 14, 2017

@amscanne

I assume you're talking about language-level virtual machines here, where the implementation is in Go.

I'm talking about a particular VM.

Why would they require constant conversions between uintptr and unsafe.Pointer?

VM memory consists of the code memory, stack, text and data segments and heap memroy. Except for the code memory, all other VM memory is allocated outside of the Go runtime using mmaps. VM pointers are uintptrs. Every access to any non code memory converts a particular uintptr to an unsafe.Pointer. Essentially every VM operation/instruction has two or more mmaped memory accesses, for example AddI32.

@amscanne
Copy link
Contributor Author

Isn't that exactly the use-case that the Go uintptr type (and the corresponding C99 uintptr_t type) is meant to address? (Why return void* / unsafe.Pointer when the caller can always do that conversion explicitly once they have verified that the value is, in fact, a valid pointer?)

Sure, but this doesn't always happen. See the runtime. Note that in the reference change, I update the code there to use the uintptr correctly:
https://go-review.googlesource.com/c/go/+/34719/1/src/runtime/mem_linux.go

The runtime can rely upon whatever implementation details it likes. (Runtime code is not user code!)

Uff. The argument that the runtime doesn't need to be "valid" Go code is disconcerting. (I say this because one of the referenced issues was closed as not a valid thing to do.) I understand the need for the pragmatic considerations and exceptions for the runtime (hence the exception in the original change), but that's a different thing.

I'm talking about a particular VM.

Gotcha. Yes, I see the calls you're referring to here:
https://github.com/cznic/virtual/blob/06a14e3fe719b20921b8be3ff81847eac0e53527/cpu.go#L65

This does hit the crux of the issue. The code there could be slightly restructured and there would be the risk of triggering runtime errors. (If the pointers lived on the stack during a growth event instead of being immediately consumed.)

I'm not really sure about the cost in that case anyways. The branch predictor will likely make most of it disappear. If the proposal is friendly (though I get the sense that it is not), I would happily run some benchmark if you're got it to assess the impact for your case.

@bcmills
Copy link
Contributor

bcmills commented Oct 16, 2017

Note that in the reference change, I update the code there to use the uintptr correctly

That seems like a good idea regardless of whether we also add the proposed dynamic check.

@bcmills
Copy link
Contributor

bcmills commented Oct 16, 2017

I note the following in the reference change description:

This will only happen on conversion to unsafe.Pointer from
non-pointer types, so most internal runtime pointer mangling should be
free from these checks.

Does that actually address a significant fraction of bad pointers? If the pointer is coming from C code, odds are good that it's already encoded as an unsafe.Pointer.

Or does the code generated by cgo itself do explicit conversions? (If so, it still seems unfortunate to couple the runtime pointer-validity check to a cgo implementation detail, especially when the interaction between cgo and the compiler is in question: see #16623.)

@amscanne
Copy link
Contributor Author

Does that actually address a significant fraction of bad pointers? If the pointer is coming from C code, odds are good that it's already encoded as an unsafe.Pointer.

Or does the code generated by cgo itself do explicit conversions? (If so, it still seems unfortunate to couple the runtime pointer-validity check to a cgo implementation detail, especially when the interaction between cgo and the compiler is in question: see #16623.)

This is a good question. It does seem like the structure would currently avoid this check. (The generated code effectively passes a *unsafe.Pointer which is filled in by the generated C stub.) I think the logical thing to do would be to update the CGo code generation to use uintptr across the boundary and convert to unsafe.Pointer only on the Go side.

@gopherbot
Copy link

Change https://golang.org/cl/71270 mentions this issue: runtime: separate error result for mmap

@aclements
Copy link
Member

I propose to add code that causes a recoverable panic at the time the conversion is made. This conversion adds a small cost to each unsafe.Pointer cast.

This seems like a not unreasonable debugging mode to me, but it's not clear that it should be on by default. It could be enabled, for example, by a GOEXPERIMENT setting. In that case, I wouldn't mind having it on even in the runtime, since the runtime shouldn't be making up bad unsafe.Pointers either.

Conversions that create bad unsafe.Pointers must violate the unsafe.Pointer rules. Having the ability to trap on clear violations of the rules seems like a good idea. Related to this, I've been exploring adding significantly more safe-points to code, which could expose more code that violates these rules. Having a more aggressive check would be useful for testing this.

Alternate proposal
I propose that a new debug variable is introduced, debug.stackptr, that toggles the behavior of the check during stack growth. I would advocate that this check should default to off, but regardless it would allow just that check to be disabled if it proves problematic.

The real danger here is not that we observe a small-valued not-really-a-pointer. The danger is that such code may also generate a not-really-a-pointer that actually looks like a real pointer. That can crash GC hard. The main point of detecting small-valued pointers is as a first line of defense against this. Turning off that check won't do anyone any favors.

gopherbot pushed a commit that referenced this issue Oct 18, 2017
Currently mmap returns an unsafe.Pointer that encodes OS errors as
values less than 4096. In practice this is okay, but it borders on
being really unsafe: for example, the value has to be checked
immediately after return and if stack copying were ever to observe
such a value, it would panic. It's also not remotely idiomatic.

Fix this by making mmap return a separate pointer value and error,
like a normal Go function.

Updates #22218.

Change-Id: Iefd965095ffc82cc91118872753a5d39d785c3a6
Reviewed-on: https://go-review.googlesource.com/71270
Run-TryBot: Austin Clements <austin@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
@ianlancetaylor
Copy link
Contributor

There doesn't seem to be much support for the original proposal, which seems to be unconditionally adding some checking code to conversions from uintptr to unsafe.Pointer. That would slow down correct uses. It might also require some mechanism for turning it off for the garbage collector or other code; not sure.

As a debugging mode it makes sense, perhaps through a compiler flag.

@rsc
Copy link
Contributor

rsc commented Apr 2, 2018

Following Austin's line of thought, I suggest GOEXPERIMENT=checkptr and make it a throw, not a panic. It would be great as a debugging mode.

@aclements
Copy link
Member

I apologize if I've lost track of some of the history of this, but was this motivated by an observed problem, or is this theoretical? Neither the issue nor the CL give a concrete motivation.

I discussed this with several of the runtime and compiler developers today and came up with a counter-proposal: enable this behind a GOEXPERIMENT flag, but make the check much more exhaustive than simply looking for small-valued pointers. The small-valued pointers check in stack copying and GC is just a canary in a coal mine: it's meant to detect strong evidence that there are bad pointers around, but it's in no way exhaustive. Silencing the canary is too little, to the point of being misleading. Pointers < 4096 are not more bad than any other bad pointers. So if we're going to add a debugging mode for this, we should own it and detect all of the bad pointers we can detect, including pointers to dead objects in the Go heap.

@amscanne
Copy link
Contributor Author

amscanne commented Apr 6, 2018

This was an issue hit in a production system. Given the nature of the issue (panic on stack check), it wasn't obvious to debug.

An experimental check mode seems like a good proposal, presumably it would have caught the bad usage fixed by https://golang.org/cl/71270 at a minimum.

@rsc
Copy link
Contributor

rsc commented Apr 16, 2018

Everyone seems to agree that the resolution here would be to add GOEXPERIMENT=checkptr and check for more pointers than just < 4096. Accepting that proposal, although no one is promising to work on it.

@rsc rsc modified the milestones: Proposal, Unplanned Apr 16, 2018
@rsc rsc changed the title proposal: Check pointers for illegal values on conversion from uintptr proposal: add GOEXPERIMENT=checkptr Apr 16, 2018
@mdempsky
Copy link
Member

mdempsky commented Feb 13, 2019

Some suggestions:

  1. GOEXPERIMENT is very heavy weight and requires rebuilding the entire toolchain, whereas I don't see any reason this can't just be a compiler flag. We can hide it behind -d initially while still experimenting with ideas.

  2. We can check that when converting unsafe.Pointer to *T that the pointer is aligned according to T's alignment. This would be a very lightweight check.

  3. When converting unsafe.Pointer to *T, if the result points into the heap, make sure objectoffset + sizeof(T) doesn't exceed the span's sizeclass. This seems unlikely to find many issues in practice, but shouldn't be too hard to instrument.

  4. We can check that when converting uintptr to unsafe.Pointer that if it points to a Go object in the heap, then it was derived from an existing pointer to that same object according to the pointer arithmetic rules.

For the last one, at compile time, given unsafe.Pointer(U) where U is a uintptr-typed expression, we can define O(U) as:

O(uintptr(P)) = [P]    // if P has type unsafe.Pointer
O(U1 + U2) = O(U1) || O(U2)
O(U1 - _) = O(U1)
O(U1 &^ _) = O(U1)
O(_) = []

This picks out all the unsafe.Pointer expressions that according to package unsafe's strict arithmetic rules (add or subtract offsets, and use &^ to round pointers) that the resulting pointer could derive from. (In most cases, this is likely exactly one pointer.)

The compiler then just needs to invoke a runtime function like:

func checkPointerArithmetic(derived unsafe.Pointer, originals ...unsafe.Pointer) {
    baseD, _, _ := findObject(derived, 0, 0)
    if baseD == 0 {
         // Not in the heap.
         // TODO: Apply checks for stacks and/or globals?
         return
    }

    for _, original := range originals {
        baseO, _, _ := findObject(original, 0, 0)
        if baseD == baseO {
            // derived was computed from original.
            return
        }
    }

    throw("bad pointer arithmetic")
}

This is a bit more expensive than alignment checking, but should still be reasonably fast for how infrequent pointer arithmetic is.

It may also be possible to apply checks for objects on the stack or in globals. For example, we can at least guarantee that a pointer into a stack object or global object was derived from a pointer to that same stack/object region.

@gopherbot
Copy link

Change https://golang.org/cl/162237 mentions this issue: cmd/compile: add -d=checkptr to validate unsafe.Pointer rules

@mdempsky
Copy link
Member

mdempsky commented Feb 13, 2019

I pushed a mostly functional proof-of-concept CL to 162237 that checks for pointers between 0 and minLegalPointer, pointer alignment, and pointer arithmetic. (It does not check pointed-to-object size.) It noticed a handful of "failures" within the standard library:

  • strings/builder.go's noescape function.
  • v := Mincore(unsafe.Pointer(uintptr(1)), 1, &dst) in runtime/runtime_linux_test.go.
  • StorePointer(addr, unsafe.Pointer(new)) in atomic_test.go.
  • syscall.UnixRights creates a past-the-end-of-the-object pointer

I'd be interested if it catches anything interesting in other code bases. To try it out:

  1. Apply the above patch locally.
  2. Reinstall cmd/compile (i.e., go install cmd/compile).
  3. Build programs or run tests with -gcflags=-d=checkptr.

@josharian
Copy link
Contributor

  • syscall.UnixRights creates a past-the-end-of-the-object pointer

This seems like a bona fide failure, not a scare-quote failure. I'm surprised it hasn't caused any GC-related crashes.

@mdempsky
Copy link
Member

I think golang.org/cl/162237 is polished and ready for testing if folks want to try it out an real code bases.

Install cmd/compile with that CL applied, and then build/test your programs with -gcflags=all=-d=checkptr.

@gopherbot
Copy link

Change https://golang.org/cl/201617 mentions this issue: syscall: avoid "just past the end" pointers in UnixRights

gopherbot pushed a commit that referenced this issue Oct 17, 2019
Caught with -d=checkptr.

Updates #22218.

Change-Id: Ic0fcff4d2c8d83e4e7f5e0c6d01f03c9c7766c6d
Reviewed-on: https://go-review.googlesource.com/c/go/+/201617
Run-TryBot: Matthew Dempsky <mdempsky@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
@gopherbot
Copy link

Change https://golang.org/cl/201778 mentions this issue: cmd/compile: detect unsafe conversions from smaller to larger types

gopherbot pushed a commit that referenced this issue Oct 17, 2019
This CL extends the runtime instrumentation for (*T)(ptr) to also
check that the first and last bytes of *(*T)(ptr) are part of the same
heap object.

Updates #22218.
Updates #34959.

Change-Id: I2c8063fe1b7fe6e6145e41c5654cb64dd1c9dd41
Reviewed-on: https://go-review.googlesource.com/c/go/+/201778
Run-TryBot: Matthew Dempsky <mdempsky@google.com>
Reviewed-by: Keith Randall <khr@golang.org>
Reviewed-by: Cuong Manh Le <cuong.manhle.vn@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
@gopherbot
Copy link

Change https://golang.org/cl/201781 mentions this issue: cmd/compile: escape unsafe.Pointer conversions when -d=checkptr

gopherbot pushed a commit that referenced this issue Oct 17, 2019
This CL tweaks escape analysis to treat unsafe.Pointer(ptr) as an
escaping operation when -d=checkptr is enabled. This allows better
detection of unsafe pointer arithmetic and conversions, because the
runtime checkptr instrumentation can currently only detect object
boundaries for heap objects, not stack objects.

Updates #22218.
Fixes #34959.

Change-Id: I856812cc23582fe4d0d401592583323e95919f28
Reviewed-on: https://go-review.googlesource.com/c/go/+/201781
Run-TryBot: Matthew Dempsky <mdempsky@google.com>
Reviewed-by: Keith Randall <khr@golang.org>
Reviewed-by: Cherry Zhang <cherryyz@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
@gopherbot
Copy link

Change https://golang.org/cl/201782 mentions this issue: cmd/compile: fix -d=checkptr for named unsafe.Pointer types

gopherbot pushed a commit that referenced this issue Oct 17, 2019
We need to explicitly convert pointers to unsafe.Pointer before
passing to the runtime checkptr instrumentation in case the user
declared their own type with underlying type unsafe.Pointer.

Updates #22218.
Fixes #34966.

Change-Id: I3baa2809d77f8257167cd78f57156f819130baa8
Reviewed-on: https://go-review.googlesource.com/c/go/+/201782
Run-TryBot: Matthew Dempsky <mdempsky@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Keith Randall <khr@golang.org>
@gopherbot
Copy link

Change https://golang.org/cl/201840 mentions this issue: cmd/compile: only escape unsafe.Pointer conversions when -d=checkptr=2

@gopherbot
Copy link

Change https://golang.org/cl/201839 mentions this issue: cmd/compile: recognize (*[Big]T)(ptr)[:n:m] pattern for -d=checkptr

@gopherbot
Copy link

Change https://golang.org/cl/201937 mentions this issue: unix: avoid "just past the end" pointers in UnixRights

gopherbot pushed a commit to golang/sys that referenced this issue Oct 18, 2019
Same as CL 201617 did for package syscall.

Caught with -d=checkptr

Updates golang/go#22218

Change-Id: I8208f8e6d9bd62376bf9e0458dc18956daabd785
Reviewed-on: https://go-review.googlesource.com/c/sys/+/201937
Run-TryBot: Tobias Klauser <tobias.klauser@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Elias Naur <mail@eliasnaur.com>
gopherbot pushed a commit that referenced this issue Oct 18, 2019
Escaping all unsafe.Pointer conversions for -d=checkptr seems like it
might be a little too aggressive to enable for -race/-msan mode, since
at least some tests are written to expect unsafe.Pointer conversions
to not affect escape analysis.

So instead only enable that functionality behind -d=checkptr=2.

Updates #22218.
Updates #34959.

Change-Id: I2f0a774ea5961dabec29bc5b8ebe387a1b90d27b
Reviewed-on: https://go-review.googlesource.com/c/go/+/201840
Run-TryBot: Matthew Dempsky <mdempsky@google.com>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
@gopherbot
Copy link

Change https://golang.org/cl/202580 mentions this issue: reflect, internal/reflectlite: set capacity when slicing unsafe pointers

gopherbot pushed a commit that referenced this issue Oct 21, 2019
A common idiom for turning an unsafe.Pointer into a slice is to write:

    s := (*[Big]T)(ptr)[:n:m]

This technically violates Go's unsafe pointer rules (rule #1 says T2
can't be bigger than T1), but it's fairly common and not too difficult
to recognize, so might as well allow it for now so we can make
progress on #34972.

This should be revisited if #19367 is accepted.

Updates #22218.
Updates #34972.

Change-Id: Id824e2461904e770910b6e728b4234041d2cc8bc
Reviewed-on: https://go-review.googlesource.com/c/go/+/201839
Run-TryBot: Matthew Dempsky <mdempsky@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
gopherbot pushed a commit that referenced this issue Oct 21, 2019
Follow the idiom for allowing -d=checkptr to recognize and verify
correctness.

Updates #22218.
Updates #34972.

Change-Id: Ib6001c6f0e6dc535a36bcfaa1ae48e29e0c737f8
Reviewed-on: https://go-review.googlesource.com/c/go/+/202580
Run-TryBot: Matthew Dempsky <mdempsky@google.com>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
@gopherbot
Copy link

Change https://golang.org/cl/202677 mentions this issue: runtime: somewhat better checkptr error messages

gopherbot pushed a commit that referenced this issue Oct 22, 2019
They're still lacking in details, but at least better than being
printed as raw interface values.

Updates #22218.

Change-Id: I4fd813253afdd6455c0c9b5a05c61659805abad1
Reviewed-on: https://go-review.googlesource.com/c/go/+/202677
Run-TryBot: Matthew Dempsky <mdempsky@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
@gopherbot
Copy link

Change https://golang.org/cl/214238 mentions this issue: runtime: add tests for checkptr

gopherbot pushed a commit that referenced this issue Jan 10, 2020
We had a few test cases to make sure checkptr didn't have certain
false positives, but none to test for any true positives. This CL
fixes that.

Updates #22218.

Change-Id: I24c02e469a4af43b1748829a9df325ce510f7cc4
Reviewed-on: https://go-review.googlesource.com/c/go/+/214238
Run-TryBot: Matthew Dempsky <mdempsky@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Reviewed-by: Austin Clements <austin@google.com>
@golang golang locked and limited conversation to collaborators Jan 8, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

9 participants