preserve TMPDIR and HOSTALIASES across snap-confine invocation (LP: #1682308) #3872

Merged
merged 17 commits into from Oct 18, 2017

Conversation

Projects
None yet
7 participants
Contributor

mwhudson commented Sep 7, 2017

Because snap-confine is setuid, the dynamic loader strips out certain
environment variables when starting the process. Work around this by renaming
them before invoking snap-confine and renaming them back in snap-exec.

preserve TMPDIR and HOSTALIASES across snap-confine invocation (LP: #…
…1682308)

Because snap-confine is setuid, the dynamic loader strips out certain
environment variables when starting the process. Work around this by renaming
them before invoking snap-confine and renaming them back in snap-exec.

@mwhudson mwhudson changed the title from preserve TMPDIR and HOSTALIASES across snap-confine invocation (LP: #… to preserve TMPDIR and HOSTALIASES across snap-confine invocation (LP: #1682308) Sep 7, 2017

Contributor

mwhudson commented Sep 7, 2017

I'm not sure but perhaps this is only something one would want for classic snaps?

codecov-io commented Sep 7, 2017

Codecov Report

Merging #3872 into master will decrease coverage by 0.07%.
The diff coverage is 80%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #3872      +/-   ##
==========================================
- Coverage    75.8%   75.72%   -0.08%     
==========================================
  Files         424      428       +4     
  Lines       36580    36689     +109     
==========================================
+ Hits        27729    27784      +55     
- Misses       6905     6951      +46     
- Partials     1946     1954       +8
Impacted Files Coverage Δ
cmd/snap-exec/main.go 66.15% <70%> (-0.24%) ⬇️
snap/snapenv/snapenv.go 95.12% <86.66%> (-1.98%) ⬇️
release/release.go 90.76% <0%> (-9.24%) ⬇️
cmd/snap/complete.go 50.7% <0%> (-6.5%) ⬇️
daemon/api.go 71.62% <0%> (-0.73%) ⬇️
snap/info.go 88.29% <0%> (-0.65%) ⬇️
interfaces/builtin/unity8.go 54.16% <0%> (ø) ⬆️
interfaces/builtin/system_observe.go 100% <0%> (ø) ⬆️
cmd/cmd.go 83.87% <0%> (ø) ⬆️
interfaces/builtin/mir.go 80% <0%> (ø) ⬆️
... and 14 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 5e9ea55...2ef8443. Read the comment docs.

Two questions, code looks good but not sure about what the motivation is and semantics should be exactly.

snap/snapenv/snapenv.go
-// envMap creates a map from the given environment string list, e.g. the
-// list returned from os.Environ()
+var unsafeEnv = map[string]bool{
+ "HOSTALIASES": true,
@zyga

zyga Sep 7, 2017

Contributor

If the user sets HOSTALIASES to a directory that is not readable due to the sandbox shoud this be still preserved?

snap/snapenv/snapenv.go
-// list returned from os.Environ()
+var unsafeEnv = map[string]bool{
+ "HOSTALIASES": true,
+ "TMPDIR": true,
@zyga

zyga Sep 7, 2017

Contributor

I'm not sure TMPDIR should be preserved. The designated directory may not exist inside the execution environment. What is your motivation for preserving it?

Contributor

mwhudson commented Sep 7, 2017

The motivation is that this keeps confusing users of my Go snap: https://bugs.launchpad.net/snapd/+bug/1682308

I think your questions indicate the answer to my question: this should only be done for a classic snap, and it turns out that that's really easy (pushed).

Here is the full list of variables glibc strips out: https://sourceware.org/git/?p=glibc.git;a=blob_plain;f=sysdeps/generic/unsecvars.h;hb=HEAD -- I guess it would make sense to save some more of these, maybe even all of them?

Member

chipaca commented Sep 12, 2017

LGTM, but, should we ask Jamie to review this?

Contributor

mwhudson commented Sep 13, 2017

I fixed the conflicts by amending the second commit to not touch the changelog (it was a mistake to commit it in the first place)

Idea looks good, as long as it's only for classic confinement indeed.

Just some details please:

snap/snapenv/snapenv.go
-func envMap(env []string) map[string]string {
+var unsafeEnv = map[string]bool{
+ "HOSTALIASES": true,
+ "TMPDIR": true,
@niemeyer

niemeyer Sep 13, 2017

Contributor

Sounds good to preserve all of them.

@mwhudson

mwhudson Sep 13, 2017

Contributor

Done

snap/snapenv/snapenv.go
+// rename some variables that will be stripped out by the dynamic
+// linker executing the setuid snap-confine by prepending their names
+// with PreservedUnsafePrefix.
+func envMap(env []string, preserveUnsafeVars bool) map[string]string {
envMap := map[string]string{}
for _, kv := range env {
@niemeyer

niemeyer Sep 13, 2017

Contributor
if strings.HasPrefix(kv, PreservedUnsafePrefix) {
        continue
}
@mwhudson

mwhudson Sep 13, 2017

Contributor

It took me a moment to realize why you wanted this so I added a comment too.

mwhudson added some commits Sep 13, 2017

snap/snapenv/snapenv.go
+// rename variables that will be stripped out by the dynamic linker
+// executing the setuid snap-confine by prepending their names with
+// PreservedUnsafePrefix.
+func envMap(env []string, preserveUnsafeVars bool) map[string]string {
@mvo5

mvo5 Sep 19, 2017

Collaborator

nitpick - I'm not a fan of this boolean here. It makes it hard to reason about what the function is doing without looking at the signature. I.e.: what does envMap(env, true) mean? I would prefer a flag argument and a name, maybe something like envMap(env, {0, preserveUnsafeVars]) ?

@mwhudson

mwhudson Sep 19, 2017

Contributor

I'm not 100% what you mean, but see what I pushed?

@mvo5

mvo5 Sep 20, 2017

Collaborator

Yeah, this is what I had in mind, thanks for this! Sorry that I was hard to parse.

mvo5 approved these changes Sep 20, 2017

Collaborator

mvo5 commented Sep 20, 2017

Code looks nice, thank you! I would love to see some sort of test for this, ideally both unit and spread. Please let us know if you would prefer help with this.

mwhudson added some commits Sep 20, 2017

Contributor

mwhudson commented Sep 20, 2017

I've pushed some unit tests and a spread test. I haven't run the spread test locally so if it doesn't work I guess I'd like some help with that :)

tests/main/confinement-classic/task.yaml
@@ -17,6 +17,8 @@ execute: |
run_install --classic
$SNAPMOUNTDIR/bin/test-snapd-hello-classic | MATCH 'Hello Classic!'
+ TMPDIR=/tmpdir $SNAPMOUNTDIR/bin/test-snapd-hello-classic t | MATCH TMPDIR=/tmpdir
@mwhudson

mwhudson Sep 21, 2017

Contributor

This fails with "/bin/bash: line 65: SNAPMOUNTDIR: unbound variable" which doesn't make any sense to me. Also the instructions in HACKING.md for running the spread tests don't work. Help?

@zyga

zyga Sep 22, 2017

Contributor

If you have the a key for linode API access (aka spread key) you can run this test with:

spread -debug -v linode:ubuntu-16.04-64:tests/main/confinement-classic

(substitute target environment if you want other OS)

I would also suggest to cut on the number of workers to just one (see spread.yaml) as otherwise debugging is weird and you waste machines that do nothing useful.

mwhudson added some commits Sep 29, 2017

tests/main/confinement-classic/task.yaml
@@ -21,7 +21,7 @@ execute: |
run_install --classic
$SNAP_MOUNT_DIR/bin/test-snapd-hello-classic | MATCH 'Hello Classic!'
- TMPDIR=/tmpdir $SNAPMOUNTDIR/bin/test-snapd-hello-classic t | MATCH TMPDIR=/tmpdir
+ TMPDIR=/tmpdir $SNAP_MOUNT_DIR/bin/test-snapd-hello-classic t | MATCH TMPDIR=/tmpdir
@zyga

zyga Sep 29, 2017

Contributor

Thank you

mwhudson added some commits Oct 3, 2017

Collaborator

mvo5 commented Oct 4, 2017

This fails in the unit tests:

coverage: 50.0% of statements
ok  	github.com/snapcore/snapd/snap/snapdir	0.004s
# github.com/snapcore/snapd/snap/snapenv
snap/snapenv/snapenv_test.go:195:9: undefined: val
snap/snapenv/snapenv_test.go:197:11: undefined: val

failure looks real.

zyga approved these changes Oct 5, 2017

LGTM, some comments inline for your consideration.

snap/snapenv/snapenv.go
+type preserveUnsafeEnvFlag int8
+
+const (
+ noPreserve preserveUnsafeEnvFlag = iota
@zyga

zyga Oct 5, 2017

Contributor

noPreserve and doPreserve differ by one character. With my ageing eyes I find it hard to distinguish easily. How about simply preserve and notPreserve (other ideas welcome)

snap/snapenv/snapenv.go
-func envMap(env []string) map[string]string {
+// Environment variables glibc strips out when running a setuid binary.
+// Taken from https://sourceware.org/git/?p=glibc.git;a=blob_plain;f=sysdeps/generic/unsecvars.h;hb=HEAD
+var unsafeEnv = map[string]bool{
@zyga

zyga Oct 5, 2017

Contributor

This would be a nice thing to go generate eventually. No need to do it now but maybe add a comment.

zyga added some commits Oct 5, 2017

snap/snapenv: rename flags for less ambiguity
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
snap/snapenv: add TODO note
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>

jdstrand requested changes Oct 5, 2017 edited

This looks ok to me overall, but please address my comment on renaming the unit tests. Also, while we test in spread that TMPDIR makes it through to the classic snap, we should also test that TMPDIR doesn't make it through for non-classic.

In terms of the approach, I think it is ok because the actual setuid binary (snap-confine) will still have these variables stripped (ie, we aren't adding to the attack surface for snap-confine). If the classic snap shipped a setuid binary, it would still be stripped at exec().

Furthermore, a classic snap is intended to work more with the system settings so preserving (at least some of these) variables for classic makes sense. That said, it seems to me since we are preserving the various LD_* variables, this PR may add confusion for users and/or may complicate matters related to https://forum.snapcraft.io/t/classic-snaps-failing-on-ubuntu-17-10/2324/7. I've not studied that issue closely so will not block on this point, but please consider it before merging.

For completeness and posterity, it is also worth noting that AppArmor looks at AT_SECURE with the profile transitions for Px, Cx and Ux. This PR will not affect that, since any exec transitions will still have the variables stripped at the time of the exec(). Specifically for snappy, classic snap policy intentionally has 'pix' for profile transitions (ie, to not strip these variables when the snap executes other binaries), and a non-snap application that has an apparmor profile that is executing a classic snap that has a Px/Cx/Ux rule for it will correctly still have these vars cleared on profile transition (just as the local admin specified).

snap/snapenv/snapenv_test.go
}
+}
+
+func (s *HTestSuite) TestExecEnvRenameTMPDIRForClassic(c *C) {
@jdstrand

jdstrand Oct 5, 2017

Contributor

This should be named TestExecEnvNoRenameTMPDIRForNonClassic() and the next test should be named TestExecEnvRenameTMPDIRForClassic(), no? (in this function you are mocking a non-classic snap and the next a classic snap)

Contributor

mwhudson commented Oct 8, 2017

This looks ok to me overall, but please address my comment on renaming the unit tests.

Ack.

Also, while we test in spread that TMPDIR makes it through to the classic snap, we should also test that TMPDIR doesn't make it through for non-classic.

OK, but I think I would also argue that if non-preservation of TMPDIR etc is important, we should probably filter them out ourselves (which would be easy to do in this branch, I guess) rather than relying on the dynamic loader doing it, as e.g. it doesn't look like musl does the same stripping or if for some reason we shipped a statically linked snap-confine there wouldn't be a dynamic loader at all.

Do you think I should change envMap to filter out "unsafe" variables in the non-classic case?

In terms of the approach, I think it is ok because the actual setuid binary (snap-confine) will still have these variables stripped (ie, we aren't adding to the attack surface for snap-confine). If the classic snap shipped a setuid binary, it would still be stripped at exec().

Furthermore, a classic snap is intended to work more with the system settings so preserving (at least some of these) variables for classic makes sense. That said, it seems to me since we are preserving the various LD_* variables, this PR may add confusion for users and/or may complicate matters related to https://forum.snapcraft.io/t/classic-snaps-failing-on-ubuntu-17-10/2324/7. I've not studied that issue closely so will not block on this point, but please consider it before merging.

Although the only variable my users actually seem to care about is TMPDIR, I think I would argue that not allowing these variables through is more confusing than blocking them. My sympathy for people who set LD_LIBRARY_PATH without knowing what they are doing is a bit limited.

For completeness and posterity, it is also worth noting that AppArmor looks at AT_SECURE with the profile transitions for Px, Cx and Ux. This PR will not affect that, since any exec transitions will still have the variables stripped at the time of the exec(). Specifically for snappy, classic snap policy intentionally has 'pix' for profile transitions (ie, to not strip these variables when the snap executes other binaries), and a non-snap application that has an apparmor profile that is executing a classic snap that has a Px/Cx/Ux rule for it will correctly still have these vars cleared on profile transition (just as the local admin specified).

mwhudson added some commits Oct 8, 2017

Contributor

mwhudson commented Oct 9, 2017

That said, it seems to me since we are preserving the various LD_* variables, this PR may add confusion for users and/or may complicate matters related to https://forum.snapcraft.io/t/classic-snaps-failing-on-ubuntu-17-10/2324/7. I've not studied that issue closely so will not block on this point, but please consider it before merging.

Although the only variable my users actually seem to care about is TMPDIR, I think I would argue that not allowing these variables through is more confusing than blocking them. My sympathy for people who set LD_LIBRARY_PATH without knowing what they are doing is a bit limited.

In addition, now that I've read that thread, the solution being proposed there in effect means that LD_LIBRARY_PATH will be ignored for binaries in classic snaps anyway.

Thanks for the changes.

@mvo5 mvo5 merged commit 9b9b6ca into snapcore:master Oct 18, 2017

6 of 7 checks passed

xenial-i386 autopkgtest finished (failure)
Details
artful-amd64 autopkgtest finished (success)
Details
artful-i386 autopkgtest finished (success)
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
xenial-amd64 autopkgtest finished (success)
Details
xenial-ppc64el autopkgtest finished (success)
Details
zesty-amd64 autopkgtest finished (success)
Details
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment