extending Go backward compatibility #55090
Replies: 18 comments 48 replies
This comment was marked as spam.
This comment was marked as spam.
-
I like the simplicity of this a lot. For what it's worth, "behave like go1.x" is actually what I expected the |
Beta Was this translation helpful? Give feedback.
-
We'd need to have the GODEBUG settings override field-by-field, probably. So if you did
In front of |
Beta Was this translation helpful? Give feedback.
-
Overall, a hearty +1. One downside to the implicit go.mod behavior is that getting all security improvements from a new Go version now requires actively editing go.mod. OTOH, this matches MVS pretty well, so maybe that’s a point in its favor. |
Beta Was this translation helpful? Give feedback.
-
FWIW, the |
Beta Was this translation helpful? Give feedback.
-
Right now, if a dependency has a higher go.mod Go version number than package main, but the code compiles, everything is fine. Would that change? If not, how does a dependency indicate that it depends on new behavior? If so, it seems like this might rapidly cause dependency management pain, because everything has a Go toolchain version dependency. (I know this is your other discussion topic, I just wanted to point out that the answer there might turn out to be very important to assessing the viability here.) |
Beta Was this translation helpful? Give feedback.
-
Should we add a new name for GODEBUG / This is very minor, but perhaps a chance to make a small improvement. |
Beta Was this translation helpful? Give feedback.
-
This seems like adding another "knob" to build configurations. As @josharian pointed out, there seem to be complexity in resolving how multiple modules with conflicting GODEBUG settings are merged together. Plus, it means that GODEBUG becomes a real, official part of the module system. Just saying "go 1.x" is so simple; I would hate to give that up. Let me push back and ask whether we should even have GODEBUG in the first place. Various settings are listed above, and they seem to relate to security decisions, like SHA1, or just deciding to break things, like making HTTP/2 the default (if I understand that correctly). In the former case, those breaking changes are part of the compat promise; old programs breaking with newer Go versions that make those kinds of changes is unfortunate, but expected, and even desired. Broken programs in that context reveal security flaws in them. I'm glad Kubernetes broke in those cases. If they want the benefits of newer Go versions, then they can pay the cost of avoiding those security flaws. This very well may mean that Kubernetes has to adopt a similar breaking change policy for security concerns that Go has. I suppose it's worth debating whether Go should impose that on package makers, but in my opinion, it should. No one should be entitled to upgrade without paying a price in this context. In the latter case, perhaps those changes shouldn't have been made at all. Instead of making HTTP/2 the default, we could have instead added an http2 package, or added an optional HTTP/2 mode to clients and servers. I can't recall whether the DNS resolver change was security-related, but if it wasn't, that also could have been an opt-in setting in the runtime or net packages. ParseIP probably shouldn't have been changed, since it was working as intended without a security flaw. Perhaps GODEBUG shouldn't exist at all. If behavior is changed, and it's compatible with the compat promise, then there should be a really good reason for it, and Kubernetes or whoever should be happy that it happened. Perhaps the reason why @rsc was so surprised by how often Kubernetes was broken was because the Kube team knew that, in most cases, most of those breaking changes happened for good reasons, and they accepted it. (Just conjecture. I don't claim to know or speak for them.) |
Beta Was this translation helpful? Give feedback.
-
Strong +1 on the ability to preserve backward compatible runtime behavior, I'm glad to see this discussion happening. I'd also like to see some stated period for how many minor versions a behavior breaking change will be toggleable. Specifically for Kubernetes, there is a ~1yr community support period for release branches. These are not aligned with the Go release cadence so only maintaining compatibility for 1 year in Go will still not satisfy the Kubernetes case. I would conjecture that a large portion of real-world Kubernetes usage is through distributors which often release 3-6 months behind upstream Kubernetes, and support that release for 12-15 months. This means a particular Kubernetes release's compatibility for end users right now needs just shy of 2 years of compatibility. Distributors and providers certainly could/should be faster to release which would ease the tail end of this, but if Go doesn't provide 18mo to 2 years of support the alternative for distributors is to either live with outdated versions of Go or backport CVE patches to a fork of Go. |
Beta Was this translation helpful? Give feedback.
-
Could there be any conflicts between main module info or debug info embedded in binaries built by a newer or older toolchain than the local one installed? For example, what happens if a future Go version uses DWARF-99, or adds a new field to the module info? Would those work with |
Beta Was this translation helpful? Give feedback.
-
If we look at the go line in the go.mod file to set the ground-rules, should the GODEBUG lines that declare exceptions to the rule reside there as well? |
Beta Was this translation helpful? Give feedback.
-
Can you expand on the mindset/goal behind wanting to update go while at the same not wanting the new go version behaviors? Is the problem
|
Beta Was this translation helpful? Give feedback.
This comment was marked as spam.
This comment was marked as spam.
-
Imo implicit behavior and hidden magic should be avoided as much as possible. |
Beta Was this translation helpful? Give feedback.
-
I fully understand that the power to choose the Go version semantics lies on the main package and module, but I wonder how this affects libraries. As a library author, I will only be testing with one version's semantics. If a downstream project chooses newer or older semantics, the library could be silently broken and the toolchain wouldn't help either developer see that, I think. |
Beta Was this translation helpful? Give feedback.
-
This discussion has been very helpful. Thanks everyone! |
Beta Was this translation helpful? Give feedback.
-
Wanted to add another datapoint on a Go change that broke a downstream consumer: 16f0f9c changed the default Windows This change wasn't something we could back-out with a |
Beta Was this translation helpful? Give feedback.
-
This is now a proposal at #56986. |
Beta Was this translation helpful? Give feedback.
-
This discussion is about backward compatibility, meaning new versions of Go compiling older Go code. For the problem of old versions of Go compiling newer Go code, see this other discussion about forward compatibility.
Go 1 introduced Go's compatibility promise, which says that old programs will by and large continue to run correctly in new versions of Go. There is an exception for security problems and certain other implementation overfitting. For example, code that depends on a given type not implementing a particular interface may change behavior when the type adds a new method, which we are allowed to do.
We now have about ten years of experience with Go 1 compatibility. In general it works very well for the Go team and for users. However, there are also practices we've developed since then that it doesn't capture (specifically GODEBUG settings), and there are still times when users’ programs break. I think it is worth extending our approach to try to break programs even less often, as well as to explicitly codify GODEBUG settings and clarify when they are and are not appropriate.
As background, I've been talking to the Kubernetes team about their experiences with Go. It turns out that Go's been averaging about one Kubernetes-breaking change per year for the past few years. I don't think Kubernetes is an outlier here: I expect most large projects have similar experiences. Once per year is not high, but it's not zero either, and our goal with Go 1 compatibility is zero.
Here are some examples of Kubernetes-breaking changes that we've made:
Go 1.17 changed net.ParseIP to reject addresses with leading zeros, like 0127.0000.0000.0001. Go interpreted them as decimal, following some RFCs, while all BSD-derived systems interpret them as octal. Rejecting them avoids taking part in parser misalignment bugs. (Here is an arguably exaggerated security report.)
Kubernetes clusters may have stored configs using such addresses, so this bug required them to make a copy of the parsers in order to keep accessing old data. In the interim, they were blocked from updating to Go 1.17.
Go 1.15 changed crypto/x509 not to fall back to a certificate's CN field to find a host name when the SAN field was omitted. The old behavior was preserved when using
GODEBUG=x509ignoreCN=0
. Go 1.17 removed support for that setting.The Go 1.15 change broke a Kubernetes test and required a warning to users in Kubernetes 1.19 release notes.
The Kubernetes 1.23 release notes warned users who were using the GODEBUG override that it was gone.
Go 1.18 dropped support for SHA1 certificates, with a
GODEBUG=x509sha1=1
override. We announced removal of that setting for Go 1.19 but changed plans on request from Kubernetes. SHA1 certificates are apparently still used by some enterprise CAs for on-prem Kubernetes installations.Go 1.19 changed LookPath behavior to remove an important class of security bugs, but the change may also break existing programs, so we included a
GODEBUG=execerrdot=0
override.The impact of this change on Kubernetes is still uncertain: the Kubernetes developers flagged it as risky enough to warrant further investigation.
These kinds of behavioral changes don't only cause pain for Kubernetes developers and users. They also make it impossible to update older, long-term-supported versions of Kubernetes to a newer version of Go. Those older versions don't have the same access to performance improvements and bug fixes. Again, this is not specific to Kubernetes. I am sure lots of projects are in similar situations.
As the examples show, over time we've adopted a practice of being able to opt out of these risky changes using
GODEBUG
settings. The examples also show that we have probably been too aggressive about removing those settings. But the settings themselves have clearly become an important part of Go's compatibility story.Other important compatibility-related GODEBUG settings include:
GODEBUG=asyncpreemptoff=1
disables signal-based goroutine preemption, which occasionally uncovers operating system bugs.GODEBUG=cgocheck=0
disables the runtime's cgo pointer checks.GODEBUG=cpu.<extension>=off
disables use of a particular CPU extension at run time.GODEBUG=http2client=0
disables client-side HTTP/2.GODEBUG=http2server=0
disables server-side HTTP/2.GODEBUG=netdns=cgo
forces use of the cgo resolver.GODEBUG=netdns=go
forces use of the Go DNS resolverPrograms that need one to use these can usually set the GODEBUG variable in
func init
of package main, but for runtime variables, that's too late: the runtime reads the variable early in Go program startup, before any of the user program has run yet. For those programs, the environment variable must be set in the execution environment. It cannot be “carried with” the program.Another problem with the GODEBUGs is that you have to know they exist. If you have a large system written for Go 1.17 and want to update to Go 1.18's toolchain, you need to know which settings to flip to keep as close to Go 1.17 semantics as possible.
I believe that we should make it even easier and safer for large projects like Kubernetes to update to new Go releases. In particular, I think we should probably:
GODEBUG may not be the mechanism I'd design today, but it's what we have and it doesn't seem bad enough to be worth adding a second way, so I'm going to assume it stays. Then the two things we need are (1) a way to set individual GODEBUG defaults in package main, and (2) a way to make the default GODEBUGs match an earlier version of Go.
For (1), I am thinking about something like
in any package main source file. These would be pulled out by the go command and linked into the binary for processing at startup (before any Go code runs). The GODEBUG environment variable would still override these, of course.
For (2), I am thinking about having the go line in the go.mod of package main's module, which already defines the exact language semantics of package main's Go source files, also define the default GODEBUG settings. So if package main's go.mod says
go 1.17
, SHA1 certificates still work, and os/exec does not generate ErrDot.As noted above, some GODEBUG settings will stick around forever (for example, execerrdot=1, netdns=go), while we will want to retire others. When a newer version of Go is compiling code written for an older version, if a GODEBUG has been retired, the behavior will depend on whether it is named explicitly (as in (1)) or implicitly (as in (2)). If a retired GODEBUG is mentioned explicitly, the build should fail. If it is only implied by the earlier Go version, then build should succeed, on the assumption that the vast majority of programs that say “go 1.17” are saying it because they were written in that era, not because they require support for SHA1 certificates.
I think these two changes would go a long way toward making it even easier and safer to update to new Go toolchains, because it separates the update from the riskiest behavior changes and makes those changes easy to temporarily opt out of and also to debug.
Note that this mechanism would be inappropriate to use for new, incompatible features, because the settings in package main are affecting the entire binary, and it is unlikely that all the packages in a large program would agree on which version of a large feature they want. In contrast, for “extra-backwards compatibility shims” like these, especially in the context where you're keeping older code running, affecting the whole binary is appropriate and does not cause problems.
One thing people ask occasionally is whether Go will ever add LTS (long-term support) releases. I've always thought of Go 1 as the LTS release of Go, but subtle, necessary breaking changes like the ones listed above contradicted that idea. Being able to use a new Go toolchain but still get more faithful “old” semantics in these cases brings us much closer to Go 1 as LTS.
There is a question about what to do if go.mod says a newer version of Go than the toolchain being used for the build. In that case the older toolchain does not know what the newer version does differently. I am filing a separate discussion about this forward compatibility problem.
This is a discussion, not a proposal. I haven't implemented this nor even worked out all the implications. I'm curious what people think and what concerns they have. Thanks!
Beta Was this translation helpful? Give feedback.
All reactions