apparmor,release: add better apparmor detection/mocking code #3808

Merged
merged 12 commits into from Aug 26, 2017

Conversation

Projects
None yet
4 participants
Contributor

zyga commented Aug 25, 2017

This branch adds improved apparmor detection and mocking code. For
compatibility the existing code in the release package is left as a
pass-through to the new functionality.

The main improvement, apart from not being in the release module where it makes
little sense anymore (as it is no longer based on static inspection of the
distribution name) is that we can have varying levels of support.

Distributions that don't use apparmor, distributions that use upstream apparmor
and distributions that use ubuntu-patched apparmor are all detected separately.
Having this distinction will allow us to enable apparmor whenever it is
available and use graceful fallback when a specific feature is not available.

Building on this work apparmor will no longer have to be disabled on Debian and
in openSUSE. In addition as those distributions update to more recent kernels,
full confinement will kick-in automatically.

Signed-off-by: Zygmunt Krynicki me@zygoon.pl

interfaces/apparmor: add function for probing / mocking
This patch adds a new function for probing for supported apparmor
features as well as for mocking that. Unlike the current code in the
release module (where it doesn't really belong as it is no longer based
on looking at the /etc/os-release file).

The improvement from the old code is that the "feature level" is not a
boolean, there are three values and more may be added if desired. They
are "none" (apparmor not enabled at all), "partial" (apparmor enabled
but some features are missing) and "full" (all required features
availalble).

In the next few patches I will transition the old release interface over
to this and will start using it to make better decisions as to how
apparmor backend should be loaded and how it should operate.

Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>

Elsewhere we use dirs/dirs.go for this sort of thing.

I think using dirs/dirs.go will make the mocking a little easier.

Owner

zyga replied Aug 24, 2017

Ironically it's also a chore because of cyclic imports. I'm untangling that and I may end up being able to actually import dirs here as well. I'll try.

While not currently used in our policy, we should maybe list rlimit here for completeness. rlimit is an old feature that was added in apparmor 2.3 (the same time the /sys/kernel/security/apparmor/features was added). I confirmed that Ubuntu 12.04 3.2 kernels have it and unpatched 4.9 Debian has it.

Owner

zyga replied Aug 24, 2017

I'll add that! Thank you for noticing.

Owner

zyga replied Aug 24, 2017

Done

interfaces/apparmor: add "rlimit" to required features
Quoting Jamie:

    While not currently used in our policy, we should maybe list rlimit
    here for completeness. rlimit is an old feature that was added in
    apparmor 2.3 (the same time the
    /sys/kernel/security/apparmor/features was added). I confirmed that
    Ubuntu 12.04 3.2 kernels have it and unpatched 4.9 Debian has it.

Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>

It is probably sufficient to say if the features directory isn't present, then to return None, but aa_is_enabled (http://bazaar.launchpad.net/~apparmor-dev/apparmor/master/view/head:/libraries/libapparmor/src/kernel.c#L106) is a little more robust and might be useful for logging why we are returning None. Note that aa-enabled is a small C program wrapper around aa_is_enabled, so calling it directly from the core snap might be best.

Owner

zyga replied Aug 24, 2017

I've added logging (well, errors that can be logged). Looking at the referenced apparmor library code I can easily check for /sys/module/apparmor/parameters/enabled. I want to balance correctness and simplicity here and I'd rather not call into a C program to get the answer.

It might be nice to return the 'why' here for future logging. Eg:

    return None, fmt.Errof("...")
    return Partial, fmt.Errof("missing x, y, z")
    return Full, nil
Owner

zyga replied Aug 24, 2017

That's a good idea, I'll make that so

Owner

zyga replied Aug 24, 2017

Done

The direction of this commit is fine. Couple of small comments.

zyga added some commits Aug 24, 2017

interfaces/apparmor,apparmor: move apparmor to a new package
There's some generic apparmor code that is not tied to our interface
code that would be nice to import from other places but this cannot be
done because of cyclic imports.

This patch adds a new package and moves the probe code there.

Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
release: implement ForceDevMode based on apparmor.Probe()
This allows us to keep one copy of the relevant code (in the new apparmor
package) without a major redesign of the whole codebase. Subsequent
patches will replace particular calls into release.ForceDevMode to
apparmor.Probe and will handle the tri-state values.

Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
apparmor,release: return error details from apparmor.Probe
The additional error return value can be used to see which particular
feature is missing. This will be handy for logging.

Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
apparmor: use more realistic mock directory
The mock directory should be called "features" instead of "apparmor"
to better match the real error messages people would see.

Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
apparmor/probe.go
+
+// Probe checks which apparmor features are available.
+//
+// The error
@zyga

zyga Aug 25, 2017

Contributor

Ooops, what did I do here :)

