Join GitHub today
GitHub is home to over 20 million developers working together to host and review code, manage projects, and build software together.
cmd/snap-update-ns: add C preamble for setns #3095
Conversation
| + } | ||
| + memset(buf, 0, buf_size); | ||
| + ssize_t num_read; | ||
| + if ((num_read = read(fd, buf, buf_size)) < 0) { |
niemeyer
Mar 28, 2017
Contributor
More readable to have the read outside the if. Then, ideally this would also check:
if (num_read < 0 || num_read == sizeof buf_size)
| + if (argv0_len == num_read) { | ||
| + return NULL; | ||
| + } | ||
| + return &buf[argv0_len + 1]; |
niemeyer
Mar 28, 2017
Contributor
If \0 is the last byte, then argv_len < num_read, yet argv0_len+1 is an overrun.
| + // Find the name of the snap by scanning the cmdline. If there's no snap | ||
| + // name given, just bail out. The go parts will scan this too. | ||
| + const char* snap_name = find_snap_name(cmdline, sizeof cmdline, num_read); | ||
| + if (*snap_name == '\0') { |
niemeyer
Mar 28, 2017
Contributor
find_snap_name returns NULL.
Slightly worried about the rate of important errors in this logic. Please make sure to get a good second review.
stolowski
requested changes
Mar 29, 2017
•
A caveat - I'm not familiar with namespaces nor with the "big picture" for the problem this branch solves, so cannot comment on the approach and overall semantics. The code looks good with some general comments:
- the read_cmdlne, find_snap_name, sanitize_snap_name should have tests (it may be neccessary to make /proc/../cmdline path mock-able/configurable for testing purposes).
- [paranoid mode] if we want to be really paranoid, then we should be checking all system calls for errors, including close()
- again, if we want to be really paranoid, we could cassert (just to be safe) on function arguments where it's critical for correct operation, such as making sure functions don't receive NULL for a buffer it operates on).
[/paranoid mode]
| + * | ||
| + */ | ||
| + | ||
| +#include "bootstrap.h" |
zyga
Mar 29, 2017
Contributor
Why not? This is standard practice to ensure that headers match implementation.
morphis
Mar 29, 2017
Contributor
Ah yeah, we're in C and not in C++, so makes more sense here to spot API/implementation differences.
| + * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
| + * | ||
| + */ | ||
| + |
morphis
Mar 29, 2017
Contributor
Please add proper header guards
#ifndef SNAP_UPDATE_NS_BOOTSTRAP_H_
#define SNAP_UPDATE_NS_BOOTSTRAP_H_
[...]
#endif
| + * | ||
| + */ | ||
| + | ||
| +extern int bootstrap_errno; |
morphis
Mar 29, 2017
Contributor
This one is only read from other code parts so a _get function is more what we want here.
zyga
Apr 3, 2017
Contributor
I decided not to, it's just more code that needs to be written where we want less code to exist.
| + */ | ||
| + | ||
| +extern int bootstrap_errno; | ||
| +extern const char* bootstrap_msg; |
morphis
Mar 29, 2017
Contributor
Would just make the API contract clear. We don't support write, just read.
|
I'm struggling to see how update-ns being in Go is really saving us on complexity and maintenance when I see comments like this:
which is making us have to write C code to do the OS-level operations that are required. |
|
In general, cmdline is something that is user-controlled. I understand that snapd is the caller of update-ns, but how are the arguments to update-ns created? Is it possible for a user to invoke a snap run command and manipulate cmdline? If so, this preamble code needs to be carefully written to handled untrusted input via cmdline. |
|
@jdstrand to reply to your concerns: The amount of system C code we need to write is minimal, it's really just one system call. The only complication is that we have no access to the command line in a convenient way and must execute this function before golang bootstrap begins, at which we no longer have one thread. This program is not setuid root and does not process user input in C. The golang parts, which are significantly harder to attack, process content generated by snapd (the mount profile) based on what two participating snaps declare. Having said that at this level there is no user input parsed. |
zyga
added some commits
Mar 28, 2017
mvo5
reviewed
Apr 3, 2017
Some questions and suggestions. Sorry that its slightly long :/
| + } | ||
| + memset(buf, 0, buf_size); | ||
| + ssize_t num_read = read(fd, buf, buf_size); | ||
| + if (num_read < 0 || num_read == buf_size) { |
mvo5
Apr 3, 2017
Collaborator
I wonder if it makes sense to have two different error messages here. It seems to me that num_read < 0 is a read releated error so that handling looks appropriate. However num_read == buf_size is a different kind of error, it means something like "internal error: buffer to read cmdline is too small" (right?). So maybe worth reporting separately.
mvo5
Apr 3, 2017
Collaborator
Actually - I'm slightly confused. In the case of num_read == buf_size the bootstrap_{errno,msg} are set. However in the later code its only an error if num_read is < 0 - is there code missing here that sets num_read = -1 if num_read == buf_size ? Or am I just misreading this code altogether :) ?
| + bootstrap_errno = errno; | ||
| + bootstrap_msg = "cannot read /proc/self/cmdline"; | ||
| + } | ||
| + close(fd); |
zyga
Apr 3, 2017
Contributor
In practice it is an useless error that should not be checked for or handled. See: https://lwn.net/Articles/576478/
| + | ||
| +// find_snap_name scans the command line buffer and looks for the 1st argument. | ||
| +const char* | ||
| +find_snap_name(char* buf, size_t buf_size, size_t num_read) |
mvo5
Apr 3, 2017
Collaborator
Silly(?) question - but why do we need both buf_size and num_read here? Isn't num_read always smaller than buf_size and sufficient for the size checks in here?
mvo5
Apr 3, 2017
Collaborator
Honest question, why is it simpler (and more correct) to use buf_size for the first check and num_read for the second? I mean, it seems checking for just num_read in the strnlen() is equally correct (and arguably simpler because when reading the code there is no need to ponder why in one place buf_size is used and in the other place num_read. Don't get me wrong, its fine, I'm just trying to understand the background a bit better.
zyga
Apr 3, 2017
Contributor
I think it is simpler because one argument is not a variable but a compile-time constant. I can fuse this into one variable if you want but I like the explicitness of "this is the size of the buffer" in C APIs.
niemeyer
Apr 6, 2017
Contributor
Having more logic to think about makes things more complex rather than simpler. If we should never go above the smaller number – and we shouldn't – there's no reason to have another number and make things any more complex.
Trivial and potentially serious (tomorrow) bug to introduce: invert the two parameters.
| @@ -42,13 +44,19 @@ func main() { | ||
| func parseArgs(args []string) error { | ||
| parser := flags.NewParser(&opts, flags.HelpFlag|flags.PassDoubleDash|flags.PassAfterNonOption) | ||
| _, err := parser.ParseArgs(args) | ||
| - return err | ||
| + if err != nil { |
mvo5
Apr 3, 2017
Collaborator
This could be written as a single line: if _, err := parser.ParseArgs(args); err != nil {...
| } | ||
| func run() error { | ||
| if err := parseArgs(os.Args[1:]); err != nil { | ||
| return err | ||
| } | ||
| - | ||
| + if err := bootstrapError(); err != nil { |
mvo5
Apr 3, 2017
Collaborator
Maybe a small comment here that that the bootstrap C code is automatically called and that we need to check errors for that here?
zyga
added some commits
Apr 3, 2017
|
I updated this, tweaking slightly how initialization is done so that we can test more code. I added unit tests for what is reasonable to test this way (skipping the actual setns as that will be covered by spread). I can add a smoke spread test that just checks we setns correctly and reach the "not implemented" trigger (I will do that in a moment) but I'd like to land this and iterate. |
| + bootstrap_msg = "cannot read /proc/self/cmdline"; | ||
| + } else if (num_read == buf_size) { | ||
| + bootstrap_errno = 0; | ||
| + bootstrap_msg = "cannot fit all of /proc/self/cmdline, buffer too small"; |
mvo5
Apr 3, 2017
Collaborator
I think this also needs to set num_read = -1 or else read_cmdline() will not fail in https://github.com/snapcore/snapd/pull/3095/files#diff-018c9af8a9874391aef7a2bfa0300d19R142 but carry on with an incorrect buffer.
| + | ||
| +// find_snap_name scans the command line buffer and looks for the 1st argument. | ||
| +const char* | ||
| +find_snap_name(char* buf, size_t buf_size, size_t num_read) |
mvo5
Apr 3, 2017
Collaborator
Silly(?) question - but why do we need both buf_size and num_read here? Isn't num_read always smaller than buf_size and sufficient for the size checks in here?
mvo5
Apr 3, 2017
Collaborator
Honest question, why is it simpler (and more correct) to use buf_size for the first check and num_read for the second? I mean, it seems checking for just num_read in the strnlen() is equally correct (and arguably simpler because when reading the code there is no need to ponder why in one place buf_size is used and in the other place num_read. Don't get me wrong, its fine, I'm just trying to understand the background a bit better.
zyga
Apr 3, 2017
Contributor
I think it is simpler because one argument is not a variable but a compile-time constant. I can fuse this into one variable if you want but I like the explicitness of "this is the size of the buffer" in C APIs.
niemeyer
Apr 6, 2017
Contributor
Having more logic to think about makes things more complex rather than simpler. If we should never go above the smaller number – and we shouldn't – there's no reason to have another number and make things any more complex.
Trivial and potentially serious (tomorrow) bug to introduce: invert the two parameters.
| +// on command line. | ||
| +void bootstrap(void) | ||
| +{ | ||
| +// NOTE: This lets use use cgo/go to write tests without running the bulk |
zyga
added some commits
Apr 3, 2017
stolowski
approved these changes
Apr 3, 2017
Looks good, thanks for the tests! Just one nitpick and one question, but not blockers.
| + | ||
| +// sanitize_snap_name performs partial validation of the given name. | ||
| +// The goal is to ensure that there are no / or .. in the name. | ||
| +int sanitize_snap_name(const char* snap_name) |
stolowski
Apr 3, 2017
Contributor
Nitpick: wouldn't "validate" be a better name since this method doesn't actually alter the name in any way?
zyga
Apr 3, 2017
Contributor
Yes, good point. How about partially_validate_snap_name (to make it clear it's not a full validation)?
| +// of the code automatically. In snapd we can just set the required | ||
| +// environment variable. | ||
| +#define TRIGGER_KEY "SNAPD_INTERNAL" | ||
| +#define TRIGGER_VAL "x-switch-namespace=1," |
stolowski
Apr 3, 2017
Contributor
Is x-... some form of new convention we want to introduce with this PR? Do we use it elsewhere in snapd? Looks a little verbose.
zyga
Apr 3, 2017
Contributor
Ha, I was just trying to be generic, theSNAPD_INTERNAL variable could hold a list of comma-separated key-value pairs. I'm fine with reworking this in any way that we find suitable.
stolowski
Apr 3, 2017
Contributor
I see. Ok, no arguing with that, just wanted to understand the reason ;)
niemeyer
Apr 6, 2017
Contributor
Something more straightforward would be nice indeed. How about
SNAPD_DEBUG_UPDATE_NS="skip-bootstrap"
pedronis
Apr 6, 2017
Contributor
why not just have a bit of C code that sets a global no_bootstrap flag, that is compiled in only when building tests?
zyga
added some commits
Apr 3, 2017
niemeyer
requested changes
Apr 6, 2017
It looks good. A few more comments, and probably the last ones.
| + | ||
| +// find_snap_name scans the command line buffer and looks for the 1st argument. | ||
| +const char* | ||
| +find_snap_name(char* buf, size_t buf_size, size_t num_read) |
mvo5
Apr 3, 2017
Collaborator
Silly(?) question - but why do we need both buf_size and num_read here? Isn't num_read always smaller than buf_size and sufficient for the size checks in here?
mvo5
Apr 3, 2017
Collaborator
Honest question, why is it simpler (and more correct) to use buf_size for the first check and num_read for the second? I mean, it seems checking for just num_read in the strnlen() is equally correct (and arguably simpler because when reading the code there is no need to ponder why in one place buf_size is used and in the other place num_read. Don't get me wrong, its fine, I'm just trying to understand the background a bit better.
zyga
Apr 3, 2017
Contributor
I think it is simpler because one argument is not a variable but a compile-time constant. I can fuse this into one variable if you want but I like the explicitness of "this is the size of the buffer" in C APIs.
niemeyer
Apr 6, 2017
Contributor
Having more logic to think about makes things more complex rather than simpler. If we should never go above the smaller number – and we shouldn't – there's no reason to have another number and make things any more complex.
Trivial and potentially serious (tomorrow) bug to introduce: invert the two parameters.
| +// of the code automatically. In snapd we can just set the required | ||
| +// environment variable. | ||
| +#define TRIGGER_KEY "SNAPD_INTERNAL" | ||
| +#define TRIGGER_VAL "x-switch-namespace=1," |
stolowski
Apr 3, 2017
Contributor
Is x-... some form of new convention we want to introduce with this PR? Do we use it elsewhere in snapd? Looks a little verbose.
zyga
Apr 3, 2017
Contributor
Ha, I was just trying to be generic, theSNAPD_INTERNAL variable could hold a list of comma-separated key-value pairs. I'm fine with reworking this in any way that we find suitable.
stolowski
Apr 3, 2017
Contributor
I see. Ok, no arguing with that, just wanted to understand the reason ;)
niemeyer
Apr 6, 2017
Contributor
Something more straightforward would be nice indeed. How about
SNAPD_DEBUG_UPDATE_NS="skip-bootstrap"
pedronis
Apr 6, 2017
Contributor
why not just have a bit of C code that sets a global no_bootstrap flag, that is compiled in only when building tests?
| +#define TRIGGER_KEY "SNAPD_INTERNAL" | ||
| +#define TRIGGER_VAL "x-switch-namespace=1," | ||
| + const char* snapd_internal = getenv(TRIGGER_KEY); | ||
| + if (snapd_internal == NULL || strstr(snapd_internal, TRIGGER_VAL) == NULL) { |
niemeyer
Apr 6, 2017
Contributor
The logic here looks reversed. Bootstraping is the normal case, so the variable should be used to disable it instead, right?
zyga
Apr 6, 2017
Contributor
The problem is that we cannot say "don't bootstrap" from go. By the time go runs it's too late. I think it has to stay reversed.
niemeyer
Apr 6, 2017
Contributor
We don't want update-ns to require setting a debug flag to work at all, in the normal case.
| + // Find the name of the snap by scanning the cmdline. If there's no snap | ||
| + // name given, just bail out. The go parts will scan this too. | ||
| + const char* snap_name = find_snap_name(cmdline, sizeof cmdline, num_read); | ||
| + if (snap_name == NULL || *snap_name == '\0') { |
niemeyer
Apr 6, 2017
Contributor
The second case would be more safely handled inside find_snap_name. If it got an empty string, it didn't actually find a snap name.
| + // Look for known offenders in the snap name (.. and /) and do nothing if | ||
| + // those are found. The golang code will validate snap name and print a | ||
| + // proper error message but this just ensures we don't try to open / setns | ||
| + // anything unusual. |
| +// findSnapName parses the argv-like array and finds the 1st argument. | ||
| +// Subsequent arguments are spearated by NUL-bytes. | ||
| +func findSnapName(buf []byte) *string { | ||
| + if ptr := C.find_snap_name((*C.char)(unsafe.Pointer(&buf[0])), C.size_t(cap(buf)), C.size_t(len(buf))); ptr != nil { |
niemeyer
Apr 6, 2017
Contributor
That doesn't look safe. That call is returning with ptr pointing to memory managed by Go's GC. If the memory moves, the function below will be accessing unknown memory. Instead, buf should be copied out into a malloc controlled buffer, defer free, and then passed into find_snap_name.
zyga
Apr 6, 2017
•
Contributor
On 2nd look I think this is correct:
The major change is the definition of rules for sharing Go pointers with C code, to ensure that such C code can coexist with Go's garbage collector. Briefly, Go and C may share memory allocated by Go when a pointer to that memory is passed to C as part of a cgo call, provided that the memory itself contains no pointers to Go-allocated memory, and provided that C does not retain the pointer after the call returns. These rules are checked by the runtime during program execution: if the runtime detects a violation, it prints a diagnosis and crashes the program. The checks can be disabled by setting the environment variable GODEBUG=cgocheck=0, but note that the vast majority of code identified by the checks is subtly incompatible with garbage collection in one way or another. Disabling the checks will typically only lead to more mysterious failure modes. Fixing the code in question should be strongly preferred over turning off the checks. See the cgo documentation for more details.
This is from cgo 1.6 release notes
niemeyer
Apr 6, 2017
Contributor
Sorry, I think you are right. I was looking at the result of the function as if it was data that wouldn't be moved, but it's already a pointer, so if there's a move the GC will take it along as well.
| +func findSnapName(buf []byte) *string { | ||
| + if ptr := C.find_snap_name((*C.char)(unsafe.Pointer(&buf[0])), C.size_t(cap(buf)), C.size_t(len(buf))); ptr != nil { | ||
| + str := C.GoString(ptr) | ||
| + return &str |
niemeyer
Apr 6, 2017
Contributor
*string is pretty unusual in Go. If we're wrapping, we can use Go's convention and return (string, error).
zyga
Apr 6, 2017
Contributor
(this is just for unit testing btw) I did it to be able to differentiate nil and "" as returned from find_snap_name. I wanted to avoid extra wrappers as the real intent is to test the C implementation, not provide a Go-like API for something that is not used in Go otherwise.
niemeyer
Apr 6, 2017
Contributor
You have extra wrappers already. It is Go code that is deciding when to return nil or &str.
|
Thank you for the review. I'll address the trivial bits ASAP but I think the fundamental question about how when to skip the bootstrap code needs to stay as-is. The problem is, as I mentioned above, that we cannot do anything from go to say "don't bootstrap" because the whole bootstrap code runs before any go starts. We need to not bootstrap by default and let snapd set something that would make the bootstrap happen. I made sure we don't miss this by always failing if the bootstrap code is skipped so that in production if we skip bootstrap (for whatever reason) we will fail without doing anything else. |
zyga
added some commits
Apr 6, 2017
|
Think outside the box. What hints you can use to decide not to bootstrap? |
|
@niemeyer I've updated the bootstrap logic to look at argv0 and skip bootstrap automatically if it ends with |
niemeyer
approved these changes
Apr 11, 2017
LGTM, thanks for the changes. One trivial for a follow up:
| + return NULL; | ||
| + } | ||
| + char* snap_name = &buf[argv0_len + 1]; | ||
| + if (snap_name != NULL && *snap_name == '\0') { |
zyga commentedMar 28, 2017
This patch adds a preamble/bootstrap code to snap-update-ns so that it
can call setns(..., CLONE_NEWNS) before golang starts to create threads.
The setns(2) manual page states that CLONE_NEWNS fails if there are any
threads present, apart from the main thread.
The preamble has no access to command line arguments so it works around
this by probing /proc/self/cmdline and parsing this manually.
Signed-off-by: Zygmunt Krynicki zygmunt.krynicki@canonical.com