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

snapctl: support running by the snap app itself, not only just from hooks #2644

Closed
wants to merge 29 commits into from

Conversation

stolowski
Copy link
Contributor

Support running snapctl outside of hooks by the apps from the snaps. This is realized by generating a unique SNAP_CONTEXT cookie-file on per-snap basis when the snap is installed, and passing it via the env in snap-confine.

TODO: support upgrades (generate context files for already existing snaps). Needs security review (design notes, in progress - CANNOT BE LANDED without this).

Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some first pass comments

@@ -79,6 +79,11 @@ func (s *setCommand) Execute(args []string) error {

transaction.Set(s.context().SnapName(), key, value)
}
context.Lock()
if context.IsSnapContext() {
transaction.Commit()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks suspicious, we should instead call context.Done() I think from some place higher even in this case

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, when the snapctl call comes in, we should test if IsEphemeral right before returning and call its Done method right there if that's the case. The *Context won't be saved anywhere, so it'll be garbage collected shortly after.

}

return &Context{
task: nil,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this means things like Context.Get/Set will explode, I wonder whether we should have a Context interface with the shared bits and two implementations SnapContext and TaskContext, hmm

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, but so far they still feel very close to each other for the splitting to pay off.

For Get and Set, we actually want them working. The ctlcmd code should not have to know whether the context is ephemeral or not before stashing data for recovering in Done, etc.

@@ -154,6 +154,7 @@ func verifyInstallUpdateTasks(c *C, opts, discards int, ts *state.TaskSet, st *s
"copy-snap-data",
"setup-profiles",
"link-snap",
"setup-snap-context",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would better happen before link-snap

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, we definitely need the file in place before the snap is considered linked. We can probably have it inside link itself, though.

@@ -790,6 +793,7 @@ func (s *snapmgrTestSuite) TestRemoveTasks(c *C) {
"discard-snap",
"clear-aliases",
"discard-conns",
"remove-snap-context",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems ok though might come a bit earlier but after unlink

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same (inside unlink). Please remember to clear the cache.

Copy link
Collaborator

@zyga zyga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for changing snap-confine. I've marked a few details I'd like to see changed before this lands. I'll review the rest later.


char *read_snap_context(const char *snap_name)
{
char context_path[1000];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use PATH_MAX for this

die("SNAP_NAME is not set");
}

must_snprintf(context_path, 1000, "%s/snap.%s", CONTEXTS_DIR,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use sizeof context_path for this

snap_name);

int fd = open(context_path, O_RDONLY);
if (fd < 0) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should die() here instead or rename this to _nonfatal and produce an error object.

int fd = open(context_path, O_RDONLY);
if (fd < 0) {
error
("Cannot open context file %s, SNAP_CONTEXT will not be set: %s",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the error messages are lower case.

if (context_val == NULL) {
die("Failed to allocate memory for snap context");
}
if (read(fd, context_val, 45) < 0) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The context must be a valid C string so the maximum size is actually one byte less. This also doesn't zero-terminate the value you've obtained.


if (snap_context != NULL) {
set_snap_context_env(snap_context);
free(snap_context);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this free and use a cleanup function instead.

must_snprintf(context_path, 1000, "%s/snap.%s", CONTEXTS_DIR,
snap_name);

int fd = open(context_path, O_RDONLY);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add O_NOFOLLOW | O_CLOEXEC here.

must_snprintf(context_path, 1000, "%s/snap.%s", CONTEXTS_DIR,
snap_name);

int fd = open(context_path, O_RDONLY);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please initialize this to -1 and use a cleanup function to close the file descriptor.

if (fd < 0) {
error
("Cannot open context file %s, SNAP_CONTEXT will not be set: %s",
context_path, strerror(errno));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use die or switch to errors, they handle strerror part internally.

If the design is to make the absence of context non-fatal then switch to errors and ensure the function has nonfatal in the name.

}
if (read(fd, context_val, 45) < 0) {
free(context_val);
error("Failed to read context file %s: %s", context_path,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comments about errors as above.

@@ -88,6 +89,13 @@ int main(int argc, char **argv)
die("need to run as root or suid");
}
#endif

char *snap_context = NULL;
const char *snap_name = getenv("SNAP_NAME");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to validate that snap name is not malicious here.

@@ -67,7 +67,9 @@ snap_confine_SOURCES = \
ns-support.c \
ns-support.h \
apparmor-support.c \
apparmor-support.h
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please merge with master and alter the (now single) Makefile.am

Copy link
Collaborator

@zyga zyga left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for all the changes to the C code, it looks much better. I left a few more suggestions, some simplifications and some correction for you to consider.

}

int fd __attribute__ ((cleanup(sc_cleanup_close))) = -1;
must_snprintf(context_path, sizeof(context_path), "%s/snap.%s",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick, move must_sprintf above the int fd declaration please.

fd = open(context_path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0) {
err =
sc_error_init(SC_CONTEXT_DOMAIN, 0,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can just use the errno domain. Check the documentation of the error system. Unless you need to switch/case on the error number the domain is useless. Using the errno domain also gives you free strerror handling.

free(context_val);
context_val = NULL;
err =
sc_error_init(SC_CONTEXT_DOMAIN, 0,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above. Just use the errno domain for this.

void sc_context_set_environment(const char *context)
{
if (context != NULL) {
// Overwrite context env value.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The discussion about context management for hooks vs snaps made me realize that you were right initially and we should preserve the hook-specific context.

NB: I feel that context overloading is somewhat confusing and we should use two variables for this. One will never be set by snap-confine and the other one will always be set by snap-confine.

/**
* Error domain for errors related to snap context handling.
**/
#define SC_CONTEXT_DOMAIN "context"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you agree with my suggestions and there's no code that needs to examine why context operation failed you can remove this.

@@ -88,6 +89,26 @@ int main(int argc, char **argv)
die("need to run as root or suid");
}
#endif

char *snap_context __attribute__ ((cleanup(sc_cleanup_string))) = NULL;
const char *snap_name = getenv("SNAP_NAME");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Can you find the other place that cares about SNAP_NAME and either also verify it there or obtain the value once and pass it around please.


char *snap_context __attribute__ ((cleanup(sc_cleanup_string))) = NULL;
const char *snap_name = getenv("SNAP_NAME");
if (snap_name != NULL) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SNAP_NAME cannot be NULL so I guess you can de-indent the whole part a little. If SNAP_NAME is NULL we should die as well.

@@ -154,7 +174,9 @@ int main(int argc, char **argv)
#if 0
setup_user_xdg_runtime_dir();
#endif

if (snap_context != NULL) {
sc_context_set_environment(snap_context);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you check for NULL in both places maybe just call this sc_maybe_set_context_environment() and allow it to be called with NULL.

int status = regexec(&re, name, 0, NULL, 0);
regfree(&re);

return (status == 0);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can drop the ( ), I think

@@ -21,5 +21,7 @@
#include <stdbool.h>

bool verify_security_tag(const char *security_tag);
bool verify_is_hook_security_tag(const char *security_tag);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

verify_is_hook_security_tag feels mis-named. Is it checking if a security tag is a hook security tag or checking if a hook security tag is valid? Please name it more appropriately.

_, err := rand.Read(idBytes)
if err != nil {
return nil, fmt.Errorf("cannot generate context ID: %s", err)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize this is copy & paste from the above, but we have a function to generate random yet readable IDs at strutil.MakeRandomString. Let's please use it here.

@@ -211,3 +252,8 @@ func (c *Context) Done() error {

return firstErr
}

// IsSnapContext returns true if the context is a snap context.
func (c *Context) IsSnapContext() bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's please rename this to IsEphemeral. More details on the naming below.

if c.task != nil {
c.task.State().Lock()
} else {
c.state.Lock()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is always set, so let's always do this instead of branching.

if c.task != nil {
c.task.State().Unlock()
} else {
c.state.Unlock()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same.

setup: setup,
id: base64.URLEncoding.EncodeToString(idBytes),
handler: handler,
cache: make(map[interface{}]interface{}),
}, nil
}

// NewSnapContext returns a new snap Context.
func NewSnapContext(state *state.State, setup *HookSetup, handler Handler) (*Context, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two functions are almost exactly the same. Let's please just teach the one above to take task == nil as well instead of adding a new one.

}

// NewSnapContextWithID returns a new snap Context with a predefined contextID (must be base64-encoded).
func NewSnapContextWithID(state *state.State, setup *HookSetup, handler Handler, contextID string) *Context {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, we can make contextID an optional parameter on that one function. If empty, we set it.

}

return &Context{
task: nil,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, but so far they still feel very close to each other for the splitting to pay off.

For Get and Set, we actually want them working. The ctlcmd code should not have to know whether the context is ephemeral or not before stashing data for recovering in Done, etc.

@@ -165,7 +206,7 @@ func (c *Context) Get(key string, value interface{}) error {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to make Get and Set work, as described above. If task == nil, we can work with a memory-only "hook-context" out of c.cache.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, not "hook-context".. that's awkward given the idea here. Maybe "ephemeral-context".

runner: runner,
repository: newRepository(),
contexts: make(map[string]*Context),
snapContexts: newSnapContexts(s),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually don't want these saved in memory persistently. Every time a snapctl call comes over, we need a new context which should live during that call only (thus ephemeral), otherwise two concurrent snapctl calls will corrupt each other's state. This is similar to how hooks work: two hooks, two contexts, even though the snap and the hook may be exactly the same.

@@ -79,6 +79,11 @@ func (s *setCommand) Execute(args []string) error {

transaction.Set(s.context().SnapName(), key, value)
}
context.Lock()
if context.IsSnapContext() {
transaction.Commit()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, when the snapctl call comes in, we should test if IsEphemeral right before returning and call its Done method right there if that's the case. The *Context won't be saved anywhere, so it'll be garbage collected shortly after.

@niemeyer
Copy link
Contributor

@stolowski This needs some love, when the current tasks are out of our pipeline.

@niemeyer
Copy link
Contributor

This is open for a very long time, has open reviews for a month, and is not being worked on. Closing until @stolowski can work on it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants