cmd/snap-update-ns: add C preamble for setns #3095

Merged
merged 27 commits into from Apr 11, 2017
Commits
Jump to file or symbol
Failed to load files and symbols.
+482 −3
Split
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2017 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "bootstrap.h"
@morphis

morphis Mar 29, 2017

Contributor

Not really needed.

@zyga

zyga Mar 29, 2017

Contributor

Why not? This is standard practice to ensure that headers match implementation.

@morphis

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.

+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+// bootstrap_errno contains a copy of errno if a system call fails.
+int bootstrap_errno = 0;
+// bootstrap_msg contains a static string if something fails.
+const char* bootstrap_msg = NULL;
+
+// read_cmdline reads /proc/self/cmdline into the specified buffer, returning
+// number of bytes read.
+ssize_t read_cmdline(char* buf, size_t buf_size)
+{
+ int fd = open("/proc/self/cmdline", O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
+ if (fd < 0) {
+ bootstrap_errno = errno;
+ bootstrap_msg = "cannot open /proc/self/cmdline";
+ return -1;
+ }
+ memset(buf, 0, buf_size);
+ ssize_t num_read = read(fd, buf, buf_size);
+ if (num_read < 0) {
+ bootstrap_errno = errno;
+ 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

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.

+ num_read = -1;
+ }
+ close(fd);
+ return num_read;
+}
+
+// find_argv0 scans the command line buffer and looks for the 0st argument.
+const char*
+find_argv0(char* buf, size_t num_read)
+{
+ // cmdline is an array of NUL ('\0') separated strings.
+ size_t argv0_len = strnlen(buf, num_read);
+ if (argv0_len == num_read) {
+ // ensure that the buffer is properly terminated.
+ return NULL;
+ }
+ return buf;
+}
+
+// find_snap_name scans the command line buffer and looks for the 1st argument.
+// if the 1st argument exists but is empty NULL is returned.
+const char*
+find_snap_name(char* buf, size_t num_read)
+{
+ // cmdline is an array of NUL ('\0') separated strings. We can skip over
+ // the first entry (program name) and look at the second entry, in our case
+ // it should be the snap name.
+ size_t argv0_len = strnlen(buf, num_read);
+ if (argv0_len + 1 >= num_read) {
+ return NULL;
+ }
+ char* snap_name = &buf[argv0_len + 1];
+ if (*snap_name == '\0') {
+ return NULL;
+ }
+ return snap_name;
+}
+
+// setns_into_snap switches mount namespace into that of a given snap.
+static int
+setns_into_snap(const char* snap_name)
+{
+ // Construct the name of the .mnt file to open.
+ char buf[PATH_MAX];
+ int n = snprintf(buf, sizeof buf, "/run/snapd/ns/%s.mnt", snap_name);
+ if (n >= sizeof buf || n < 0) {
+ bootstrap_errno = errno;
+ bootstrap_msg = "cannot format mount namespace file name";
+ return -1;
+ }
+
+ // Open the mount namespace file, note that we don't specify O_NOFOLLOW as
+ // that file is always a special, broken symbolic link.
+ int fd = open(buf, O_RDONLY | O_CLOEXEC);
+ if (fd < 0) {
+ bootstrap_errno = errno;
+ bootstrap_msg = "cannot open mount namespace file";
+ return -1;
+ }
+
+ // Switch to the mount namespace of the given snap.
+ int err = setns(fd, CLONE_NEWNS);
+ if (err < 0) {
+ bootstrap_errno = errno;
+ bootstrap_msg = "cannot switch mount namespace";
+ };
+
+ close(fd);
+ return err;
+}
+
+// partially_validate_snap_name performs partial validation of the given name.
+// The goal is to ensure that there are no / or .. in the name.
+int partially_validate_snap_name(const char* snap_name)
+{
+ // NOTE: neither set bootstrap_{msg,errno} but the return value means that
+ // bootstrap does nothing. The name is re-validated by golang.
+ if (strstr(snap_name, "..") != NULL) {
+ return -1;
+ }
+ if (strchr(snap_name, '/') != NULL) {
+ return -1;
+ }
+}
+
+// bootstrap prepares snap-update-ns to work in the namespace of the snap given
+// on command line.
+void bootstrap(void)
+{
+ // We don't have argc/argv so let's imitate that by reading cmdline
+ char cmdline[1024];
+ memset(cmdline, 0, sizeof cmdline);
+ ssize_t num_read;
+ if ((num_read = read_cmdline(cmdline, sizeof cmdline)) < 0) {
+ return;
+ }
+
+ // Find the name of the called program. If it is ending with "-test" then do nothing.
+ // NOTE: This lets us use cgo/go to write tests without running the bulk
+ // of the code automatically. In snapd we can just set the required
+ // environment variable.
+ const char* argv0 = find_argv0(cmdline, (size_t)num_read);
+ if (argv0 == NULL) {
+ bootstrap_errno = 0;
+ bootstrap_msg = "argv0 is corrupted";
+ return;
+ }
+ const char* argv0_suffix_maybe = strstr(argv0, ".test");
+ if (argv0_suffix_maybe != NULL && argv0_suffix_maybe[strlen(".test")] == '\0') {
+ bootstrap_errno = 0;
+ bootstrap_msg = "bootstrap is not enabled while testing";
+ return;
+ }
+
+ // 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, (size_t)num_read);
+ if (snap_name == NULL) {
+ return;
+ }
+
+ // 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.
@niemeyer

niemeyer Apr 6, 2017

Contributor

Nice.

+ if (partially_validate_snap_name(snap_name) < 0) {
+ return;
+ }
+
+ // Switch the mount namespace to that of the snap given on command line.
+ if (setns_into_snap(snap_name) < 0) {
+ return;
+ }
+}
@@ -0,0 +1,85 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2017 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package main
+
+// Use a pre-main helper to switch the mount namespace. This is required as
+// golang creates threads at will and setns(..., CLONE_NEWNS) fails if any
+// threads apart from the main thread exist.
+
+/*
+
+#include <stdlib.h>
+#include "bootstrap.h"
+
+__attribute__((constructor)) static void init(void) {
+ bootstrap();
+}
+
+// NOTE: do not add anything before the following `import "C"'
+*/
+import "C"
+
+import (
+ "fmt"
+ "syscall"
+ "unsafe"
+)
+
+// Error returns error (if any) encountered in pre-main C code.
+func BootstrapError() error {
+ if C.bootstrap_msg == nil {
+ return nil
+ }
+ errno := syscall.Errno(C.bootstrap_errno)
+ if errno != 0 {
+ return fmt.Errorf("%s: %s", C.GoString(C.bootstrap_msg), errno)
+ }
+ return fmt.Errorf("%s", C.GoString(C.bootstrap_msg))
+}
+
+// readCmdline is a wrapper around the C function read_cmdline.
+func readCmdline(buf []byte) C.ssize_t {
+ return C.read_cmdline((*C.char)(unsafe.Pointer(&buf[0])), C.size_t(cap(buf)))
+}
+
+// findArgv0 parses the argv-like array and finds the 0st argument.
+func findArgv0(buf []byte) *string {
+ if ptr := C.find_argv0((*C.char)(unsafe.Pointer(&buf[0])), C.size_t(len(buf))); ptr != nil {
+ str := C.GoString(ptr)
+ return &str
+ }
+ return nil
+}
+
+// findSnapName parses the argv-like array and finds the 1st argument.
+func findSnapName(buf []byte) *string {
+ if ptr := C.find_snap_name((*C.char)(unsafe.Pointer(&buf[0])), C.size_t(len(buf))); ptr != nil {
+ str := C.GoString(ptr)
+ return &str
+ }
+ return nil
+}
+
+// partiallyValidateSnapName checks if snap name is seemingly valid.
+// The real part of the validation happens on the go side.
+func partiallyValidateSnapName(snapName string) int {
+ cStr := C.CString(snapName)
+ return int(C.partially_validate_snap_name(cStr))
+}
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
@morphis

morphis Mar 29, 2017

Contributor

Please add proper header guards

#ifndef SNAP_UPDATE_NS_BOOTSTRAP_H_
#define SNAP_UPDATE_NS_BOOTSTRAP_H_
[...]
#endif

@zyga

zyga Mar 29, 2017

Contributor

Ack, will do

@zyga

zyga Apr 3, 2017

Contributor

Done

+#ifndef SNAPD_CMD_SNAP_UPDATE_NS_H
+#define SNAPD_CMD_SNAP_UPDATE_NS_H
+
+#define _GNU_SOURCE
+
+#include <unistd.h>
+
+extern int bootstrap_errno;
+extern const char* bootstrap_msg;
+
+void bootstrap(void);
+ssize_t read_cmdline(char* buf, size_t buf_size);
+const char* find_argv0(char* buf, size_t num_read);
+const char* find_snap_name(char* buf, size_t num_read);
+int partially_validate_snap_name(const char* snap_name);
+
+#endif
Oops, something went wrong.