-
Notifications
You must be signed in to change notification settings - Fork 601
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
many: improve environment handling, fixing duplicate entries #8242
Conversation
This patch adds three new types and supporting methods. Environment is an unordered map representing environment entries that is convenient to modify. EnvironmentDelta is an ordered map representing changes to an environment that also supports variable substitution and expansion. Finally RawEnvironment is a slice representing key=value pairs that is useful for low-level system interfaces. Environment and EnvironmentDelta can be modified with simple map-like methods Get, Set, Del. RawEnvironment can only be parsed to an Environment. The patch also contains Environment.ApplyDelta function, which correctly applies a single delta to a given environment. The types are designed to avoid the mistake of appending two slices representing environment strings together. This was the original cause of bug https://bugs.launchpad.net/snapd/+bug/1860369 Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
This patch replaces all ad-hoc environment handling with the three aforementioned types. Environment is used whenever we are working with the real complete environment (starting from OSEnvironment). EnvironmentDelta is used to modify it according to snap-level and application-level or hook-level overrides. Finally RawEnvironment is only used by snap run and snap-exec, immediately before calling execve. This fixes incorrect handling of environment in snap-exec and also avoids this class of bugs by making it hard, due to type system, to express mistakes again. Fixes: https://bugs.launchpad.net/snapd/+bug/1860369 Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
cmd/snap-exec/main_test.go
Outdated
|
||
var env strutil.Environment | ||
for k, v := range t.env { | ||
env.Set(k, v) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick, this could become:
env := strutil.NewEnvironment(t.env)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
skimmed this, some high-level comments/questions
strutil/env.go
Outdated
return Quoted(raw) | ||
} | ||
|
||
// Environment is an unordered map of key=value strings. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
os represents Environment as []string so probably Enviroment itself should be an OrderedMap as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I started with an ordered map but discarded that later on as it simplifies a few things. Order is irrelevant when there are no duplicates. Environment is specifically not RawEnvironment, with all of the associated complexity of duplicates, malformed entries and the like. When we produce a RawEnvironment we always sort entries so the results are deterministic where it matters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what does it simplifies? just syntax? a map is a map in the end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted RawEnvironment to be deterministic irrespective of the order of construction of the environment. Currently I produce order sorted by keys, which is natural for an otherwise unordered map.
When Environment had an intrinsic order discarding it felt wrong. At the same time the order of environment entries should really not be anything but an implementation detail so having order there was weird.
I looked back to when app and snap environment overrides were introduced. The only reason we had order was to support expanding expressions. Since Environment specifically has just key=values, not key=expressions, having order was not required.
I only kept order for EnvironmentDelta because we already represented environment overrides (in the yaml) as ordered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted RawEnvironment to be deterministic irrespective of the order of construction of the environment.
I understand this but it's again quite a change from current behavior of snapd, somebody can have a test expecting overridden things at the tail of what they get, and now they will be sorted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if I understand your concern correctly but environment order is not something applications perceive through most APIs. Only low level code can observe the order. As long as there are no maliciously crafted entries (not key=value, not duplicated keys) the order is totally irrelevant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Specifically after we chain through snap-exec the resulting order is not even the order we set in snap-run. It is the order as it was transformed by libc running in snap-confine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked a bit more and snapenv itself scrambles everything because of the through map roundtrip there, so my point here is moot. OTOH we should try to add as little as possible new code for the new types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think everything in this tread is addressed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a few more comments and suggestions
strutil/env.go
Outdated
// If multiple keys are transformed into the same key the value of the last | ||
// (lexicographically) key is used. If the transformed key is empty then the | ||
// corresponding entry is removed. | ||
func (env *Environment) Transform(tr func(key, value string) (string, string)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would try to avoid this part of the change for now, also because it's unclear to me that the splitting of responsibilities make sense. If this lives in osutil, the list of setgid discarded stuff could live there and the logic be moved to helpers that load os env and respectively prepare it for exec.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
This style is shorter and we use it nearly everywhere else. I'm about to combine more tests into this file and I don't want to invert their style over to the verbose one. Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
There's a lot of churn but that's unavoidable in such a move. Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
The raw environment is necessary only for executing programs. We can remove the type and replace it with []string in tests (where it has most of the usage) and a few call sites. The corresponding method from Environment was renamed to ForExec(). Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Some extra methods such as Get, Set and Delete are retained but they are all arguably unnecessarily and will be removed in another pass. Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
This includes Get, Delete and Contains but not Set. Set has extra semantics that will be removed in another pass. Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
This patch removes the leftover map-like method from the Environment type. Some extra care was applied to ensure we don't try to write through a nil map, something that Set protected against. Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
The main feature of what was called EnvironmentDelta is the fact it could expand expressions in variable definitions. This makes it more obvious. Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
There's a lot of extra changes to make the journey more complete. First of all both snap.AppInfo and snap.HookInfo provide a method EnvStack() []osutil.ExpandableEnv which replaces the earlier EnvironmentOverrides. Instead of merging environment entries the entire chain is modeled. This will be relevant when we want to expand values. This also means that support code to make EnvStack mutable is gone. Second of all, snap/snapenv.ExecEnv has reverted to using plain Environment (aka map[string]string) for basicEnv and userEnv. The values never had expressions so using the more sophisticated code there is not really necessary. Delta from master could be reduced further but I went for clarity of the progression over the clarity of the end-to-end diff. Lastly, a small but important change, snap-exec now expands each ExpandableEnv in sequence. First for the snap-level and then for the app or hook level. This should fix the issue that each level wanted to expand but not completely erase the value set previously. Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
With the current type it's not any better than just defining a map locally. Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
It is only required for OSEnvironment and is not used anywhere outside of the package. Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
When initially implemented as a part of this branch, SetExpandableEnv (nee ApplyDelta) behaved in somewhat unusual and "strong" way, where forward references _were_ expanded. This was not previously so and arguably is not needed. As the code stands now, we have proper chaining starting from system environment, to snap-level environment declarations (that can expand variables) and all the way down to application or hook-level environment declarations (also with variable expansions). Since such declarations are ordered this provides the equivalence of shell execution performing the same expansions. Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Instead of offering specialized environment transformation functions offer just the things we need: escaping and un-escaping of unsafe variables removed by the dynamic linker when loading binaries with AT_SECURE flag. Various bits move from snap/snapenv to osutil. Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
@mvo5 squash merging this will make my weekly commit quota look terrible ;-) |
Codecov Report
@@ Coverage Diff @@
## master #8242 +/- ##
=========================================
- Coverage 80.74% 80.65% -0.1%
=========================================
Files 716 717 +1
Lines 57123 57254 +131
=========================================
+ Hits 46124 46177 +53
- Misses 7410 7481 +71
- Partials 3589 3596 +7
Continue to review full report at Codecov.
|
@zyga fair enough - without squash there is no way for this to go into 2.44 though |
@zyga well, no way is too strong :) I will not do a backport PR but if you want you can |
@mvo5 I think it's not going into 2.44 - mainly because we want to not rush it. |
@mvo5 I'm going to review this now given that the discussions are fresh on my mind |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
did a pass, looking good, I can address most comments myself later today
cmd/snap-exec/main.go
Outdated
if err != nil { | ||
return err | ||
} | ||
// NOTE: we do not do env.UnescapeSaved() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is probably not intended, otoh given where hooks are run from for now, it's not material, symmmetry is probably preferable though
osutil/env.go
Outdated
// Environment is modified in place. Each variable defined by expandable | ||
// environment is expanded according to os.Expand, using the environment itself | ||
// as data source. Undefined variables expand to an empty string. | ||
func (env *Environment) SetExpandableEnv(eenv ExpandableEnv) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Set" is slightly odd because usually refers to setting "single" values. I will rename to "ExtendWithExpanded", bit of a Smalltalk-like naming.
// as data source. Undefined variables expand to an empty string. | ||
func (env *Environment) SetExpandableEnv(eenv ExpandableEnv) { | ||
if *env == nil { | ||
*env = make(Environment) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is not tested.
osutil/env.go
Outdated
|
||
return out | ||
func (env Environment) EscapeUnsafeVariables() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer this to be folded into a mode of ForExec via flags.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also lost some logic to drop already escaped variables that is on master, to be fair it was untested there, but probably best to preserve it.
osutil/env.go
Outdated
} | ||
|
||
func (env Environment) UnescapeSaved() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this can probably be a 2nd variant of OSEnviroment
snap/info.go
Outdated
// IsService returns whether app represents a daemon/service. | ||
func (app *AppInfo) IsService() bool { | ||
return app.Daemon != "" | ||
} | ||
|
||
func (app *AppInfo) EnvStack() []osutil.ExpandableEnv { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this lost the doc comment, also to keep the terminology smaller maybe we should call this EnvChain
snap/info.go
Outdated
env := []string{} | ||
for _, k := range envMap.Keys() { | ||
env = append(env, fmt.Sprintf("%s=%s", k, envMap.Get(k))) | ||
func (hook *HookInfo) EnvStack() []osutil.ExpandableEnv { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same
snap/snapenv/snapenv.go
Outdated
preserve = preserveUnsafeFlag | ||
func ExecEnv(info *snap.Info) (osutil.Environment, error) { | ||
// Start with OS environment. | ||
env, err := osutil.OSEnvironment() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this probably should change and not find the os enviroment on its own, tests would also be simpler
that was not possible before because the preserving logic was also here
also adjust doc comments and cover ExtendWithExpanded for the nil initial env case
support the escaping/unescaping that we use for preserving vars stripped out by ld.so for snap-confine, via variants of OSEnvironment/ForExec so that is happens at the process boundaries, loading os env and setting env for exec add/change some tests related to this adjust comments/doc comments or add
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, but one question.
// Environment can be manipulated with available methods and eventually | ||
// converted to low-level representation necessary when executing programs. | ||
// This approach discourages operations that could result in duplicate | ||
// environment variable definitions from being constructed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️
func parseEnvEntry(entry string) (string, string, error) { | ||
parts := strings.SplitN(entry, "=", 2) | ||
if len(parts) != 2 { | ||
return "", "", fmt.Errorf("cannot parse environment entry: %q", entry) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, a
FOO="A=B"
is a valid env value, but we will fail here; I'm not sure how likely it is to encounter it in practice; should we try to be smarter here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure? Why would we fail? We expect exactly 2 items and use SplitN
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, you're right, I missed this. Sorry about the noise ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Thank you for working on this @pedronis :)
This branch contains two patches: one adding three new types for working with
environment to strutil and another that uses them across the tree, making
correct environment usage easy and incorrect very hard.
The main observation is that environment is handled in one of two forms, as a
map of key=value entries and as a slice of strings that internally (hopefully)
contain key=value entries.
Technically environment is passed to a process in the form of slices. Our code
goes back and forth between the two representations and at one time appends two
slices together. This may, and indeed did, cause duplicate variable definitions
where one key has two successive values.
This is now discouraged by the use of the type system. Not to repeat the
details, I encourage you to read the commit message of both patches. There's
some small churn in snap/snapenv where maps turned to lists of arguments but
otherwise everything should be rather readable. I tried to remove all kinds of
duplicate implementations that turn maps to slices and back.
The branch can be naturally split into two patches. Let me know if this is
preferred.