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
Conversation
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.
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() |
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 looks suspicious, we should instead call context.Done() I think from some place higher even in this case
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.
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, |
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 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
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.
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", |
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 this would better happen before link-snap
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.
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", |
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 seems ok though might come a bit earlier but after unlink
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 (inside unlink). Please remember to clear the cache.
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.
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]; |
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.
Use PATH_MAX for this
die("SNAP_NAME is not set"); | ||
} | ||
|
||
must_snprintf(context_path, 1000, "%s/snap.%s", CONTEXTS_DIR, |
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.
Use sizeof context_path
for this
snap_name); | ||
|
||
int fd = open(context_path, O_RDONLY); | ||
if (fd < 0) { |
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.
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", |
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.
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) { |
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.
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); |
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.
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); |
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.
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); |
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.
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)); |
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.
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, |
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 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"); |
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 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 |
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.
Please merge with master and alter the (now single) Makefile.am
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 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", |
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, 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, |
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 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, |
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 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. |
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.
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" |
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.
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"); |
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.
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) { |
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.
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); |
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.
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); |
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 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); |
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.
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) | ||
} |
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 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 { |
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.
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() |
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 always set, so let's always do this instead of branching.
if c.task != nil { | ||
c.task.State().Unlock() | ||
} else { | ||
c.state.Unlock() |
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.
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) { |
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.
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 { |
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.
Similarly, we can make contextID an optional parameter on that one function. If empty, we set it.
} | ||
|
||
return &Context{ | ||
task: nil, |
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.
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 { | |||
|
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 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
.
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.
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), |
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 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() |
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.
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.
@stolowski This needs some love, when the current tasks are out of our pipeline. |
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. |
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).