You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mesos.apache.org by gi...@apache.org on 2019/01/28 07:52:36 UTC

[mesos] 05/11: Added a parser for the Docker Seccomp config format.

This is an automated email from the ASF dual-hosted git repository.

gilbert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mesos.git

commit 12c4ebe1c67209c94afc243a2a5452933a888582
Author: Andrei Budnik <ab...@mesosphere.com>
AuthorDate: Sun Jan 27 23:51:59 2019 -0800

    Added a parser for the Docker Seccomp config format.
    
    Docker Seccomp config is a JSON file containing Seccomp filtering
    rules. This patch introduces a parser for Docker Seccomp config format.
    This parser accepts a JSON-string, parses and validates it, then
    returns a prepared `ContainerSeccompProfile` message.
    
    Review: https://reviews.apache.org/r/68019/
---
 src/CMakeLists.txt                   |   3 +-
 src/Makefile.am                      |   4 +-
 src/linux/seccomp/seccomp_parser.cpp | 518 +++++++++++++++++++++++++++++++++++
 src/linux/seccomp/seccomp_parser.hpp |  39 +++
 4 files changed, 562 insertions(+), 2 deletions(-)

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 9301490..7cd92fb 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -344,7 +344,8 @@ endif()
 
 if (ENABLE_SECCOMP_ISOLATOR)
   list(APPEND LINUX_SRC
-    linux/seccomp/seccomp.cpp)
+    linux/seccomp/seccomp.cpp
+    linux/seccomp/seccomp_parser.cpp)
 endif ()
 
 set(LOCAL_SRC
diff --git a/src/Makefile.am b/src/Makefile.am
index 0d1940f..2e263f6 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1424,7 +1424,9 @@ endif
 if ENABLE_SECCOMP_ISOLATOR
 MESOS_LINUX_FILES +=							\
   linux/seccomp/seccomp.cpp						\
-  linux/seccomp/seccomp.hpp
+  linux/seccomp/seccomp.hpp						\
+  linux/seccomp/seccomp_parser.cpp					\
+  linux/seccomp/seccomp_parser.hpp
 endif
 
 if ENABLE_LINUX_ROUTING
diff --git a/src/linux/seccomp/seccomp_parser.cpp b/src/linux/seccomp/seccomp_parser.cpp
new file mode 100644
index 0000000..3dcfcd6
--- /dev/null
+++ b/src/linux/seccomp/seccomp_parser.cpp
@@ -0,0 +1,518 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <string.h> // For strlen().
+
+#include <stout/json.hpp>
+#include <stout/nothing.hpp>
+#include <stout/strings.hpp>
+
+#include <stout/os/read.hpp>
+
+#include "linux/seccomp/seccomp.hpp"
+#include "linux/seccomp/seccomp_parser.hpp"
+
+using std::string;
+
+using process::Owned;
+
+using mesos::internal::seccomp::SeccompFilter;
+
+using mesos::seccomp::ContainerSeccompProfile;
+
+namespace mesos {
+namespace internal {
+namespace seccomp {
+
+constexpr char SCMP_PREFIX[] = "SCMP_";
+constexpr char CAP_PREFIX[] = "CAP_";
+
+
+Try<ContainerSeccompProfile::Syscall::Action> parseSyscallAction(
+    const string& value)
+{
+  ContainerSeccompProfile::Syscall::Action result;
+
+  if (!strings::startsWith(value, SCMP_PREFIX)) {
+    return Error("Unexpected syscall action: '" + value + "'");
+  }
+
+  if (!ContainerSeccompProfile::Syscall::Action_Parse(
+          string(value, strlen(SCMP_PREFIX)), &result)) {
+    return Error("Unknown syscall action: '" + value + "'");
+  }
+
+  return std::move(result);
+}
+
+
+Try<ContainerSeccompProfile::Architecture> parseArchitecture(
+    const string& value)
+{
+  ContainerSeccompProfile::Architecture result;
+
+  if (!strings::startsWith(value, SCMP_PREFIX)) {
+    return Error("Unexpected architecture: '" + value + "'");
+  }
+
+  if (!ContainerSeccompProfile::Architecture_Parse(
+          string(value, strlen(SCMP_PREFIX)), &result)) {
+    return Error("Unknown architecture: '" + value + "'");
+  }
+
+  return std::move(result);
+}
+
+
+Try<Nothing> parseDefaultAction(
+    const JSON::Object& json,
+    ContainerSeccompProfile* profile)
+{
+  Result<JSON::String> defaultAction = json.at<JSON::String>("defaultAction");
+  if (!defaultAction.isSome()) {
+    return Error(
+        "Cannot determine 'defaultAction' for the Seccomp configuration: " +
+        (defaultAction.isError() ? defaultAction.error() : "Not found"));
+  }
+
+  const auto defaultActionValue = parseSyscallAction(defaultAction->value);
+  if (defaultActionValue.isError()) {
+    return Error(defaultActionValue.error());
+  }
+
+  profile->set_default_action(defaultActionValue.get());
+
+  return Nothing();
+}
+
+
+Try<Nothing> parseArchMap(
+    const JSON::Object& json,
+    ContainerSeccompProfile* profile)
+{
+  Result<JSON::Array> archMap = json.at<JSON::Array>("archMap");
+  if (!archMap.isSome()) {
+    return Error(
+        "Cannot determine 'archMap' for the Seccomp configuration: " +
+        (archMap.isError() ? archMap.error() : "Not found"));
+  }
+
+  foreach (const JSON::Value& item, archMap->values) {
+    if (!item.is<JSON::Object>()) {
+      return Error("'archMap' contains a non-object item");
+    }
+
+    const auto architecture =
+      item.as<JSON::Object>().at<JSON::String>("architecture");
+
+    if (!architecture.isSome()) {
+      return Error(
+          "Cannot determine 'architecture' field for 'archMap' item: " +
+          (architecture.isError() ? architecture.error() : "Not found"));
+    }
+
+    const auto arch = parseArchitecture(architecture->value);
+    if (arch.isError()) {
+      return Error(arch.error());
+    }
+
+    Try<bool> nativeArch = SeccompFilter::nativeArch(arch.get());
+    if (nativeArch.isError()) {
+      return Error(nativeArch.error());
+    }
+
+    // We ignore architectures that doesn't match the native architecture of
+    // this host.
+    if (!nativeArch.get()) {
+      continue;
+    }
+
+    // Add this architecture with corresponding subarchitectures to the list of
+    // architectures of the profile.
+    const auto subArchitectures =
+      item.as<JSON::Object>().at<JSON::Array>("subArchitectures");
+
+    if (!subArchitectures.isSome()) {
+      return Error(
+          "Cannot determine 'subArchitectures' field for 'archMap' item: " +
+          (subArchitectures.isError() ? subArchitectures.error()
+                                      : "Not found"));
+    }
+
+    foreach (const JSON::Value& subItem, subArchitectures->values) {
+      if (!subItem.is<JSON::String>()) {
+        return Error("'subArchitectures' contains a non-string item");
+      }
+
+      const auto subArch = parseArchitecture(subItem.as<JSON::String>().value);
+      if (subArch.isError()) {
+        return Error(subArch.error());
+      }
+
+      profile->mutable_architectures()->Add(subArch.get());
+    }
+
+    profile->mutable_architectures()->Add(arch.get());
+  }
+
+  return Nothing();
+}
+
+
+Try<ContainerSeccompProfile::Syscall::Filter> parseSyscallFilter(
+    const JSON::Object& json,
+    bool* architectureMatched)
+{
+  ContainerSeccompProfile::Syscall::Filter result;
+
+  // Parse `arches` section which defines filtering rules by CPU architecture.
+  const auto arches = json.at<JSON::Array>("arches");
+  if (arches.isError()) {
+    return Error(arches.error());
+  }
+
+  if (arches.isSome()) {
+    static const hashmap<string, ContainerSeccompProfile::Architecture>
+      architectures({{"x86", ContainerSeccompProfile::ARCH_X86},
+                     {"amd64", ContainerSeccompProfile::ARCH_X86_64},
+                     {"x32", ContainerSeccompProfile::ARCH_X32},
+                     {"arm", ContainerSeccompProfile::ARCH_ARM},
+                     {"arm64", ContainerSeccompProfile::ARCH_AARCH64},
+                     {"mips", ContainerSeccompProfile::ARCH_MIPS},
+                     {"mipsel", ContainerSeccompProfile::ARCH_MIPSEL},
+                     {"mips64", ContainerSeccompProfile::ARCH_MIPS64},
+                     {"mipsel64", ContainerSeccompProfile::ARCH_MIPSEL64},
+                     {"mips64n32", ContainerSeccompProfile::ARCH_MIPS64N32},
+                     {"mipsel64n32", ContainerSeccompProfile::ARCH_MIPSEL64N32},
+                     {"ppc", ContainerSeccompProfile::ARCH_PPC},
+                     {"ppc64le", ContainerSeccompProfile::ARCH_PPC64LE},
+                     {"s390", ContainerSeccompProfile::ARCH_S390},
+                     {"s390x", ContainerSeccompProfile::ARCH_S390X}});
+
+    foreach (const JSON::Value& item, arches->values) {
+      if (!item.is<JSON::String>()) {
+        return Error("'arches' contains non-string item");
+      }
+
+      const auto arch = item.as<JSON::String>().value;
+      if (!architectures.contains(arch)) {
+        return Error("Unknown architecture: '" + arch + "'");
+      }
+
+      Try<bool> nativeArch =
+        SeccompFilter::nativeArch(architectures.get(arch).get());
+
+      if (nativeArch.isError()) {
+        return Error(nativeArch.error());
+      }
+
+      *architectureMatched = nativeArch.get();
+      if (*architectureMatched) {
+        break;
+      }
+    }
+  }
+
+  // Parse `caps` section which defines filtering rules by Linux capabilities.
+  const auto caps = json.at<JSON::Array>("caps");
+  if (caps.isError()) {
+    return Error(caps.error());
+  }
+
+  if (caps.isSome()) {
+    foreach (const JSON::Value& item, caps->values) {
+      if (!item.is<JSON::String>()) {
+        return Error("'caps' contains non-string item");
+      }
+
+      const auto cap = item.as<JSON::String>().value;
+
+      if (!strings::startsWith(cap, CAP_PREFIX)) {
+        return Error("Unexpected capability: '" + cap + "'");
+      }
+
+      CapabilityInfo::Capability capability;
+
+      if (!CapabilityInfo::Capability_Parse(
+              string(cap, strlen(CAP_PREFIX)), &capability)) {
+        return Error("Unknown capability: '" + cap + "'");
+      }
+
+      result.mutable_capabilities()->Add(capability);
+    }
+  }
+
+  return std::move(result);
+}
+
+
+Try<Nothing> parseSyscallArgument(
+  const JSON::Object& json,
+  ContainerSeccompProfile::Syscall::Arg* arg)
+{
+  auto parseField = [&json](const string& field) -> Try<JSON::Number> {
+    const auto number = json.at<JSON::Number>(field);
+    if (!number.isSome()) {
+      return Error(
+          "Cannot determine '" + field + "' field for 'args' item: " +
+          (number.isError() ? number.error() : "Not found"));
+    }
+
+    return number.get();
+  };
+
+  const auto index = parseField("index");
+  if (index.isError()) {
+    return Error(index.error());
+  }
+
+  arg->set_index(index->as<uint32_t>());
+
+  const auto value = parseField("value");
+  if (value.isError()) {
+    return Error(value.error());
+  }
+
+  arg->set_value(value->as<uint64_t>());
+
+  const auto valueTwo = parseField("valueTwo");
+  if (valueTwo.isError()) {
+    return Error(valueTwo.error());
+  }
+
+  arg->set_value_two(valueTwo->as<uint64_t>());
+
+  const auto op = json.at<JSON::String>("op");
+  if (!op.isSome()) {
+    return Error(
+        "Cannot determine 'op' field for 'args' item: " +
+        (op.isError() ? op.error() : "Not found"));
+  }
+
+  if (!strings::startsWith(op->value, SCMP_PREFIX)) {
+    return Error("Unexpected operation: '" + op->value + "'");
+  }
+
+  ContainerSeccompProfile::Syscall::Arg::Operator opVal;
+
+  if (!ContainerSeccompProfile::Syscall::Arg::Operator_Parse(
+          string(op->value, strlen(SCMP_PREFIX)), &opVal)) {
+    return Error("Unknown operator: '" + op->value + "'");
+  }
+
+  arg->set_op(opVal);
+
+  return Nothing();
+}
+
+
+Try<Nothing> parseSyscalls(
+  const JSON::Object& json,
+  ContainerSeccompProfile* profile)
+{
+  Result<JSON::Array> syscalls = json.at<JSON::Array>("syscalls");
+  if (!syscalls.isSome()) {
+    return Error(
+        "Cannot determine 'syscalls' for the Seccomp configuration: " +
+        (syscalls.isError() ? syscalls.error() : "Not found"));
+  }
+
+  // Each item in `syscalls` section defines a seccomp filter for a subset
+  // of system calls.
+  foreach (const JSON::Value& item, syscalls->values) {
+    if (!item.is<JSON::Object>()) {
+      return Error("'syscalls' contains a non-object item");
+    }
+
+    ContainerSeccompProfile::Syscall syscall;
+
+    // Both `includes` and `excludes` sections define rules for filtering out
+    // this seccomp filter. We omit this seccomp rule in two cases:
+    //   1. `excludes` rule is matched.
+    //   2. `includes` rule is not matched.
+    // Currently, we support filtering by Linux capabilities and by CPU
+    // architecture. We do filtering by CPU architecture here, while we postpone
+    // filtering by Linux capabilities until starting a container via the Linux
+    // launcher. We can't filter by Linux capabilities here, because the list of
+    // capabilities is unknown at the moment the `linux/seccomp` isolator parses
+    // the seccomp profile.
+
+    // Parse `includes` section.
+    const auto includes = item.as<JSON::Object>().at<JSON::Object>("includes");
+    if (!includes.isSome()) {
+      return Error(
+          "Cannot determine 'includes' field for 'syscalls' item: " +
+          (includes.isError() ? includes.error() : "Not found"));
+    }
+
+    bool architectureMatched = false;
+    const auto includesFilter =
+      parseSyscallFilter(includes.get(), &architectureMatched);
+
+    if (includesFilter.isError()) {
+      return Error(includesFilter.error());
+    }
+
+    if (includes->values.count("arches") && !architectureMatched) {
+      continue;
+    }
+
+    if (includes->values.count("caps")) {
+      syscall.mutable_includes()->CopyFrom(includesFilter.get());
+    }
+
+    // Parse `excludes` section.
+    const auto excludes = item.as<JSON::Object>().at<JSON::Object>("excludes");
+    if (!excludes.isSome()) {
+      return Error(
+          "Cannot determine 'excludes' field for 'syscalls' item: " +
+          (excludes.isError() ? excludes.error() : "Not found"));
+    }
+
+    architectureMatched = false;
+    const auto excludesFilter =
+      parseSyscallFilter(excludes.get(), &architectureMatched);
+
+    if (excludesFilter.isError()) {
+      return Error(excludesFilter.error());
+    }
+
+    if (excludes->values.count("arches") && architectureMatched) {
+      continue;
+    }
+
+    if (excludes->values.count("caps")) {
+      syscall.mutable_excludes()->CopyFrom(excludesFilter.get());
+    }
+
+    // Parse `names` section which contains a list of syscalls.
+    const auto names = item.as<JSON::Object>().at<JSON::Array>("names");
+    if (!names.isSome()) {
+      return Error(
+          "Cannot determine 'names' field for 'syscalls' item: " +
+          (names.isError() ? names.error() : "Not found"));
+    }
+
+    foreach (const JSON::Value& namesItem, names->values) {
+      if (!namesItem.is<JSON::String>()) {
+        return Error("'names' contains a non-string item");
+      }
+
+      syscall.add_names(namesItem.as<JSON::String>().value);
+    }
+
+    if (syscall.names().empty()) {
+      return Error("Syscall 'names' list is empty");
+    }
+
+    // Parse `action` section.
+    const auto action = item.as<JSON::Object>().at<JSON::String>("action");
+    if (!action.isSome()) {
+      return Error(
+          "Cannot determine 'action' for 'syscalls' item: " +
+          (action.isError() ? action.error() : "Not found"));
+    }
+
+    const auto actionValue = parseSyscallAction(action->value);
+    if (actionValue.isError()) {
+      return Error(actionValue.error());
+    }
+
+    syscall.set_action(actionValue.get());
+
+    // Parse `args` section which contains seccomp filtering rules for syscall
+    // arguments.
+    const auto args = item.as<JSON::Object>().at<JSON::Array>("args");
+    if (!args.isSome()) {
+      return Error(
+          "Cannot determine 'args' field for 'syscalls' item: " +
+          (args.isError() ? args.error() : "Not found"));
+    }
+
+    foreach (const JSON::Value& argsItem, args->values) {
+      if (!argsItem.is<JSON::Object>()) {
+        return Error("'names' contains a non-object item");
+      }
+
+      Try<Nothing> arg =
+        parseSyscallArgument(argsItem.as<JSON::Object>(), syscall.add_args());
+
+      if (arg.isError()) {
+        return Error(arg.error());
+      }
+    }
+
+    profile->add_syscalls()->CopyFrom(syscall);
+  }
+
+  return Nothing();
+}
+
+
+Try<ContainerSeccompProfile> parseProfileData(const string& data)
+{
+  Try<JSON::Object> json = JSON::parse<JSON::Object>(data);
+  if (json.isError()) {
+    return Error(json.error());
+  }
+
+  ContainerSeccompProfile profile;
+
+  Try<Nothing> defaultAction = parseDefaultAction(json.get(), &profile);
+  if (defaultAction.isError()) {
+    return Error(defaultAction.error());
+  }
+
+  Try<Nothing> archMap = parseArchMap(json.get(), &profile);
+  if (archMap.isError()) {
+    return Error(archMap.error());
+  }
+
+  Try<Nothing> syscalls = parseSyscalls(json.get(), &profile);
+  if (syscalls.isError()) {
+    return Error(syscalls.error());
+  }
+
+  // Verify Seccomp profile.
+  Try<Owned<SeccompFilter>> seccompFilter = SeccompFilter::create(profile);
+  if (seccompFilter.isError()) {
+    return Error(seccompFilter.error());
+  }
+
+  return std::move(profile);
+}
+
+
+Try<ContainerSeccompProfile> parseProfile(const string& path)
+{
+  Try<string> read = os::read(path);
+  if (read.isError()) {
+    return Error(
+        "Failed to read Seccomp profile file '" + path + "': " + read.error());
+  }
+
+  Try<ContainerSeccompProfile> profile = parseProfileData(read.get());
+  if (profile.isError()) {
+    return Error(
+        "Failed to parse Seccomp profile '" + path + "': " + profile.error());
+  }
+
+  return profile;
+}
+
+} // namespace seccomp {
+} // namespace internal {
+} // namespace mesos {
diff --git a/src/linux/seccomp/seccomp_parser.hpp b/src/linux/seccomp/seccomp_parser.hpp
new file mode 100644
index 0000000..5dfae0b
--- /dev/null
+++ b/src/linux/seccomp/seccomp_parser.hpp
@@ -0,0 +1,39 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef __LINUX_SECCOMP_PARSER_HPP__
+#define __LINUX_SECCOMP_PARSER_HPP__
+
+#include <mesos/seccomp/seccomp.hpp>
+
+#include <stout/try.hpp>
+
+namespace mesos {
+namespace internal {
+namespace seccomp {
+
+Try<mesos::seccomp::ContainerSeccompProfile> parseProfileData(
+    const std::string& data);
+
+
+Try<mesos::seccomp::ContainerSeccompProfile> parseProfile(
+    const std::string& path);
+
+} // namespace seccomp {
+} // namespace internal {
+} // namespace mesos {
+
+#endif // __LINUX_SECCOMP_PARSER_HPP__