]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
New directives NoExecPaths= ExecPaths=
authorTopi Miettinen <toiwoton@gmail.com>
Sat, 16 Jan 2021 11:49:32 +0000 (13:49 +0200)
committerTopi Miettinen <topimiettinen@users.noreply.github.com>
Fri, 29 Jan 2021 12:40:52 +0000 (12:40 +0000)
Implement directives `NoExecPaths=` and `ExecPaths=` to control `MS_NOEXEC`
mount flag for the file system tree. This can be used to implement file system
W^X policies, and for example with allow-listing mode (NoExecPaths=/) a
compromised service would not be able to execute a shell, if that was not
explicitly allowed.

Example:
[Service]
NoExecPaths=/
ExecPaths=/usr/bin/daemon /usr/lib64 /usr/lib

Closes: #17942.
15 files changed:
man/org.freedesktop.systemd1.xml
man/systemd.exec.xml
src/core/dbus-execute.c
src/core/execute.c
src/core/execute.h
src/core/load-fragment-gperf.gperf.m4
src/core/namespace.c
src/core/namespace.h
src/shared/bus-unit-util.c
src/shared/mount-util.c
src/test/test-execute.c
src/test/test-namespace.c
src/test/test-ns.c
test/fuzz/fuzz-unit-file/directives.service
test/test-execute/exec-noexecpaths-simple.service [new file with mode: 0644]

index c4e1d1f94acc3a3d0c75f401d83072c7f37a9dde..6783d19b215c9d90e6da679f07d2b0a8d4206aea 100644 (file)
@@ -2643,6 +2643,10 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
       readonly as InaccessiblePaths = ['...', ...];
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+      readonly as ExecPaths = ['...', ...];
+      @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+      readonly as NoExecPaths = ['...', ...];
+      @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
       readonly t MountFlags = ...;
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
       readonly b PrivateTmp = ...;
@@ -3154,6 +3158,10 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
 
     <!--property InaccessiblePaths is not documented!-->
 
+    <!--property ExecPaths is not documented!-->
+
+    <!--property NoExecPaths is not documented!-->
+
     <!--property PrivateTmp is not documented!-->
 
     <!--property PrivateDevices is not documented!-->