zyga added some commits Aug 25, 2017

apparmor: complete documentation for Probe
Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
apparmor: indicate exactly which features are missing
In case when some apparmor features are missing we will no longer stop
on the first missing feature and instead collect them all for a more
meaningful error message.

Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>

codecov-io commented Aug 25, 2017

Codecov Report

Merging #3808 into master will increase coverage by 0.03%.
The diff coverage is 87.69%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #3808      +/-   ##
==========================================
+ Coverage   75.82%   75.85%   +0.03%     
==========================================
  Files         402      403       +1     
  Lines       34793    34835      +42     
==========================================
+ Hits        26381    26425      +44     
+ Misses       6539     6537       -2     
  Partials     1873     1873
Impacted Files Coverage Δ
release/release.go 100% <100%> (+10.81%) ⬆️
apparmor/probe.go 86.44% <86.44%> (ø)
interfaces/sorting.go 98.71% <0%> (-1.29%) ⬇️
wrappers/binaries.go 79.54% <0%> (+6.81%) ⬆️

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 0586e14...6fc7559. Read the comment docs.

@zyga zyga requested a review from mvo5 Aug 25, 2017

+1 but there are two questions inline.

I'll also note that since this code will support the possibility of booting in a Full kernel, rebooting into a Partial kernel, rebooting into a None kernel, then back into a Full kernel (and any combination), the apparmor_parser should handle this for us with how the apparmor unit calls it, since it will invalidate the cache if the version in the cache is different from the calculated version on the running system (the version is calculated in part based on the features of the kernel).

+
+var (
+ // featureSysPath points to the sysfs directory where apparmor features are listed.
+ featuresSysPath = "/sys/kernel/security/apparmor/features"
@jdstrand

jdstrand Aug 25, 2017

Contributor

Can you comment (not necessarily in the code, but in the PR) why you weren't able to use dirs/dirs.go?

@zyga

zyga Aug 25, 2017

Contributor

Because of recursive imports: apparmor->dirs->release->apparmor

release/release.go
- }
-
- return false
+ level, _ := apparmor.Probe()
@jdstrand

jdstrand Aug 25, 2017

Contributor

I'd like to see a prominent (ie,non-DEBUG) log message stating that snapd is going to force all snaps to be in devmode, with the reason why. Because you discard the error here, we can't do that.

Are you planning on doing this in a follow-up PR?

@zyga

zyga Aug 25, 2017

Contributor

I have this already in the branch that uses this. I think that once we get to the warnings framework it will be a proper user-visible message. EDIT: by user-visible I mean that this will be in the inbox until actually read/dismissed and a proper set of "you have an important message" notifications will be displayed.

Thanks for the branch. Some ideas/suggestions inline.

