From 8bbccf0f3894bf46ba34d7010ced8b6f4c59121f Mon Sep 17 00:00:00 2001 From: Jon Henderson Date: Thu, 30 May 2024 09:48:59 +0000 Subject: [PATCH 1/3] Support for running Android containers - Manage mounting android partitions - OCI Template modifications for Android - Extra settings for Android specific bind mounts and devices --- CMakeLists.txt | 5 + bundle/lib/include/DobbyRootfs.h | 1 + bundle/lib/include/DobbySpecConfig.h | 8 + bundle/lib/source/DobbyRootfs.cpp | 49 +++- bundle/lib/source/DobbySpecConfig.cpp | 141 +++++++++- .../OciConfigJson1.0.2-dobby.template | 241 ++++++++++++++++-- bundle/runtime-schemas/defs-plugins.json | 37 +++ daemon/lib/source/Dobby.cpp | 2 +- settings/include/IDobbySettings.h | 49 ++++ settings/include/Settings.h | 11 + settings/source/Settings.cpp | 159 +++++++++++- utils/source/DobbyUtils.cpp | 36 ++- 12 files changed, 705 insertions(+), 34 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a4ce50c..510dbe2c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -296,6 +296,7 @@ option(PLUGIN_STORAGE "Include Storage plugin" ON) option(PLUGIN_MINIDUMP "Include Minidump plugin" ON) option(PLUGIN_THUNDER "Include Thunder plugin" ON) option(PLUGIN_OOMCRASH "Include OOMCrash plugin" ON) +option(PLUGIN_ANDROIDRUNTIME "Include Android plugin" ON) # Optional RDK plugins option(PLUGIN_TESTPLUGIN "Include TestPlugin plugin" OFF) @@ -373,6 +374,10 @@ if(PLUGIN_GAMEPAD) add_subdirectory(rdkPlugins/Gamepad) endif() +if(PLUGIN_ANDROIDRUNTIME) + add_subdirectory(rdkPlugins/AndroidRuntime) +endif() + # Export targets in Dobby package # --------------------------------------------------------- diff --git a/bundle/lib/include/DobbyRootfs.h b/bundle/lib/include/DobbyRootfs.h index c8a172ab..1582a4c1 100644 --- a/bundle/lib/include/DobbyRootfs.h +++ b/bundle/lib/include/DobbyRootfs.h @@ -100,6 +100,7 @@ class DobbyRootfs std::string mPath; int mDirFd; bool mPersist; + bool androidRootfs; }; diff --git a/bundle/lib/include/DobbySpecConfig.h b/bundle/lib/include/DobbySpecConfig.h index bc10574a..b595dcb9 100644 --- a/bundle/lib/include/DobbySpecConfig.h +++ b/bundle/lib/include/DobbySpecConfig.h @@ -109,6 +109,9 @@ class DobbySpecConfig : public DobbyConfig } MountPoint; std::vector mountPoints() const; +public: + bool androidEnabled() const; + public: const std::string& rootfsPath() const override; @@ -143,6 +146,7 @@ class DobbySpecConfig : public DobbyConfig JSON_FIELD_PROCESSOR(processDevices); JSON_FIELD_PROCESSOR(processCapabilities); JSON_FIELD_PROCESSOR(processSeccomp); + JSON_FIELD_PROCESSOR(processAndroid); #undef JSON_FIELD_PROCESSOR @@ -180,6 +184,7 @@ class DobbySpecConfig : public DobbyConfig const std::shared_ptr mUtilities; const std::shared_ptr mGpuSettings; const std::shared_ptr mVpuSettings; + const std::shared_ptr mAndroidSettings; const std::vector mDefaultPlugins; const Json::Value mRdkPluginsData; @@ -230,6 +235,9 @@ class DobbySpecConfig : public DobbyConfig std::string mEtcGroup; std::string mEtcLdSoPreload; +private: + bool mAndroidEnabled; + private: static int mNumCores; diff --git a/bundle/lib/source/DobbyRootfs.cpp b/bundle/lib/source/DobbyRootfs.cpp index d226dc8d..a82002a8 100644 --- a/bundle/lib/source/DobbyRootfs.cpp +++ b/bundle/lib/source/DobbyRootfs.cpp @@ -53,6 +53,7 @@ DobbyRootfs::DobbyRootfs(const std::shared_ptr& utils, , mBundle(bundle) , mDirFd(-1) , mPersist(false) + , androidRootfs(false) { AI_LOG_FN_ENTRY(); @@ -72,6 +73,12 @@ DobbyRootfs::DobbyRootfs(const std::shared_ptr& utils, return; } + if (config->androidEnabled()) + { + AI_LOG_WARN("Android container, using Android rootfs"); + androidRootfs = true; + } + // try and open the new directory mDirFd = openat(bundle->dirFd(), dirName.c_str(), O_CLOEXEC | O_DIRECTORY); if (mDirFd < 0) @@ -82,14 +89,41 @@ DobbyRootfs::DobbyRootfs(const std::shared_ptr& utils, return; } - // and finally construct the rootfs contents based on the config - if (!constructRootfs(mDirFd, config)) + if (!androidRootfs) { - AI_LOG_ERROR_EXIT("failed to construct bundle rootfs"); - cleanUp(); - return; + if (!constructRootfs(mDirFd, config)) + { + AI_LOG_ERROR_EXIT("failed to construct bundle rootfs"); + cleanUp(); + return; + } } + else + { + if (!createStandardMountPoints(mDirFd)) + { + AI_LOG_ERROR("Failed to create standard mount points"); + cleanUp(); + return; + } + + // process any extra mounts added by the client + const std::vector extraMounts = config->mountPoints(); + for (const DobbySpecConfig::MountPoint &mountPoint : extraMounts) + { + AI_LOG_DEBUG("attempting to create mount point '%s' %s", + mountPoint.destination.c_str(), + (mountPoint.type == DobbySpecConfig::MountPoint::Directory) ? + "directory" : "file"); + if (!createMountPoint(mDirFd, mountPoint.destination, + (mountPoint.type == DobbySpecConfig::MountPoint::Directory))) + { + AI_LOG_FN_EXIT(); + return; + } + } + } // store the complete path mPath = bundle->path() + "/" + dirName + "/"; @@ -113,6 +147,7 @@ DobbyRootfs::DobbyRootfs(const std::shared_ptr& utils, , mBundle(bundle) , mDirFd(-1) , mPersist(false) + , androidRootfs(false) { AI_LOG_FN_ENTRY(); @@ -282,7 +317,7 @@ void DobbyRootfs::cleanUp() if (mDirFd >= 0) { - if (!mPersist) + if (!mPersist && !androidRootfs) { if (!mUtilities->rmdirContents(mDirFd)) { @@ -296,7 +331,7 @@ void DobbyRootfs::cleanUp() } // the rootfs directory should now be empty, so can now delete it - if (!mPath.empty() && (rmdir(mPath.c_str()) != 0)) + if (!androidRootfs && !mPath.empty() && (rmdir(mPath.c_str()) != 0)) { AI_LOG_SYS_ERROR(errno, "failed to delete rootfs dir"); } diff --git a/bundle/lib/source/DobbySpecConfig.cpp b/bundle/lib/source/DobbySpecConfig.cpp index adeaae05..67b6b3e0 100644 --- a/bundle/lib/source/DobbySpecConfig.cpp +++ b/bundle/lib/source/DobbySpecConfig.cpp @@ -60,6 +60,11 @@ static const ctemplate::StaticTemplateString USERNS_ENABLED = static const ctemplate::StaticTemplateString USERNS_DISABLED = STS_INIT(USERNS_DISABLED, "USERNS_DISABLED"); +static const ctemplate::StaticTemplateString ANDROID_ENABLED = + STS_INIT(ANDROID_ENABLED, "ANDROID_ENABLED"); +static const ctemplate::StaticTemplateString ANDROID_DISABLED = + STS_INIT(ANDROID_DISABLED, "ANDROID_DISABLED"); + static const ctemplate::StaticTemplateString MEM_LIMIT = STS_INIT(MEM_LIMIT, "MEM_LIMIT"); @@ -187,15 +192,31 @@ static const ctemplate::StaticTemplateString SECCOMP_SYSCALLS = #define JSON_FLAG_FILECAPABILITIES (0x1U << 20) #define JSON_FLAG_VPU (0x1U << 21) #define JSON_FLAG_SECCOMP (0x1U << 22) +#define JSON_FLAG_ANDROID (0x1U << 23) int DobbySpecConfig::mNumCores = -1; // TODO: should we only allowed these if a network namespace is enabled ? const std::map DobbySpecConfig::mAllowedCaps = { + { "CAP_AUDIT_CONTROL", CAP_AUDIT_CONTROL }, + { "CAP_CHOWN", CAP_CHOWN }, + { "CAP_DAC_OVERRIDE", CAP_DAC_OVERRIDE }, + { "CAP_DAC_READ_SEARCH", CAP_DAC_READ_SEARCH }, + { "CAP_FOWNER", CAP_FOWNER }, + { "CAP_KILL", CAP_KILL }, + { "CAP_MKNOD", CAP_MKNOD }, + { "CAP_NET_ADMIN", CAP_NET_ADMIN }, { "CAP_NET_BIND_SERVICE", CAP_NET_BIND_SERVICE }, { "CAP_NET_BROADCAST", CAP_NET_BROADCAST }, { "CAP_NET_RAW", CAP_NET_RAW }, + { "CAP_SETGID", CAP_SETGID }, + { "CAP_SETPCAP", CAP_SETPCAP }, + { "CAP_SETUID", CAP_SETUID }, + { "CAP_SYSLOG", CAP_SYSLOG }, + { "CAP_SYS_ADMIN", CAP_SYS_ADMIN }, + { "CAP_SYS_PTRACE", CAP_SYS_PTRACE }, + { "CAP_SYS_RESOURCE", CAP_SYS_RESOURCE } }; // ----------------------------------------------------------------------------- @@ -216,6 +237,7 @@ DobbySpecConfig::DobbySpecConfig(const std::shared_ptr &utils, : mUtilities(utils) , mGpuSettings(settings->gpuAccessSettings()) , mVpuSettings(settings->vpuAccessSettings()) + , mAndroidSettings(settings->androidAccessSettings()) , mDefaultPlugins(settings->defaultPlugins()) , mRdkPluginsData(settings->rdkPluginsData()) , mDictionary(nullptr) @@ -229,6 +251,7 @@ DobbySpecConfig::DobbySpecConfig(const std::shared_ptr &utils, , mDebugDbus(IDobbyIPCUtils::BusType::NoneBus) , mConsoleDisabled(true) , mConsoleLimit(-1) + , mAndroidEnabled(false) , mRootfsPath("rootfs") { // get the number of (online) cpu cores on the system if we haven't already @@ -296,6 +319,7 @@ DobbySpecConfig::DobbySpecConfig(const std::shared_ptr &utils, : mUtilities(utils) , mGpuSettings(settings->gpuAccessSettings()) , mVpuSettings(settings->vpuAccessSettings()) + , mAndroidSettings(settings->androidAccessSettings()) , mDictionary(nullptr) , mConf(nullptr) , mSpecVersion(SpecVersion::Unknown) @@ -405,6 +429,11 @@ const std::string& DobbySpecConfig::consolePath() const return mConsolePath; } +bool DobbySpecConfig::androidEnabled() const +{ + return mAndroidEnabled; +} + const std::map& DobbySpecConfig::legacyPlugins() const { return mLegacyPlugins; @@ -479,6 +508,7 @@ bool DobbySpecConfig::parseSpec(ctemplate::TemplateDictionary* dictionary, static const ProcessorMap processors = { + { "android", { JSON_FLAG_ANDROID, &DobbySpecConfig::processAndroid } }, /* NOTE: process android before user */ { "env", { JSON_FLAG_ENV, &DobbySpecConfig::processEnv } }, { "args", { JSON_FLAG_ARGS, &DobbySpecConfig::processArgs } }, { "cwd", { JSON_FLAG_CWD, &DobbySpecConfig::processCwd } }, @@ -627,6 +657,11 @@ bool DobbySpecConfig::parseSpec(ctemplate::TemplateDictionary* dictionary, dictionary->SetValue(NO_NEW_PRIVS, "true"); } + if (!(flags & JSON_FLAG_ANDROID)) + { + dictionary->ShowSection(ANDROID_DISABLED); + } + // step 6 - enable the RDK plugins section dictionary->ShowSection(ENABLE_RDK_PLUGINS); @@ -829,9 +864,10 @@ bool DobbySpecConfig::processUser(const Json::Value& value, mUserId = uid.asUInt(); mGroupId = gid.asUInt(); + // sanity check the uid and gid are valid, and make sure we aren't being - // asked to start the container as root - if (mUserId == 0) + // asked to start the container as root unless it is Android + if ((mUserId == 0) && (!mAndroidEnabled)) { AI_LOG_ERROR("the user.uid cannot be root (0)"); return false; @@ -841,13 +877,112 @@ bool DobbySpecConfig::processUser(const Json::Value& value, AI_LOG_ERROR("invalid uid or gid field, values must be less than 65535"); return false; } - dictionary->SetIntValue(USER_ID, mUserId); dictionary->SetIntValue(GROUP_ID, mGroupId); return true; } +// ----------------------------------------------------------------------------- +/** + * @brief Processes the android field of the json spec + * + * Example json: + * + * "android": true + * "android": false + * + * This field controls whether to enable android customisations or not. + * + * @param[in] value The json spec document from the client + * @param[in] dictionary Pointer to the OCI dictionary to populate + * + * @return true if correctly processed the value, otherwise false. + */ +bool DobbySpecConfig::processAndroid(const Json::Value& value, + ctemplate::TemplateDictionary* dictionary) +{ + bool enabled; + mAndroidEnabled = false; + if (value.isBool()) + { + enabled = value.asBool(); + } + else if (value.isNull()) + { + enabled = false; + } + else + { + AI_LOG_ERROR("invalid android field"); + return false; + } + + mAndroidEnabled = enabled; + + AI_LOG_INFO("Android is %s", enabled ? "enabled" : "disabled"); + + dictionary->ShowSection(enabled ? ANDROID_ENABLED : ANDROID_DISABLED); + + if(!enabled) // No need for extra settings if Android disabled + { + return true; + } + + // add any extra mounts (ie ipc sockets, shared memory files, etc) + for (const auto& extraMount : mAndroidSettings->extraMounts) + { + ctemplate::TemplateDictionary *subDict = dictionary->AddSectionDictionary(MOUNT_SECTION); + subDict->SetValue(MOUNT_SRC, extraMount.source); + subDict->SetValue(MOUNT_DST, extraMount.target); + subDict->SetValue(MOUNT_TYPE, extraMount.type); + + for (const std::string& flag : extraMount.flags) + { + ctemplate::TemplateDictionary *optSubDict = subDict->AddSectionDictionary(MOUNT_OPT_SECTION); + optSubDict->SetValue(MOUNT_OPT, flag); + } + + // store the mount point for rootfs construction + storeMountPoint(extraMount.type, extraMount.source, extraMount.target); + } + + // device nodes should be static, so get the details once and store for + // all subsequent calls + static std::mutex scanningLock; + static std::atomic scannedDevNodes(false); + static std::list devNodes; + + if (!scannedDevNodes) + { + std::lock_guard locker(scanningLock); + + if (!scannedDevNodes) + { + for (const auto& devNode : mAndroidSettings->deviceNodes) + { + devNodes.emplace_back(DevNode{ devNode.path, + devNode.major, + devNode.minor, + devNode.filemode }); + } + scannedDevNodes = true; + } + } + + // add to the additional device node section + for (const DobbyConfig::DevNode &devNode : devNodes) + { + ctemplate::TemplateDictionary *subDict = dictionary->AddSectionDictionary(ADDITIONAL_DEVICE_NODES); + subDict->SetValue(DEVICE_PATH, devNode.path); + subDict->SetIntValue(DEVICE_MAJOR, devNode.major); + subDict->SetIntValue(DEVICE_MINOR, devNode.minor); + subDict->SetIntValue(DEVICE_FILE_MODE, devNode.mode); + } + + return true; +} + // ----------------------------------------------------------------------------- /** * @brief Processes the userNs field of the json spec diff --git a/bundle/lib/source/templates/OciConfigJson1.0.2-dobby.template b/bundle/lib/source/templates/OciConfigJson1.0.2-dobby.template index 9bd0afe2..a3099d74 100644 --- a/bundle/lib/source/templates/OciConfigJson1.0.2-dobby.template +++ b/bundle/lib/source/templates/OciConfigJson1.0.2-dobby.template @@ -27,8 +27,21 @@ static const char* ociJsonTemplate = R"JSON( "arch": "arm" }, - "process": { + {{#ANDROID_ENABLED}} + "annotations": { + "run.oci.hooks.stderr": "/dev/stderr", + "run.oci.hooks.stdout": "/dev/stdout" + }, + {{/ANDROID_ENABLED}} + + + "process": { + {{#ANDROID_DISABLED}} "terminal": false, + {{/ANDROID_DISABLED}} + {{#ANDROID_ENABLED}} + "terminal": true, + {{/ANDROID_ENABLED}} "user": { {{#USERNS_DISABLED}} "uid": {{USER_ID}}, @@ -41,16 +54,122 @@ static const char* ociJsonTemplate = R"JSON( "additionalGids": [ {{#ADDITIONAL_GIDS}} {{ADDITIONAL_GID}}{{#ADDITIONAL_GIDS_separator}},{{/ADDITIONAL_GIDS_separator}} {{/ADDITIONAL_GIDS}} ] }, "args": [ + {{#ANDROID_DISABLED}} "/usr/libexec/DobbyInit", {{#ARGS_VAR_SECTION}}"{{ARGS_VAR_VALUE:json_escape}}"{{#ARGS_VAR_SECTION_separator}},{{/ARGS_VAR_SECTION_separator}} {{/ARGS_VAR_SECTION}} ], + {{/ANDROID_DISABLED}} + {{#ANDROID_ENABLED}} + {{#ARGS_VAR_SECTION}}"{{ARGS_VAR_VALUE:json_escape}}"{{#ARGS_VAR_SECTION_separator}},{{/ARGS_VAR_SECTION_separator}} {{/ARGS_VAR_SECTION}} + ], + {{/ANDROID_ENABLED}} "cwd": "{{WORKING_DIRECTORY}}", + {{#ANDROID_DISABLED}} "env": [ {{#ENV_VAR_SECTION}}"{{ENV_VAR_VALUE:json_escape}}", {{/ENV_VAR_SECTION}} {{EXTRA_ENV_VARS}} "HOME=/home/private", "PATH=/usr/sbin:/usr/bin:/sbin:/bin" ], + {{/ANDROID_DISABLED}} + {{#ANDROID_ENABLED}} + "env": [ + {{#ENV_VAR_SECTION}}"{{ENV_VAR_VALUE:json_escape}}", {{/ENV_VAR_SECTION}} + {{EXTRA_ENV_VARS}} + "PATH=/system/bin:/" + ], + {{/ANDROID_ENABLED}} + {{#ANDROID_ENABLED}} + "capabilities": { + "bounding": [ + "CAP_AUDIT_CONTROL", + "CAP_CHOWN", + "CAP_DAC_OVERRIDE", + "CAP_DAC_READ_SEARCH", + "CAP_FOWNER", + "CAP_KILL", + "CAP_MKNOD", + "CAP_NET_ADMIN", + "CAP_SETGID", + "CAP_SETPCAP", + "CAP_SETUID", + "CAP_SYSLOG", + "CAP_SYS_ADMIN", + "CAP_SYS_PTRACE", + "CAP_SYS_RESOURCE" + {{#EXTRA_CAPS_SECTION}},"{{EXTRA_CAPS_VALUE}}"{{/EXTRA_CAPS_SECTION}} + ], + "effective": [ + "CAP_AUDIT_CONTROL", + "CAP_CHOWN", + "CAP_DAC_OVERRIDE", + "CAP_DAC_READ_SEARCH", + "CAP_FOWNER", + "CAP_KILL", + "CAP_MKNOD", + "CAP_NET_ADMIN", + "CAP_SETGID", + "CAP_SETPCAP", + "CAP_SETUID", + "CAP_SYSLOG", + "CAP_SYS_ADMIN", + "CAP_SYS_PTRACE", + "CAP_SYS_RESOURCE" + ], + "inheritable": [ + "CAP_AUDIT_CONTROL", + "CAP_CHOWN", + "CAP_DAC_OVERRIDE", + "CAP_DAC_READ_SEARCH", + "CAP_FOWNER", + "CAP_KILL", + "CAP_MKNOD", + "CAP_NET_ADMIN", + "CAP_SETGID", + "CAP_SETPCAP", + "CAP_SETUID", + "CAP_SYSLOG", + "CAP_SYS_ADMIN", + "CAP_SYS_PTRACE", + "CAP_SYS_RESOURCE" + ], + "permitted": [ + "CAP_AUDIT_CONTROL", + "CAP_CHOWN", + "CAP_DAC_OVERRIDE", + "CAP_DAC_READ_SEARCH", + "CAP_FOWNER", + "CAP_KILL", + "CAP_MKNOD", + "CAP_NET_ADMIN", + "CAP_SETGID", + "CAP_SETPCAP", + "CAP_SETUID", + "CAP_SYSLOG", + "CAP_SYS_ADMIN", + "CAP_SYS_PTRACE", + "CAP_SYS_RESOURCE" + ], + "ambient": [ + "CAP_AUDIT_CONTROL", + "CAP_CHOWN", + "CAP_DAC_OVERRIDE", + "CAP_DAC_READ_SEARCH", + "CAP_FOWNER", + "CAP_KILL", + "CAP_MKNOD", + "CAP_NET_ADMIN", + "CAP_SETGID", + "CAP_SETPCAP", + "CAP_SETUID", + "CAP_SYSLOG", + "CAP_SYS_ADMIN", + "CAP_SYS_PTRACE", + "CAP_SYS_RESOURCE" + ] + }, + {{/ANDROID_ENABLED}} + {{#ANDROID_DISABLED}} "capabilities": { "bounding": [ "CAP_AUDIT_WRITE", @@ -79,6 +198,8 @@ static const char* ociJsonTemplate = R"JSON( "CAP_NET_BIND_SERVICE" ] }, + {{/ANDROID_DISABLED}} + {{#ANDROID_DISABLED}} "rlimits": [ { "type": "RLIMIT_NOFILE", @@ -97,15 +218,26 @@ static const char* ociJsonTemplate = R"JSON( "soft": {{RLIMIT_RTPRIO}} }{{/RTLIMIT_ENABLED}} ], + {{/ANDROID_DISABLED}} "noNewPrivileges": {{NO_NEW_PRIVS}} }, + {{#ANDROID_DISABLED}} "root": { "path": "rootfs", "readonly": true }, "rootfsPropagation": "rprivate", + {{/ANDROID_DISABLED}} + + {{#ANDROID_ENABLED}} + "root": { + "path": "rootfs", + "readonly": false + }, + "rootfsPropagation": "rshared", + {{/ANDROID_ENABLED}} "hostname": "dobby", @@ -115,11 +247,16 @@ static const char* ociJsonTemplate = R"JSON( "type": "tmpfs", "source": "tmpfs", "options": [ + {{#ANDROID_DISABLED}} "nosuid", "noexec", "nodev", "size=65536k", "nr_inodes=8k" + {{/ANDROID_DISABLED}} + {{#ANDROID_ENABLED}} + "mode=0755" + {{/ANDROID_ENABLED}} ] }, { @@ -127,24 +264,18 @@ static const char* ociJsonTemplate = R"JSON( "type": "tmpfs", "source": "tmpfs", "options": [ + {{#ANDROID_DISABLED}} "nosuid", "noexec", "strictatime", "mode=755", "size=65536k" - ] - }, - { - "destination": "/dev/pts", - "type": "devpts", - "source": "devpts", - "options": [ - "nosuid", + {{/ANDROID_DISABLED}} + {{#ANDROID_ENABLED}} "noexec", - "newinstance", - "ptmxmode=0666", - "mode=0620", - "gid=5" + "strictatime", + "mode=755" + {{/ANDROID_ENABLED}} ] }, { @@ -166,8 +297,10 @@ static const char* ociJsonTemplate = R"JSON( "options": [ "nosuid", "noexec", - "nodev", - "ro" + {{#ANDROID_DISABLED}} + "ro", + {{/ANDROID_DISABLED}} + "nodev" ] }, { @@ -177,10 +310,18 @@ static const char* ociJsonTemplate = R"JSON( "options": [ "nosuid", "noexec", + {{#ANDROID_DISABLED}} "nodev", "relatime" + {{/ANDROID_DISABLED}} + {{#ANDROID_ENABLED}} + "none", + "name=", + "strictatime" + {{/ANDROID_ENABLED}} ] }, + {{#ANDROID_DISABLED}} { "destination": "/lib", "type": "bind", @@ -225,6 +366,26 @@ static const char* ociJsonTemplate = R"JSON( "ro" ] }, + {{/ANDROID_DISABLED}} + {{#ANDROID_DISABLED}} + { + "destination": "/proc", + "type": "proc", + "source": "proc", + "options": [ + "nosuid", + "noexec", + "nodev" + ] + }, + {{/ANDROID_DISABLED}} + {{#ANDROID_ENABLED}} {{! /proc before MOUNT_SECTION so we can override /proc/cmdline for Android }} + { + "destination": "/proc", + "type": "proc", + "source": "proc" + }, + {{/ANDROID_ENABLED}} {{#SYSLOG_SECTION}}{ "destination": "/dev/log", "type": "bind", @@ -244,13 +405,16 @@ static const char* ociJsonTemplate = R"JSON( ] },{{/MOUNT_SECTION}} { - "destination": "/proc", - "type": "proc", - "source": "proc", + "destination": "/dev/pts", + "type": "devpts", + "source": "devpts", "options": [ "nosuid", "noexec", - "nodev" + "newinstance", + "ptmxmode=0666", + "mode=0620", + "gid=5" ] } ], @@ -306,6 +470,7 @@ static const char* ociJsonTemplate = R"JSON( {{/ADDITIONAL_DEVICE_NODES}} ], "resources": { + {{#ANDROID_DISABLED}} "devices": [ { "allow": false, @@ -330,9 +495,22 @@ static const char* ociJsonTemplate = R"JSON( } {{/DEV_WHITELIST_SECTION}} ], + {{/ANDROID_DISABLED}} + {{#ANDROID_DISABLED}} "memory": { "limit": {{MEM_LIMIT}} }, + {{/ANDROID_DISABLED}} + {{#ANDROID_ENABLED}} + "memory": { + "kernel": -1, + "kernelTCP": -1, + "limit": {{MEM_LIMIT}}, + "swap": 1073741824, + "swappiness": 0, + "disableOOMKiller": false + }, + {{/ANDROID_ENABLED}} "cpu": { {{#CPU_SHARES_ENABLED}} "shares": {{CPU_SHARES_VALUE}}, @@ -345,6 +523,10 @@ static const char* ociJsonTemplate = R"JSON( } }, "namespaces": [ + {{#ANDROID_ENABLED}} + { + "type": "cgroup" + },{{/ANDROID_ENABLED}} { "type": "pid" },{{#NETNS_ENABLED}} @@ -364,6 +546,7 @@ static const char* ociJsonTemplate = R"JSON( "type": "mount" } ], + {{#ANDROID_DISABLED}} "maskedPaths": [ "/proc/kcore", "/proc/latency_stats", @@ -378,6 +561,26 @@ static const char* ociJsonTemplate = R"JSON( "/proc/sys", "/proc/sysrq-trigger" ] + {{/ANDROID_DISABLED}} + {{#ANDROID_ENABLED}} + "maskedPaths": [ + "/sys/block", + "/sys/bus/mmc", + "/sys/class/block", + "/sys/class/misc/binder", + "/sys/class/misc/hwbinder", + "/sys/class/misc/vndbinder", + "/sys/class/mmc_host", + "/sys/devices/platform/ffe05000.sdio", + "/sys/devices/platform/ffe07000.emmc", + "/sys/devices/virtual/misc/binder", + "/sys/devices/virtual/misc/hwbinder", + "/sys/devices/virtual/misc/vndbinder", + "/sys/firmware/devicetree/base/emmc@ffe07000", + "/sys/module/mmcblk", + "/sys/power" + ] + {{/ANDROID_ENABLED}} }, {{#ENABLE_LEGACY_PLUGINS}} "legacyPlugins": { diff --git a/bundle/runtime-schemas/defs-plugins.json b/bundle/runtime-schemas/defs-plugins.json index b93abbfb..d04f5b0e 100644 --- a/bundle/runtime-schemas/defs-plugins.json +++ b/bundle/runtime-schemas/defs-plugins.json @@ -654,6 +654,40 @@ } } }, + "AndroidRuntime": { + "type": "object", + "required": [ + "required" + ], + "properties": { + "required": { + "type": "boolean" + }, + "dependsOn": { + "$ref": "defs.json#/definitions/ArrayOfStrings" + }, + "data": { + "type": "object", + "properties": { + "systemPath": { + "type": "string" + }, + "vendorPath": { + "type": "string" + }, + "dataPath": { + "type": "string" + }, + "cachePath": { + "type": "string" + }, + "cmdlinePath": { + "type": "string" + } + } + } + } + }, "LegacyPlugins": { "type": "object", "properties": { @@ -715,6 +749,9 @@ }, "gamepad": { "$ref": "#/definitions/Gamepad" + }, + "androidruntime": { + "$ref": "#/definitions/AndroidRuntime" } } } diff --git a/daemon/lib/source/Dobby.cpp b/daemon/lib/source/Dobby.cpp index 7ccc9e15..f0ebb721 100644 --- a/daemon/lib/source/Dobby.cpp +++ b/daemon/lib/source/Dobby.cpp @@ -971,7 +971,7 @@ void Dobby::startFromSpec(std::shared_ptr replySender std::vector envVars; // The command argument might not be sent at all (e.g. from AI) so we should - // be able to withstand receiving 3 or 6 arguments + // be able to withstand receiving 3, 4 or 6 arguments bool parseArgsSuccess = false; if (replySender->getMethodCallArguments().size() == 3) { diff --git a/settings/include/IDobbySettings.h b/settings/include/IDobbySettings.h index e2afdb6a..d20d0a77 100644 --- a/settings/include/IDobbySettings.h +++ b/settings/include/IDobbySettings.h @@ -157,6 +157,55 @@ class IDobbySettings */ virtual std::shared_ptr vpuAccessSettings() const = 0; + /** + * Describes the details of any extra device nodes needed for Android Runtime. + * This is needed due to the need for specific major and minor numbers. + */ + struct AndroidDeviceNode + { + std::string path; + int major; + int minor; + int filemode; + }; + + // ------------------------------------------------------------------------- + /** + * Describes the details of anything extra needed to enable Android Runtime. + * + * - deviceNodes + * List of extra device nodes that need to be mapped into + * the container to allow the apps to use the H/W. + * - groupIds + * The group id that the app needs to be in to access the + * H/W device nodes. If not empty then the containered app will be + * in that supplementary group(s). + * - extraMounts + * The details of any additional mounts required to access + * the H/W. For example this is used on nexus platforms to map in + * the nexus server socket. This can also be used to map in + * extra files / sockets used by the software. + * - extraEnvVariables + * A list of extra environment variables that will be set for all + * containers if the given H/W access is requested. + * + */ + struct AndroidAccessSettings + { + std::list deviceNodes; + std::set groupIds; + std::list extraMounts; + std::map extraEnvVariables; + }; + + // ------------------------------------------------------------------------- + /** + * @brief Returns any extra details needed to access the VPU (video + * pipeline) inside the container. + * + */ + virtual std::shared_ptr androidAccessSettings() const = 0; + // ------------------------------------------------------------------------- /** * @brief Returns the set of external interface that container traffic diff --git a/settings/include/Settings.h b/settings/include/Settings.h index 075285ff..0aa06aea 100644 --- a/settings/include/Settings.h +++ b/settings/include/Settings.h @@ -68,6 +68,7 @@ class Settings final : public IDobbySettings public: std::shared_ptr gpuAccessSettings() const override; std::shared_ptr vpuAccessSettings() const override; + std::shared_ptr androidAccessSettings() const override; std::vector externalInterfaces() const override; std::string addressRangeStr() const override; @@ -110,6 +111,15 @@ class Settings final : public IDobbySettings std::shared_ptr getHardwareAccess(const Json::Value& root, const Json::Path& path) const; + std::list getAndroidNodes(const Json::Value& root, + const Json::Path& path) const; + + bool processAndroidDevice(const Json::Value& value, + AndroidDeviceNode* devNode) const; + + std::shared_ptr getAndroidAccess(const Json::Value& root, + const Json::Path& path) const; + void dumpHardwareAccess(int aiLogLevel, const std::string& name, const std::shared_ptr& hwAccess) const; @@ -122,6 +132,7 @@ class Settings final : public IDobbySettings std::shared_ptr mGpuHardwareAccess; std::shared_ptr mVpuHardwareAccess; + std::shared_ptr mAndroidHardwareAccess; std::vector mExternalInterfaces; std::pair mAddressRange; diff --git a/settings/source/Settings.cpp b/settings/source/Settings.cpp index 96081033..be670a97 100644 --- a/settings/source/Settings.cpp +++ b/settings/source/Settings.cpp @@ -164,6 +164,12 @@ Settings::Settings(const Json::Value& settings) getHardwareAccess(settings, Json::Path(".vpu")); } + // process the Android settings + { + mAndroidHardwareAccess = + getAndroidAccess(settings, Json::Path(".android")); + } + // process the network settings { Json::Value externalIfaces = Json::Path(".network.externalInterfaces").resolve(settings); @@ -473,6 +479,16 @@ std::shared_ptr Settings::vpuAccessSetti return mVpuHardwareAccess; } +// ----------------------------------------------------------------------------- +/** + * @brief + * + */ +std::shared_ptr Settings::androidAccessSettings() const +{ + return mAndroidHardwareAccess; +} + // ----------------------------------------------------------------------------- /** * @brief @@ -592,6 +608,7 @@ void Settings::dump(int aiLogLevel) const dumpHardwareAccess(aiLogLevel, "gpu", mGpuHardwareAccess); dumpHardwareAccess(aiLogLevel, "vpu", mVpuHardwareAccess); + //dumpHardwareAccess(aiLogLevel, "android", mAndroidHardwareAccess); } // ----------------------------------------------------------------------------- @@ -941,6 +958,66 @@ std::list Settings::getDevNodes(const Json::Value& root, return result; } +std::list Settings::getAndroidNodes(const Json::Value& root, + const Json::Path& path) const +{ + const Json::Value &devNodes = Json::Path(path).resolve(root); + if (devNodes.isNull()) + { + return std::list(); + } + else if (!devNodes.isArray()) + { + AI_LOG_ERROR("JSON value in settings file is not an array (of dev nodes)"); + return std::list(); + } + + std::list result; + for (const Json::Value &devNode : devNodes) + { + if (!devNode.isObject()) + { + AI_LOG_ERROR("invalid JSON value in dev nodes array in settings file"); + return std::list(); + } + + AndroidDeviceNode androidDevNode; + if (processAndroidDevice(devNode, &androidDevNode)) + { + result.emplace_back(androidDevNode); + } + } + + return result; +} + +bool Settings::processAndroidDevice(const Json::Value& value, AndroidDeviceNode* devNode) const +{ + const Json::Value& path = value["path"]; + const Json::Value& major = value["major"]; + const Json::Value& minor = value["minor"]; + const Json::Value& fileMode = value["fileMode"]; + + if (!path.isString()) + { + AI_LOG_ERROR("invalid 'path' JSON field"); + return false; + } + + if (!major.isInt() || !minor.isInt() || !fileMode.isInt()) + { + AI_LOG_ERROR("invalid 'major', 'minor' or 'fileMode' JSON field"); + return false; + } + + devNode->path = path.asString(); + devNode->major = major.asInt(); + devNode->minor = minor.asInt(); + devNode->filemode = fileMode.asInt(); + + return true; +} + // ----------------------------------------------------------------------------- /** * @brief Attempts to read the mount JSON structure(s) from the object. @@ -1022,7 +1099,8 @@ bool Settings::processMountObject(const Json::Value& value, ExtraMount* mount) c static const std::set mountFlags = { "rbind", "bind", "silent", "ro", "sync", "nosuid", "dirsync", - "nodiratime", "relatime", "noexec", "nodev", "noatime", "strictatime" + "nodiratime", "relatime", "noexec", "nodev", "noatime", "strictatime", + "mode=0755" }; // convert the mount flags @@ -1124,3 +1202,82 @@ std::shared_ptr Settings::getHardwareAcc return accessSettings; } + +// ----------------------------------------------------------------------------- +/** + * @brief Processes a json 'android' object. + * + * The JSON is expected to look like the following: + * + * { + * "groupIds": [ "video" ], + * "devNodes": [ + * { + * "path": "/dev/kmsg", + * "fileMode": 438, + * "major": 1, + * "minor": 11 + * }, + * "extraEnvVariables": [ + * "ENABLE_MEDIAINFO=0" + * ], + * "extraMounts": [ + * { + * "source": "/etc/xdg/gstomx.conf", + * "destination": "/etc/xdg/gstomx.conf", + * "type": "bind", + * "options": [ "bind", "ro", "nosuid", "nodev", "noexec" ] + * }, + * ... + * ] + * } + * + * @return + */ + +std::shared_ptr Settings::getAndroidAccess(const Json::Value& root, + const Json::Path& path) const +{ + auto accessSettings = + std::make_shared(); + + // get the 'android' object from the json + const Json::Value hw = Json::Path(path).resolve(root); + if (hw.isNull()) + { + // it's not an error if the value does not exist in the JSON + return accessSettings; + } + else if (!hw.isObject()) + { + // however it is an error if present but not a json object + AI_LOG_ERROR("invalid 'android' JSON field in dobby settings file"); + return accessSettings; + } + + + // get the group id(s) required + const Json::Value groupIds = hw["groupIds"]; + if (!groupIds.isNull()) + { + accessSettings->groupIds = getGroupIds(groupIds); + } + else + { + const Json::Value groupId = hw["groupId"]; + if (!groupId.isNull()) + accessSettings->groupIds = getGroupIds(groupId); + } + + // Nb: validation that the paths are actually dev nodes is done in the + // DobbyConfig code + accessSettings->deviceNodes = getAndroidNodes(hw, Json::Path(".devNodes")); + + // get any extra mounts + accessSettings->extraMounts = getExtraMounts(hw, Json::Path(".extraMounts")); + + // get any extra environment vars + accessSettings->extraEnvVariables = getEnvVarsFromJson(hw, Json::Path(".extraEnvVariables")); + + return accessSettings; +} diff --git a/utils/source/DobbyUtils.cpp b/utils/source/DobbyUtils.cpp index 4c803a3d..d5dd31bd 100644 --- a/utils/source/DobbyUtils.cpp +++ b/utils/source/DobbyUtils.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -242,9 +243,38 @@ bool DobbyUtils::deleteRecursive(int dirfd, int availDepth) // try unlinking the file / directory / symlink / whatever if (unlinkat(dirfd, entry->d_name, flags) != 0) { - AI_LOG_SYS_ERROR(errno, "failed to remove '%s'", entry->d_name); - success = false; - break; + if(errno == EBUSY) + { + AI_LOG_WARN("EBUSY whilst trying to remove '%s', try unmounting..", entry->d_name); + if (fchdir(dirfd) != 0) + { + AI_LOG_SYS_ERROR(errno, "failed to change to file directory"); + success = false; + break; + } + else if (umount2(entry->d_name, UMOUNT_NOFOLLOW) != 0) + { + AI_LOG_SYS_ERROR(errno, "failed to remove (unmount) '%s'", entry->d_name); + success = false; + break; + } + else if (unlinkat(dirfd, entry->d_name, flags) != 0) + { + AI_LOG_SYS_ERROR(errno, "failed to remove file after unmount '%s'", entry->d_name); + success = false; + break; + } + else + { + AI_LOG_INFO("unmounted and removed '%s'", entry->d_name); + } + } + else + { + AI_LOG_SYS_ERROR(errno, "failed to remove '%s'", entry->d_name); + success = false; + break; + } } } From d9a5f68d11a3858403f7e8869b099b5858d227be Mon Sep 17 00:00:00 2001 From: Jon Henderson Date: Thu, 6 Jun 2024 09:57:46 +0000 Subject: [PATCH 2/3] Add extra masked paths Signed-off-by: Jon Henderson --- .../lib/source/templates/OciConfigJson1.0.2-dobby.template | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bundle/lib/source/templates/OciConfigJson1.0.2-dobby.template b/bundle/lib/source/templates/OciConfigJson1.0.2-dobby.template index a3099d74..4393140f 100644 --- a/bundle/lib/source/templates/OciConfigJson1.0.2-dobby.template +++ b/bundle/lib/source/templates/OciConfigJson1.0.2-dobby.template @@ -578,7 +578,11 @@ static const char* ociJsonTemplate = R"JSON( "/sys/devices/virtual/misc/vndbinder", "/sys/firmware/devicetree/base/emmc@ffe07000", "/sys/module/mmcblk", - "/sys/power" + "/sys/power", + "/sys/devices/platform/rdb/84b1000.sdhci", + "/sys/devices/virtual/devlink/brcm_scmi@0--84b1000.sdhci", + "/sys/firmware/devicetree/base/rdb/sdhci@84b1000", + "/sys/firmware/devicetree/base/rdb/sdhci@84b0000" ] {{/ANDROID_ENABLED}} }, From 02c6c3848831e05b6a6f40db228c361c9810bf30 Mon Sep 17 00:00:00 2001 From: Jon Henderson Date: Wed, 12 Jun 2024 15:11:02 +0000 Subject: [PATCH 3/3] Add Android AppArmor profile setting Provide the ability to use an alternate AppArmor profile when running android containers, i.e. { "android": { "appArmorProfile": "aosp_default" } } If no setting is provided, the normal Dobby profile will be used. --- daemon/lib/source/DobbyManager.cpp | 9 ++++++- settings/include/IDobbySettings.h | 1 + settings/source/Settings.cpp | 38 ++++++++++++++++++++---------- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/daemon/lib/source/DobbyManager.cpp b/daemon/lib/source/DobbyManager.cpp index 272423f8..8a5326d4 100644 --- a/daemon/lib/source/DobbyManager.cpp +++ b/daemon/lib/source/DobbyManager.cpp @@ -850,7 +850,14 @@ int32_t DobbyManager::startContainerFromSpec(const ContainerId &id, // Set Apparmor profile if (mSettings->apparmorSettings().enabled) { - config->setApparmorProfile(mSettings->apparmorSettings().profileName); + if (config->androidEnabled() && !mSettings->androidAccessSettings()->appArmorProfile.empty()) + { + config->setApparmorProfile(mSettings->androidAccessSettings()->appArmorProfile); + } + else + { + config->setApparmorProfile(mSettings->apparmorSettings().profileName); + } } // Set pids limit diff --git a/settings/include/IDobbySettings.h b/settings/include/IDobbySettings.h index d20d0a77..edb2d308 100644 --- a/settings/include/IDobbySettings.h +++ b/settings/include/IDobbySettings.h @@ -196,6 +196,7 @@ class IDobbySettings std::set groupIds; std::list extraMounts; std::map extraEnvVariables; + std::string appArmorProfile; }; // ------------------------------------------------------------------------- diff --git a/settings/source/Settings.cpp b/settings/source/Settings.cpp index be670a97..c10d41d7 100644 --- a/settings/source/Settings.cpp +++ b/settings/source/Settings.cpp @@ -964,28 +964,28 @@ std::list Settings::getAndroidNodes(const Json::Val const Json::Value &devNodes = Json::Path(path).resolve(root); if (devNodes.isNull()) { - return std::list(); + return std::list(); } else if (!devNodes.isArray()) { AI_LOG_ERROR("JSON value in settings file is not an array (of dev nodes)"); - return std::list(); + return std::list(); } std::list result; for (const Json::Value &devNode : devNodes) { - if (!devNode.isObject()) - { - AI_LOG_ERROR("invalid JSON value in dev nodes array in settings file"); - return std::list(); - } + if (!devNode.isObject()) + { + AI_LOG_ERROR("invalid JSON value in dev nodes array in settings file"); + return std::list(); + } - AndroidDeviceNode androidDevNode; - if (processAndroidDevice(devNode, &androidDevNode)) - { - result.emplace_back(androidDevNode); - } + AndroidDeviceNode androidDevNode; + if (processAndroidDevice(devNode, &androidDevNode)) + { + result.emplace_back(androidDevNode); + } } return result; @@ -1279,5 +1279,19 @@ std::shared_ptr Settings::getAndroidAcces // get any extra environment vars accessSettings->extraEnvVariables = getEnvVarsFromJson(hw, Json::Path(".extraEnvVariables")); + const Json::Value appArmorProfile = hw["appArmorProfile"]; + if (appArmorProfile.isNull()) + { + AI_LOG_INFO("No Android AppArmor profile provided - will use default"); + } + else if (appArmorProfile.isString()) + { + accessSettings->appArmorProfile = appArmorProfile.asString(); + } + else + { + AI_LOG_ERROR("Invalid entry in android.appArmorProfile"); + } + return accessSettings; }