@@ -3722,6 +3730,10 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2eservice {
 
     <variablelist class="dbus-property" generated="True" extra-ref="InaccessiblePaths"/>
 
+    <variablelist class="dbus-property" generated="True" extra-ref="ExecPaths"/>
+
+    <variablelist class="dbus-property" generated="True" extra-ref="NoExecPaths"/>
+
     <variablelist class="dbus-property" generated="True" extra-ref="MountFlags"/>
 
     <variablelist class="dbus-property" generated="True" extra-ref="PrivateTmp"/>
@@ -4385,6 +4397,10 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
       readonly as InaccessiblePaths = ['...', ...];
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+      readonly as ExecPaths = ['...', ...];
+      @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+      readonly as NoExecPaths = ['...', ...];
+      @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
       readonly t MountFlags = ...;
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
       readonly b PrivateTmp = ...;
@@ -4924,6 +4940,10 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
 
     <!--property InaccessiblePaths is not documented!-->
 
+    <!--property ExecPaths is not documented!-->
+
+    <!--property NoExecPaths is not documented!-->
+
     <!--property PrivateTmp is not documented!-->
 
     <!--property PrivateDevices is not documented!-->
@@ -5490,6 +5510,10 @@ node /org/freedesktop/systemd1/unit/avahi_2ddaemon_2esocket {
 
     <variablelist class="dbus-property" generated="True" extra-ref="InaccessiblePaths"/>
 
+    <variablelist class="dbus-property" generated="True" extra-ref="ExecPaths"/>
+
+    <variablelist class="dbus-property" generated="True" extra-ref="NoExecPaths"/>
+
     <variablelist class="dbus-property" generated="True" extra-ref="MountFlags"/>
 
     <variablelist class="dbus-property" generated="True" extra-ref="PrivateTmp"/>
@@ -6066,6 +6090,10 @@ node /org/freedesktop/systemd1/unit/home_2emount {
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
       readonly as InaccessiblePaths = ['...', ...];
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+      readonly as ExecPaths = ['...', ...];
+      @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+      readonly as NoExecPaths = ['...', ...];
+      @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
       readonly t MountFlags = ...;
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
       readonly b PrivateTmp = ...;
@@ -6533,6 +6561,10 @@ node /org/freedesktop/systemd1/unit/home_2emount {
 
     <!--property InaccessiblePaths is not documented!-->
 
+    <!--property ExecPaths is not documented!-->
+
+    <!--property NoExecPaths is not documented!-->
+
     <!--property PrivateTmp is not documented!-->
 
     <!--property PrivateDevices is not documented!-->
@@ -7017,6 +7049,10 @@ node /org/freedesktop/systemd1/unit/home_2emount {
 
     <variablelist class="dbus-property" generated="True" extra-ref="InaccessiblePaths"/>
 
+    <variablelist class="dbus-property" generated="True" extra-ref="ExecPaths"/>
+
+    <variablelist class="dbus-property" generated="True" extra-ref="NoExecPaths"/>
+
     <variablelist class="dbus-property" generated="True" extra-ref="MountFlags"/>
 
     <variablelist class="dbus-property" generated="True" extra-ref="PrivateTmp"/>
@@ -7714,6 +7750,10 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
       readonly as InaccessiblePaths = ['...', ...];
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+      readonly as ExecPaths = ['...', ...];
+      @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
+      readonly as NoExecPaths = ['...', ...];
+      @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
       readonly t MountFlags = ...;
       @org.freedesktop.DBus.Property.EmitsChangedSignal("const")
       readonly b PrivateTmp = ...;
@@ -8167,6 +8207,10 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
 
     <!--property InaccessiblePaths is not documented!-->
 
+    <!--property ExecPaths is not documented!-->
+
+    <!--property NoExecPaths is not documented!-->
+
     <!--property PrivateTmp is not documented!-->
 
     <!--property PrivateDevices is not documented!-->
@@ -8637,6 +8681,10 @@ node /org/freedesktop/systemd1/unit/dev_2dsda3_2eswap {
 
     <variablelist class="dbus-property" generated="True" extra-ref="InaccessiblePaths"/>
 
+    <variablelist class="dbus-property" generated="True" extra-ref="ExecPaths"/>
+
+    <variablelist class="dbus-property" generated="True" extra-ref="NoExecPaths"/>
+
     <variablelist class="dbus-property" generated="True" extra-ref="MountFlags"/>
 
     <variablelist class="dbus-property" generated="True" extra-ref="PrivateTmp"/>
index 8f00ef555e0f959e986f1a0a6747e8c21549c629..25b52503ee9539417247c66889dd16eb9bad34aa 100644 (file)
@@ -1359,6 +1359,8 @@ StateDirectory=aaa/bbb ccc</programlisting>
         <term><varname>ReadWritePaths=</varname></term>
         <term><varname>ReadOnlyPaths=</varname></term>
         <term><varname>InaccessiblePaths=</varname></term>
+        <term><varname>ExecPaths=</varname></term>
+        <term><varname>NoExecPaths=</varname></term>
 
         <listitem><para>Sets up a new file system namespace for executed processes. These options may be used
         to limit access a process has to the file system. Each setting takes a space-separated list of paths
@@ -1380,12 +1382,18 @@ StateDirectory=aaa/bbb ccc</programlisting>
         <varname>BindPaths=</varname>, or <varname>BindReadOnlyPaths=</varname> inside it. For a more flexible option,
         see <varname>TemporaryFileSystem=</varname>.</para>
 
+        <para>Content in paths listed in <varname>NoExecPaths=</varname> are not executable even if the usual
+        file access controls would permit this. Nest <varname>ExecPaths=</varname> inside of
+        <varname>NoExecPaths=</varname> in order to provide executable content within non-executable
+        directories.</para>
+
         <para>Non-directory paths may be specified as well. These options may be specified more than once,
         in which case all paths listed will have limited access from within the namespace. If the empty string is
         assigned to this option, the specific list is reset, and all prior assignments have no effect.</para>
 
-        <para>Paths in <varname>ReadWritePaths=</varname>, <varname>ReadOnlyPaths=</varname> and
-        <varname>InaccessiblePaths=</varname> may be prefixed with <literal>-</literal>, in which case they will be
+        <para>Paths in <varname>ReadWritePaths=</varname>, <varname>ReadOnlyPaths=</varname>,
+        <varname>InaccessiblePaths=</varname>, <varname>ExecPaths=</varname> and
+        <varname>NoExecPaths=</varname> may be prefixed with <literal>-</literal>, in which case they will be
         ignored when they do not exist. If prefixed with <literal>+</literal> the paths are taken relative to the root
         directory of the unit, as configured with <varname>RootDirectory=</varname>/<varname>RootImage=</varname>,
         instead of relative to the root directory of the host (see above). When combining <literal>-</literal> and
@@ -1408,6 +1416,15 @@ StateDirectory=aaa/bbb ccc</programlisting>
         <varname>CapabilityBoundingSet=~CAP_SYS_ADMIN</varname> or
         <varname>SystemCallFilter=~@mount</varname>.</para>
 
+        <para>Simple allow-list example using these directives:
+        <programlisting>[Service]
+ReadOnlyPaths=/
+ReadWritePaths=/var /run
+InaccessiblePaths=-/lost+found
+NoExecPaths=/
+ExecPaths=/usr/sbin/my_daemon /usr/lib /usr/lib64
+</programlisting></para>
+
         <xi:include href="system-only.xml" xpointer="plural"/></listitem>
       </varlistentry>
 
index 0fbf0b167ce677ba4d6fb698fecb72db795bac46..1f0e27a14176fa5e0f7c0adb662638ae6c00c9d6 100644 (file)
@@ -1094,6 +1094,8 @@ const sd_bus_vtable bus_exec_vtable[] = {
         SD_BUS_PROPERTY("ReadWritePaths", "as", NULL, offsetof(ExecContext, read_write_paths), SD_BUS_VTABLE_PROPERTY_CONST),
         SD_BUS_PROPERTY("ReadOnlyPaths", "as", NULL, offsetof(ExecContext, read_only_paths), SD_BUS_VTABLE_PROPERTY_CONST),
         SD_BUS_PROPERTY("InaccessiblePaths", "as", NULL, offsetof(ExecContext, inaccessible_paths), SD_BUS_VTABLE_PROPERTY_CONST),
+        SD_BUS_PROPERTY("ExecPaths", "as", NULL, offsetof(ExecContext, exec_paths), SD_BUS_VTABLE_PROPERTY_CONST),
+        SD_BUS_PROPERTY("NoExecPaths", "as", NULL, offsetof(ExecContext, no_exec_paths), SD_BUS_VTABLE_PROPERTY_CONST),
         SD_BUS_PROPERTY("MountFlags", "t", bus_property_get_ulong, offsetof(ExecContext, mount_flags), SD_BUS_VTABLE_PROPERTY_CONST),
         SD_BUS_PROPERTY("PrivateTmp", "b", bus_property_get_bool, offsetof(ExecContext, private_tmp), SD_BUS_VTABLE_PROPERTY_CONST),
         SD_BUS_PROPERTY("PrivateDevices", "b", bus_property_get_bool, offsetof(ExecContext, private_devices), SD_BUS_VTABLE_PROPERTY_CONST),
@@ -2981,7 +2983,7 @@ int bus_exec_context_set_transient_property(
                 return 1;
 
         } else if (STR_IN_SET(name, "ReadWriteDirectories", "ReadOnlyDirectories", "InaccessibleDirectories",
-                              "ReadWritePaths", "ReadOnlyPaths", "InaccessiblePaths")) {
+                              "ReadWritePaths", "ReadOnlyPaths", "InaccessiblePaths", "ExecPaths", "NoExecPaths")) {
                 _cleanup_strv_free_ char **l = NULL;
                 char ***dirs;
                 char **p;
@@ -3007,6 +3009,10 @@ int bus_exec_context_set_transient_property(
                                 dirs = &c->read_write_paths;
                         else if (STR_IN_SET(name, "ReadOnlyDirectories", "ReadOnlyPaths"))
                                 dirs = &c->read_only_paths;
+                        else if (streq(name, "ExecPaths"))
+                                dirs = &c->exec_paths;
+                        else if (streq(name, "NoExecPaths"))
+                                dirs = &c->no_exec_paths;
                         else /* "InaccessiblePaths" */
                                 dirs = &c->inaccessible_paths;
 
index 1a679da435023d64894464698ac55938310d1dd2..b7d78f2197e143e1b781b29b26bafefedc978c6d 100644 (file)
@@ -1999,7 +1999,9 @@ bool exec_needs_mount_namespace(
 
         if (!strv_isempty(context->read_write_paths) ||
             !strv_isempty(context->read_only_paths) ||
-            !strv_isempty(context->inaccessible_paths))
+            !strv_isempty(context->inaccessible_paths) ||
+            !strv_isempty(context->exec_paths) ||
+            !strv_isempty(context->no_exec_paths))
                 return true;
 
         if (context->n_bind_mounts > 0)
@@ -3206,6 +3208,8 @@ static int apply_mount_namespace(
                             &ns_info, context->read_write_paths,
                             needs_sandboxing ? context->read_only_paths : NULL,
                             needs_sandboxing ? context->inaccessible_paths : NULL,
+                            needs_sandboxing ? context->exec_paths : NULL,
+                            needs_sandboxing ? context->no_exec_paths : NULL,
                             empty_directories,
                             bind_mounts,
                             n_bind_mounts,
@@ -4815,6 +4819,8 @@ void exec_context_done(ExecContext *c) {
         c->read_only_paths = strv_free(c->read_only_paths);
         c->read_write_paths = strv_free(c->read_write_paths);
         c->inaccessible_paths = strv_free(c->inaccessible_paths);
+        c->exec_paths = strv_free(c->exec_paths);
+        c->no_exec_paths = strv_free(c->no_exec_paths);
 
         bind_mount_free_many(c->bind_mounts, c->n_bind_mounts);
         c->bind_mounts = NULL;
@@ -5162,6 +5168,18 @@ static void strv_fprintf(FILE *f, char **l) {
                 fprintf(f, " %s", *g);
 }
 
+static void strv_dump(FILE* f, const char *prefix, const char *name, char **strv) {
+        assert(f);
+        assert(prefix);
+        assert(name);
+
+        if (!strv_isempty(strv)) {
+                fprintf(f, "%s%s:", name, prefix);
+                strv_fprintf(f, strv);
+                fputs("\n", f);
+        }
+}
+
 void exec_context_dump(const ExecContext *c, FILE* f, const char *prefix) {
         char **e, **d, buf_clean[FORMAT_TIMESPAN_MAX];
         int r;
@@ -5474,32 +5492,16 @@ void exec_context_dump(const ExecContext *c, FILE* f, const char *prefix) {
 
         fprintf(f, "%sDynamicUser: %s\n", prefix, yes_no(c->dynamic_user));
 
-        if (!strv_isempty(c->supplementary_groups)) {
-                fprintf(f, "%sSupplementaryGroups:", prefix);
-                strv_fprintf(f, c->supplementary_groups);
-                fputs("\n", f);
-        }
+        strv_dump(f, prefix, "SupplementaryGroups", c->supplementary_groups);
 
         if (c->pam_name)
                 fprintf(f, "%sPAMName: %s\n", prefix, c->pam_name);
 
-        if (!strv_isempty(c->read_write_paths)) {
-                fprintf(f, "%sReadWritePaths:", prefix);
-                strv_fprintf(f, c->read_write_paths);
-                fputs("\n", f);
-        }
-
-        if (!strv_isempty(c->read_only_paths)) {
-                fprintf(f, "%sReadOnlyPaths:", prefix);
-                strv_fprintf(f, c->read_only_paths);
-                fputs("\n", f);
-        }
-
-        if (!strv_isempty(c->inaccessible_paths)) {
-                fprintf(f, "%sInaccessiblePaths:", prefix);
-                strv_fprintf(f, c->inaccessible_paths);
-                fputs("\n", f);
-        }
+        strv_dump(f, prefix, "ReadWritePaths", c->read_write_paths);
+        strv_dump(f, prefix, "ReadOnlyPaths", c->read_only_paths);
+        strv_dump(f, prefix, "InaccessiblePaths", c->inaccessible_paths);
+        strv_dump(f, prefix, "ExecPaths", c->exec_paths);
+        strv_dump(f, prefix, "NoExecPaths", c->no_exec_paths);
 
         for (size_t i = 0; i < c->n_bind_mounts; i++)
                 fprintf(f, "%s%s: %s%s:%s:%s\n", prefix,
index f8231ba773618eb6c0be4d213713683e410076da..d615af5109218412b10db9b8b72223eb35780b7e 100644 (file)
@@ -243,7 +243,7 @@ struct ExecContext {
         char *apparmor_profile;
         char *smack_process_label;
 
-        char **read_write_paths, **read_only_paths, **inaccessible_paths;
+        char **read_write_paths, **read_only_paths, **inaccessible_paths, **exec_paths, **no_exec_paths;
         unsigned long mount_flags;
         BindMount *bind_mounts;
         size_t n_bind_mounts;
index 6e92fd80fd14bb6e3112f745ebd49f7a5f47008f..6bf22c336aeef28a9fc99fa2f4689bd37600ca07 100644 (file)
@@ -119,6 +119,8 @@ $1.InaccessibleDirectories,              config_parse_namespace_path_strv,
 $1.ReadWritePaths,                       config_parse_namespace_path_strv,            0,                                  offsetof($1, exec_context.read_write_paths)
 $1.ReadOnlyPaths,                        config_parse_namespace_path_strv,            0,                                  offsetof($1, exec_context.read_only_paths)
 $1.InaccessiblePaths,                    config_parse_namespace_path_strv,            0,                                  offsetof($1, exec_context.inaccessible_paths)
+$1.ExecPaths,                            config_parse_namespace_path_strv,            0,                                  offsetof($1, exec_context.exec_paths)
+$1.NoExecPaths,                          config_parse_namespace_path_strv,            0,                                  offsetof($1, exec_context.no_exec_paths)
 $1.BindPaths,                            config_parse_bind_paths,                     0,                                  offsetof($1, exec_context)
 $1.BindReadOnlyPaths,                    config_parse_bind_paths,                     0,                                  offsetof($1, exec_context)
 $1.TemporaryFileSystem,                  config_parse_temporary_filesystems,          0,                                  offsetof($1, exec_context)
index e8306a8d550889058f9aeb2d36b8e97b0bea5d64..4ed0991b56d199be0e858b033e7da76e2bf1e456 100644 (file)
@@ -54,6 +54,8 @@ typedef enum MountMode {
         RUN,
         READONLY,
         READWRITE,
+        NOEXEC,
+        EXEC,
         TMPFS,
         READWRITE_IMPLICIT, /* Should have the lowest priority. */
         _MOUNT_MODE_MAX,
@@ -66,6 +68,8 @@ typedef struct MountEntry {
         bool has_prefix:1;        /* Already is prefixed by the root dir? */
         bool read_only:1;         /* Shall this mount point be read-only? */
         bool nosuid:1;            /* Shall set MS_NOSUID on the mount itself */
+        bool noexec:1;            /* Shall set MS_NOEXEC on the mount itself */
+        bool exec:1;              /* Shall clear MS_NOEXEC on the mount itself */
         bool applied:1;           /* Already applied */
         char *path_malloc;        /* Use this instead of 'path_const' if we had to allocate memory */
         const char *source_const; /* The source path, for bind mounts or images */
@@ -212,6 +216,8 @@ static const char * const mount_mode_table[_MOUNT_MODE_MAX] = {
         [TMPFS]                = "tmpfs",
         [MOUNT_IMAGES]         = "mount-images",
         [READWRITE_IMPLICIT]   = "rw-implicit",
+        [EXEC]                 = "exec",
+        [NOEXEC]               = "noexec",
 };
 
 DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(mount_mode, MountMode);
@@ -231,6 +237,18 @@ static bool mount_entry_read_only(const MountEntry *p) {
         return p->read_only || IN_SET(p->mode, READONLY, INACCESSIBLE, PRIVATE_TMP_READONLY);
 }
 
+static bool mount_entry_noexec(const MountEntry *p) {
+        assert(p);
+
+        return p->noexec || IN_SET(p->mode, NOEXEC, INACCESSIBLE, SYSFS, PROCFS);
+}
+
+static bool mount_entry_exec(const MountEntry *p) {
+        assert(p);
+
+        return p->exec || p->mode == EXEC;
+}
+
 static const char *mount_entry_source(const MountEntry *p) {
         assert(p);
 
@@ -497,7 +515,10 @@ static void drop_duplicates(MountEntry *m, size_t *n) {
                     path_equal(mount_entry_path(f), mount_entry_path(previous)) &&
                     !f->applied && !previous->applied) {
                         log_debug("%s (%s) is duplicate.", mount_entry_path(f), mount_mode_to_string(f->mode));
-                        previous->read_only = previous->read_only || mount_entry_read_only(f); /* Propagate the read-only flag to the remaining entry */
+                        /* Propagate the flags to the remaining entry */
+                        previous->read_only = previous->read_only || mount_entry_read_only(f);
+                        previous->noexec = previous->noexec || mount_entry_noexec(f);
+                        previous->exec = previous->exec || mount_entry_exec(f);
                         mount_entry_done(f);
                         continue;
                 }
@@ -1057,6 +1078,8 @@ static int apply_mount(
         case READONLY:
         case READWRITE:
         case READWRITE_IMPLICIT:
+        case EXEC:
+        case NOEXEC:
                 r = path_is_mount_point(mount_entry_path(m), root_directory, 0);
                 if (r == -ENOENT && m->ignore)
                         return 0;
@@ -1064,7 +1087,7 @@ static int apply_mount(
                         return log_debug_errno(r, "Failed to determine whether %s is already a mount point: %m",
                                                mount_entry_path(m));
                 if (r > 0) /* Nothing to do here, it is already a mount. We just later toggle the MS_RDONLY
-                            * bit for the mount point if needed. */
+                            * and MS_NOEXEC bits for the mount point if needed. */
                         return 0;
                 /* This isn't a mount point yet, let's make it one. */
                 what = mount_entry_path(m);
@@ -1195,7 +1218,7 @@ static int make_read_only(const MountEntry *m, char **deny_list, FILE *proc_self
         else
                 r = bind_remount_one_with_mountinfo(mount_entry_path(m), new_flags, flags_mask, proc_self_mountinfo);
 
-        /* Not that we only turn on the MS_RDONLY flag here, we never turn it off. Something that was marked
+        /* Note that we only turn on the MS_RDONLY flag here, we never turn it off. Something that was marked
          * read-only already stays this way. This improves compatibility with container managers, where we
          * won't attempt to undo read-only mounts already applied. */
 
@@ -1207,6 +1230,40 @@ static int make_read_only(const MountEntry *m, char **deny_list, FILE *proc_self
         return 0;
 }
 
+static int make_noexec(const MountEntry *m, char **deny_list, FILE *proc_self_mountinfo) {
+        unsigned long new_flags = 0, flags_mask = 0;
+        bool submounts = false;
+        int r = 0;
+
+        assert(m);
+        assert(proc_self_mountinfo);
+
+        if (mount_entry_noexec(m)) {
+                new_flags |= MS_NOEXEC;
+                flags_mask |= MS_NOEXEC;
+        } else if (mount_entry_exec(m)) {
+                new_flags &= ~MS_NOEXEC;
+                flags_mask |= MS_NOEXEC;
+        }
+
+        if (flags_mask == 0) /* No Change? */
+                return 0;
+
+        submounts = !IN_SET(m->mode, EMPTY_DIR, TMPFS);
+
+        if (submounts)
+                r = bind_remount_recursive_with_mountinfo(mount_entry_path(m), new_flags, flags_mask, deny_list, proc_self_mountinfo);
+        else
+                r = bind_remount_one_with_mountinfo(mount_entry_path(m), new_flags, flags_mask, proc_self_mountinfo);
+
+        if (r == -ENOENT && m->ignore)
+                return 0;
+        if (r < 0)
+                return log_debug_errno(r, "Failed to re-mount '%s'%s: %m", mount_entry_path(m),
+                                       submounts ? " and its submounts" : "");
+        return 0;
+}
+
 static bool namespace_info_mount_apivfs(const NamespaceInfo *ns_info) {
         assert(ns_info);
 
@@ -1228,6 +1285,8 @@ static size_t namespace_calculate_mounts(
                 char** read_write_paths,
                 char** read_only_paths,
                 char** inaccessible_paths,
+                char** exec_paths,
+                char** no_exec_paths,
                 char** empty_directories,
                 size_t n_bind_mounts,
                 size_t n_temporary_filesystems,
@@ -1260,6 +1319,8 @@ static size_t namespace_calculate_mounts(
                 strv_length(read_write_paths) +
                 strv_length(read_only_paths) +
                 strv_length(inaccessible_paths) +
+                strv_length(exec_paths) +
+                strv_length(no_exec_paths) +
                 strv_length(empty_directories) +
                 n_bind_mounts +
                 n_mount_images +
@@ -1406,6 +1467,8 @@ int setup_namespace(
                 char** read_write_paths,
                 char** read_only_paths,
                 char** inaccessible_paths,
+                char** exec_paths,
+                char** no_exec_paths,
                 char** empty_directories,
                 const BindMount *bind_mounts,
                 size_t n_bind_mounts,
@@ -1523,6 +1586,8 @@ int setup_namespace(
                         read_write_paths,
                         read_only_paths,
                         inaccessible_paths,
+                        exec_paths,
+                        no_exec_paths,
                         empty_directories,
                         n_bind_mounts,
                         n_temporary_filesystems,
@@ -1550,6 +1615,14 @@ int setup_namespace(
                 if (r < 0)
                         goto finish;
 
+                r = append_access_mounts(&m, exec_paths, EXEC, require_prefix);
+                if (r < 0)
+                        goto finish;
+
+                r = append_access_mounts(&m, no_exec_paths, NOEXEC, require_prefix);
+                if (r < 0)
+                        goto finish;
+
                 r = append_empty_dir_mounts(&m, empty_directories);
                 if (r < 0)
                         goto finish;
@@ -1869,6 +1942,21 @@ int setup_namespace(
                                 goto finish;
                         }
                 }
+
+                /* Third round, flip the noexec bits with a simplified deny list. */
+                for (m = mounts, j = 0; m < mounts + n_mounts; ++m)
+                        if (IN_SET(m->mode, EXEC, NOEXEC))
+                                deny_list[j++] = (char*) mount_entry_path(m);
+                deny_list[j] = NULL;
+
+                for (m = mounts; m < mounts + n_mounts; ++m) {
+                        r = make_noexec(m, deny_list, proc_self_mountinfo);
+                        if (r < 0) {
+                                if (error_path && mount_entry_path(m))
+                                        *error_path = strdup(mount_entry_path(m));
+                                goto finish;
+                        }
+                }
         }
 
         /* MS_MOVE does not work on MS_SHARED so the remount MS_SHARED will be done later */
index 8e07dd37bcd2cd1ec49dc0169e2dcf3964c4d80c..b2ea4bd76be6c8335d6e6b979c00417133700819 100644 (file)
@@ -108,6 +108,8 @@ int setup_namespace(
                 char **read_write_paths,
                 char **read_only_paths,
                 char **inaccessible_paths,
+                char **exec_paths,
+                char **no_exec_paths,
                 char **empty_directories,
                 const BindMount *bind_mounts,
                 size_t n_bind_mounts,
index 07f936dc6cbf778feb1719cae097f71974ed0782..8fd2f89adc7eb63694425705c0fe4e25bfb9d5b4 100644 (file)
@@ -904,6 +904,8 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con
                               "ReadWritePaths",
                               "ReadOnlyPaths",
                               "InaccessiblePaths",
+                              "ExecPaths",
+                              "NoExecPaths",
                               "RuntimeDirectory",
                               "StateDirectory",
                               "CacheDirectory",
index 4df391949b4081edc76b9e6902d5172934525679..183a686706ef5ad030bbfc5740c9feb118931da3 100644 (file)
@@ -210,13 +210,14 @@ int bind_remount_recursive_with_mountinfo(
         assert(prefix);
         assert(proc_self_mountinfo);
 
-        /* Recursively remount a directory (and all its submounts) read-only or read-write. If the directory is already
-         * mounted, we reuse the mount and simply mark it MS_BIND|MS_RDONLY (or remove the MS_RDONLY for read-write
-         * operation). If it isn't we first make it one. Afterwards we apply MS_BIND|MS_RDONLY (or remove MS_RDONLY) to
-         * all submounts we can access, too. When mounts are stacked on the same mount point we only care for each
-         * individual "top-level" mount on each point, as we cannot influence/access the underlying mounts anyway. We
-         * do not have any effect on future submounts that might get propagated, they might be writable. This includes
-         * future submounts that have been triggered via autofs.
+        /* Recursively remount a directory (and all its submounts) with desired flags (MS_READONLY,
+         * MS_NOSUID, MS_NOEXEC). If the directory is already mounted, we reuse the mount and simply mark it
+         * MS_BIND|MS_RDONLY (or remove the MS_RDONLY for read-write operation), ditto for other flags. If it
+         * isn't we first make it one. Afterwards we apply (or remove) the flags to all submounts we can
+         * access, too. When mounts are stacked on the same mount point we only care for each individual
+         * "top-level" mount on each point, as we cannot influence/access the underlying mounts anyway. We do
+         * not have any effect on future submounts that might get propagated, they might be writable
+         * etc. This includes future submounts that have been triggered via autofs.
          *
          * If the "deny_list" parameter is specified it may contain a list of subtrees to exclude from the
          * remount operation. Note that we'll ignore the deny list for the top-level path. */
index 01e2443777c0697ca99c6e638a2f311f9f94bcb9..c0e046b5e21f0d8873549d88bdd5b438ce841b61 100644 (file)
@@ -408,6 +408,11 @@ static void test_exec_inaccessiblepaths(Manager *m) {
         test(m, "exec-inaccessiblepaths-mount-propagation.service", can_unshare ? 0 : EXIT_FAILURE, CLD_EXITED);
 }
 
+static void test_exec_noexecpaths(Manager *m) {
+
+        test(m, "exec-noexecpaths-simple.service", can_unshare ? 0 : EXIT_FAILURE, CLD_EXITED);
+}
+
 static void test_exec_temporaryfilesystem(Manager *m) {
 
         test(m, "exec-temporaryfilesystem-options.service", can_unshare ? 0 : EXIT_NAMESPACE, CLD_EXITED);
@@ -865,6 +870,7 @@ int main(int argc, char *argv[]) {
                 entry(test_exec_ignoresigpipe),
                 entry(test_exec_inaccessiblepaths),
                 entry(test_exec_ioschedulingclass),
+                entry(test_exec_noexecpaths),
                 entry(test_exec_oomscoreadjust),
                 entry(test_exec_passenvironment),
                 entry(test_exec_personality),
index 461dde5fa9c3e9e89f55827db76950bd39a38e67..b4db78492ea2f6859444b59746fcd1349af2a53e 100644 (file)
@@ -157,6 +157,8 @@ static void test_protect_kernel_logs(void) {
                                     NULL,
                                     NULL,
                                     NULL,
+                                    NULL,
+                                    NULL,
                                     NULL, 0,
                                     NULL, 0,
                                     NULL, 0,
index 3b5836e980ff5794bb652ecfe984f877e64a7004..71ccfb88f421fa5fa3dfb89b9aaa5eb7e86a4b1e 100644 (file)
@@ -26,6 +26,19 @@ int main(int argc, char *argv[]) {
                 NULL
         };
 
+        const char * const exec[] = {
+                "/lib",
+                "/usr",
+                "-/lib64",
+                "-/usr/lib64",
+                NULL
+        };
+
+        const char * const no_exec[] = {
+                "/var",
+                NULL
+        };
+
         const char *inaccessible[] = {
                 "/home/lennart/projects",
                 NULL
@@ -70,6 +83,8 @@ int main(int argc, char *argv[]) {
                             (char **) writable,
                             (char **) readonly,
                             (char **) inaccessible,
+                            (char **) exec,
+                            (char **) no_exec,
                             NULL,
                             &(BindMount) { .source = (char*) "/usr/bin", .destination = (char*) "/etc/systemd", .read_only = true }, 1,
                             &(TemporaryFileSystem) { .path = (char*) "/var", .options = (char*) "ro" }, 1,
index 2ea00ae52c0656d266024f9e21405231c1733ddf..15fa556dd64b6f1e9464d6af815ae0abd3625793 100644 (file)
@@ -85,6 +85,7 @@ DirectoryMode=
 DirectoryNotEmpty=
 Documentation=
 DynamicUser=
+ExecPaths=
 ExecReload=
 ExecCondition=
 ExecStart=
@@ -147,6 +148,7 @@ MessageQueueMaxMessages=
 MessageQueueMessageSize=
 MountAPIVFS=
 NoDelay=
+NoExecPaths=
 NoNewPrivileges=
 NonBlocking=
 NotifyAccess=
diff --git a/test/test-execute/exec-noexecpaths-simple.service b/test/test-execute/exec-noexecpaths-simple.service
new file mode 100644 (file)
index 0000000..45152a2
--- /dev/null
@@ -0,0 +1,10 @@
+[Unit]
+Description=Test for NoExecPaths=
+
+[Service]
+Type=oneshot
+# This should work, as we explicitly disable the effect of NoExecPaths=
+ExecStart=+/bin/sh -c '/bin/cat /dev/null'
+# This should also work, as we do not disable the effect of NoExecPaths= but invert the exit code
+ExecStart=/bin/sh -x -c '! /bin/cat /dev/null'
+NoExecPaths=/bin/cat