apparmor/probe.go
+//
+// The error is returned whenever less-than-full support is detected.
+func Probe() (FeatureLevel, error) {
+ _, err := os.Stat(featuresSysPath)
@mvo5

mvo5 Aug 25, 2017

Collaborator

This could be written in a single line: if _, err := ...; err != nil {

@zyga

zyga Aug 25, 2017

Contributor

Will do

@zyga

zyga Aug 25, 2017

Contributor

Done

apparmor/probe.go
+// The error is returned whenever less-than-full support is detected.
+func Probe() (FeatureLevel, error) {
+ _, err := os.Stat(featuresSysPath)
+ if err != nil {
@mvo5

mvo5 Aug 25, 2017

Collaborator

This feels strange from an API perspective. One could argue that there is no error here, the fact that there is no apparmor dir is not an error, its what is expected when there is no apparmor support. The api is also strange in that the FeatureLevel is actually valid even when an error is returned (which is not common at all in go code).

Maybe FeatureLevel can become a richer type? Something like:

type FeatureLevel struct {
   Summary string
}
type Full struct {
   FeatureLevel
}
type Partial struct {
    Featurelevel
}
type None struct {
   FeatureLevel
}
return Partial{Summary: fmt.Sprintf("apparmor features missing: %s", strings.Join(missing, ", "))

or similar? And only returning an error if there is an actual error condition (or not returning one at all as it seems we have no real errors right now in the code)?

@zyga

zyga Aug 25, 2017

Contributor

I agree about the error comment and I'll see what I can do to make this nicer. I really like the simplicity of the enum approach so far and would prefer not to cross YAGNI until I actually need it.

@zyga

zyga Aug 25, 2017

Contributor

I'm working on something nice, I'll push it shortly.

@zyga

zyga Aug 26, 2017

Contributor

Done! I re-factored the code a little and I think you will be happy with the result.

apparmor/probe.go
+ if err != nil {
+ panic(err)
+ }
+ fakeFeaturesSysPath := filepath.Join(temp, "features")
@mvo5

mvo5 Aug 25, 2017

Collaborator

Why not assign featureSysPath = filepath.Join(..) here already? The real featuresSysPath is stored so that should be fine, no?

@zyga

zyga Aug 25, 2017

Contributor

No reason, it's just lifted from the older Mock function this originated from. I'll improve this.

@zyga

zyga Aug 25, 2017

Contributor

Done

apparmor/probe.go
+ switch level {
+ case None:
+ // create no directory at all (apparmor not available).
+ break
@mvo5

mvo5 Aug 25, 2017

Collaborator

I don't think you don't need these "break"s, go has no automatic fallthrough.

@zyga

zyga Aug 25, 2017

Contributor

Right, this was just my C muscle memory. I'll clean this up.

@zyga

zyga Aug 25, 2017

Contributor

Done

zyga added some commits Aug 25, 2017

apparmor: combine error handling on one line
Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
apparmor: simplify mock code, remove variable
Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
apparmor: drop explicit break statements
Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>
apparmor,release: refactor apparmor probing / evaluation code
This patch is inspired by an idea from Michael Vogt. The probing code
now returns an apparmor.KernelSupport object which can be queried for
distinct facts: is apparmor enabled, is specific feature available.

The object can also be asked to evaluate overal support as required by
snapd.  This last operation matches the previous model where a tri-state
answer is provided (None, Partial, Full) as well as a textual summary
with more human-readable information ("this-and-that feature is
missing").

This will also allow specific interfaces to behave appropriately in
light of presence or absence of specific features.

Signed-off-by: Zygmunt Krynicki <me@zygoon.pl>

@zyga zyga merged commit 8c32b46 into snapcore:master Aug 26, 2017

7 checks passed

artful-amd64 autopkgtest finished (success)
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
xenial-amd64 autopkgtest finished (success)
Details
xenial-i386 autopkgtest finished (success)
Details
xenial-ppc64el autopkgtest finished (success)
Details
yakkety-amd64 autopkgtest finished (success)
Details
zesty-amd64 autopkgtest finished (success)
Details

@zyga zyga deleted the zyga:feature/better-apparmor-probing-mocking branch Aug 26, 2017

Some comments/suggestions inline. I would prefer waiting for two +1 for non-trivial PRs. I know we sometimes bend the rules, but usually it is for test-only PRs or trivial stuff (what is considered trivial is of course a matter of debatte :)

+}
+
+// ProbeKernel checks which apparmor features are available.
+func ProbeKernel() *KernelSupport {
@mvo5

mvo5 Aug 28, 2017

Collaborator

Why not just return the struct instead of a pointer to avoid the non-nil checks below?

@zyga

zyga Aug 28, 2017

Contributor

Just the feeling that structs containing arrays/slices should be passed by pointer, not by value.

+
+// IsEnabled returns true if apparmor is enabled.
+func (ks *KernelSupport) IsEnabled() bool {
+ return ks != nil && ks.enabled
@mvo5

mvo5 Aug 28, 2017

Collaborator

Under what circumstances would ks be nil? And why do we care here and not in any other place in the snapd code? I guess you want to be able to do something like https://play.golang.org/p/Z3OzKNPkm7 but it is not clear to me why we need it. Also, we could simply make it a non-pointer receiver to force a non-nil value here.

@zyga

zyga Aug 28, 2017

Contributor

I return nil when enabled == false, this is just a concept I learned from @chipaca recently, that you can make calls on nil receivers as long as the method handles that. If it is too magic I can change that to be more obvious / typical (no nil pointers )

@zyga

zyga Aug 28, 2017

Contributor

I return nil when enabled == false, this is just a concept I learned from @chipaca recently, that you can make calls on nil receivers as long as the method handles that. If it is too magic I can change that to be more obvious / typical (no nil pointers )

+
+// SupportsFeature returns true if a given apparmor feature is supported.
+func (ks *KernelSupport) SupportsFeature(feature string) bool {
+ return ks != nil && ks.features[feature]
@mvo5

mvo5 Aug 28, 2017

Collaborator

Same question here as above.

- }
-
- return false
+ level, _ := apparmor.ProbeKernel().Evaluate()
@mvo5

mvo5 Aug 28, 2017

Collaborator

If this is the API we use externally, we could consider to limit the API usage we expose in apparmor to exactly this. I.e. do not export probeKernel, do not return an error (that we ignore anyway) but instead just have "apparmor.Evaluate()" (or similar). YAGNI etc :)

@zyga

zyga Aug 28, 2017

Contributor

Ah, I plan to store the result of ProbeKernel (probably in the backend) and then use it in interface methods. This is just the first step towards that. Evaluate will be used alongside other tests, that check if a specific feature is on or off.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment