core: add new UnsetEnvironment= setting for unit files
authorLennart Poettering <lennart@poettering.net>
Sun, 10 Sep 2017 10:16:44 +0000 (12:16 +0200)
committerLennart Poettering <lennart@poettering.net>
Thu, 14 Sep 2017 13:17:40 +0000 (15:17 +0200)
With this setting we can explicitly unset specific variables for
processes of a unit, as last step of assembling the environment block
for them. This is useful to fix #6407.

While we are at it, greatly expand the documentation on how the
environment block for forked off processes is assembled.

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/load-fragment.c
src/core/load-fragment.h
src/shared/bus-unit-util.c

index 625063f..da1b4dd 100644 (file)
       <varlistentry>
         <term><varname>PassEnvironment=</varname></term>
 
-        <listitem><para>Pass environment variables from the systemd system
-        manager to executed processes. Takes a space-separated list of variable
-        names. This option may be specified more than once, in which case all
-        listed variables will be set. If the empty string is assigned to this
-        option, the list of environment variables is reset, all prior
-        assignments have no effect. Variables that are not set in the system
-        manager will not be passed and will be silently ignored.</para>
-
-        <para>Variables passed from this setting are overridden by those passed
-        from <varname>Environment=</varname> or
-        <varname>EnvironmentFile=</varname>.</para>
+        <listitem><para>Pass environment variables set for the system service manager to executed processes. Takes a
+        space-separated list of variable names. This option may be specified more than once, in which case all listed
+        variables will be passed. If the empty string is assigned to this option, the list of environment variables to
+        pass is reset, all prior assignments have no effect. Variables specified that are not set for the system
+        manager will not be passed and will be silently ignored. Note that this option is only relevant for the system
+        service manager, as system services by default do not automatically inherit any environment variables set for
+        the service manager itself. However, in case of the user service manager all environment variables are passed
+        to the executed processes anyway, hence this option is without effect for the user service manager.</para>
+
+        <para>Variables set for invoked processes due to this setting are subject to being overridden by those
+        configured with <varname>Environment=</varname> or <varname>EnvironmentFile=</varname>.</para>
 
         <para>Example:
         <programlisting>PassEnvironment=VAR1 VAR2 VAR3</programlisting>
       </varlistentry>
 
       <varlistentry>
+        <term><varname>UnsetEnvironment=</varname></term>
+
+        <listitem><para>Explicitly unset environment variable assignments that would normally be passed from the
+        service manager to invoked processes of this unit. Takes a space-separated list of variable names or variable
+        assignments. This option may be specified more than once, in which case all listed variables/assignments will
+        be unset. If the empty string is assigned to this option, the list of environment variables/assignments to
+        unset is reset. If a variable assignment is specified (that is: a variable name, followed by
+        <literal>=</literal>, followed by its value), then any environment variable matching this precise assignment is
+        removed. If a variable name is specified (that is a variable name without any following <literal>=</literal> or
+        value), then any assignment matching the variable name, regardless of its value is removed. Note that the
+        effect of <varname>UnsetEnvironment=</varname> is applied as final step when the environment list passed to
+        executed processes is compiled. That means it may undo assignments from any configuration source, including
+        assignments made through <varname>Environment=</varname> or <varname>EnvironmentFile=</varname>, inherited from
+        the system manager's global set of environment variables, inherited via <varname>PassEnvironment=</varname>,
+        set by the service manager itself (such as <varname>$NOTIFY_SOCKET</varname> and such), or set by a PAM module
+        (in case <varname>PAMName=</varname> is used).</para>
+
+        <para>
+        See
+        <citerefentry project='man-pages'><refentrytitle>environ</refentrytitle><manvolnum>7</manvolnum></citerefentry>
+        for details about environment variables.</para></listitem>
+      </varlistentry>
+
+      <varlistentry>
         <term><varname>StandardInput=</varname></term>
         <listitem><para>Controls where file descriptor 0 (STDIN) of
         the executed processes is connected to. Takes one of
@@ -1799,12 +1823,38 @@ CapabilityBoundingSet=~CAP_B CAP_C</programlisting>
   <refsect1>
     <title>Environment variables in spawned processes</title>
 
-    <para>Processes started by the system are executed in a clean
-    environment in which select variables listed below are set. System
-    processes started by systemd do not inherit variables from PID 1,
-    but processes started by user systemd instances inherit all
-    environment variables from the user systemd instance.
-    </para>
+    <para>Processes started by the service manager are executed with an environment variable block assembled from
+    multiple sources. Processes started by the system service manager generally do not inherit environment variables
+    set for the service manager itself (but this may be altered via <varname>PassEnvironment=</varname>), but processes
+    started by the user service manager instances generally do inherit all environment variables set for the service
+    manager itself.</para>
+
+    <para>For each invoked process the list of environment variables set is compiled from the following sources:</para>
+
+    <itemizedlist>
+      <listitem><para>Variables globally configured for the service manager, using the
+      <varname>DefaultEnvironment=</varname> setting in
+      <citerefentry><refentrytitle>systemd-system.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry>, the kernel command line option <varname>systemd.setenv=</varname> (see
+    <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>) or via
+      <command>systemctl set-environment</command> (see <citerefentry><refentrytitle>systemctl</refentrytitle><manvolnum>1</manvolnum></citerefentry>).</para></listitem>
+
+      <listitem><para>Variables defined by the service manager itself (see the list below)</para></listitem>
+
+      <listitem><para>Variables set in the service manager's own environment variable block (subject to <varname>PassEnvironment=</varname> for the system service manager)</para></listitem>
+
+      <listitem><para>Variables set via <varname>Environment=</varname> in the unit file</para></listitem>
+
+      <listitem><para>Variables read from files specified via <varname>EnvironmentFiles=</varname> in the unit file</para></listitem>
+
+      <listitem><para>Variables set by any PAM modules in case <varname>PAMName=</varname> is in effect, cf. <citerefentry project='man-pages'><refentrytitle>pam_env</refentrytitle><manvolnum>8</manvolnum></citerefentry></para></listitem>
+    </itemizedlist>
+
+    <para>If the same environment variables are set by multiple of these sources, the later source — according to the
+    order of the list above — wins. Note that as final step all variables listed in
+    <varname>UnsetEnvironment=</varname> are removed again from the compiled environment variable list, immediately
+    before it is passed to the executed process.</para>
+
+    <para>The following select environment variables are set by the service manager itself for each invoked process:</para>
 
     <variablelist class='environment-variables'>
       <varlistentry>
@@ -2120,18 +2170,6 @@ CapabilityBoundingSet=~CAP_B CAP_C</programlisting>
         </listitem>
       </varlistentry>
     </variablelist>
-
-    <para>Additional variables may be configured by the following
-    means: for processes spawned in specific units, use the
-    <varname>Environment=</varname>, <varname>EnvironmentFile=</varname>
-    and <varname>PassEnvironment=</varname> options above; to specify
-    variables globally, use <varname>DefaultEnvironment=</varname>
-    (see
-    <citerefentry><refentrytitle>systemd-system.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry>)
-    or the kernel option <varname>systemd.setenv=</varname> (see
-    <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>).
-    Additional variables may also be set through PAM,
-    cf. <citerefentry project='man-pages'><refentrytitle>pam_env</refentrytitle><manvolnum>8</manvolnum></citerefentry>.</para>
   </refsect1>
 
   <refsect1>
index eb0af24..2bcdd33 100644 (file)
@@ -758,6 +758,7 @@ const sd_bus_vtable bus_exec_vtable[] = {
         SD_BUS_PROPERTY("Environment", "as", NULL, offsetof(ExecContext, environment), SD_BUS_VTABLE_PROPERTY_CONST),
         SD_BUS_PROPERTY("EnvironmentFiles", "a(sb)", property_get_environment_files, 0, SD_BUS_VTABLE_PROPERTY_CONST),
         SD_BUS_PROPERTY("PassEnvironment", "as", NULL, offsetof(ExecContext, pass_environment), SD_BUS_VTABLE_PROPERTY_CONST),
+        SD_BUS_PROPERTY("UnsetEnvironment", "as", NULL, offsetof(ExecContext, unset_environment), SD_BUS_VTABLE_PROPERTY_CONST),
         SD_BUS_PROPERTY("UMask", "u", bus_property_get_mode, offsetof(ExecContext, umask), SD_BUS_VTABLE_PROPERTY_CONST),
         SD_BUS_PROPERTY("LimitCPU", "t", bus_property_get_rlimit, offsetof(ExecContext, rlimit[RLIMIT_CPU]), SD_BUS_VTABLE_PROPERTY_CONST),
         SD_BUS_PROPERTY("LimitCPUSoft", "t", bus_property_get_rlimit, offsetof(ExecContext, rlimit[RLIMIT_CPU]), SD_BUS_VTABLE_PROPERTY_CONST),
@@ -1849,6 +1850,47 @@ int bus_exec_context_set_transient_property(
 
                 return 1;
 
+        } else if (streq(name, "UnsetEnvironment")) {
+
+                _cleanup_strv_free_ char **l = NULL, **q = NULL;
+
+                r = sd_bus_message_read_strv(message, &l);
+                if (r < 0)
+                        return r;
+
+                r = unit_full_printf_strv(u, l, &q);
+                if (r < 0)
+                        return r;
+
+                if (!strv_env_name_or_assignment_is_valid(q))
+                        return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid UnsetEnvironment= list.");
+
+                if (mode != UNIT_CHECK) {
+                        if (strv_length(q) == 0) {
+                                c->unset_environment = strv_free(c->unset_environment);
+                                unit_write_drop_in_private_format(u, mode, name, "UnsetEnvironment=");
+                        } else {
+                                _cleanup_free_ char *joined = NULL;
+                                char **e;
+
+                                e = strv_env_merge(2, c->unset_environment, q);
+                                if (!e)
+                                        return -ENOMEM;
+
+                                strv_free(c->unset_environment);
+                                c->unset_environment = e;
+
+                                /* We write just the new settings out to file, with unresolved specifiers */
+                                joined = strv_join_quoted(l);
+                                if (!joined)
+                                        return -ENOMEM;
+
+                                unit_write_drop_in_private_format(u, mode, name, "UnsetEnvironment=%s", joined);
+                        }
+                }
+
+                return 1;
+
         } else if (streq(name, "TimerSlackNSec")) {
 
                 nsec_t n;
@@ -1965,7 +2007,7 @@ int bus_exec_context_set_transient_property(
                         return r;
 
                 if (!strv_env_name_is_valid(l))
-                        return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid PassEnvironment block.");
+                        return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid PassEnvironment= block.");
 
                 if (mode != UNIT_CHECK) {
                         if (strv_isempty(l)) {
index 21c0149..9dcc02a 100644 (file)
@@ -1673,8 +1673,10 @@ static int build_pass_environment(const ExecContext *c, char ***ret) {
                 x = strjoin(*i, "=", v);
                 if (!x)
                         return -ENOMEM;
+
                 if (!GREEDY_REALLOC(pass_env, n_bufsize, n_env + 2))
                         return -ENOMEM;
+
                 pass_env[n_env++] = x;
                 pass_env[n_env] = NULL;
                 x = NULL;
@@ -3031,6 +3033,19 @@ static int exec_child(
 #endif
         }
 
+        if (!strv_isempty(context->unset_environment)) {
+                char **ee = NULL;
+
+                ee = strv_env_delete(accum_env, 1, context->unset_environment);
+                if (!ee) {
+                        *exit_status = EXIT_MEMORY;
+                        return -ENOMEM;
+                }
+
+                strv_free(accum_env);
+                accum_env = ee;
+        }
+
         final_argv = replace_env_argv(argv, accum_env);
         if (!final_argv) {
                 *exit_status = EXIT_MEMORY;
@@ -3222,6 +3237,7 @@ void exec_context_done(ExecContext *c) {
         c->environment = strv_free(c->environment);
         c->environment_files = strv_free(c->environment_files);
         c->pass_environment = strv_free(c->pass_environment);
+        c->unset_environment = strv_free(c->unset_environment);
 
         for (l = 0; l < ELEMENTSOF(c->rlimit); l++)
                 c->rlimit[l] = mfree(c->rlimit[l]);
@@ -3582,6 +3598,9 @@ void exec_context_dump(ExecContext *c, FILE* f, const char *prefix) {
         STRV_FOREACH(e, c->pass_environment)
                 fprintf(f, "%sPassEnvironment: %s\n", prefix, *e);
 
+        STRV_FOREACH(e, c->unset_environment)
+                fprintf(f, "%sUnsetEnvironment: %s\n", prefix, *e);
+
         fprintf(f, "%sRuntimeDirectoryPreserve: %s\n", prefix, exec_preserve_mode_to_string(c->runtime_directory_preserve_mode));
 
         for (dt = 0; dt < _EXEC_DIRECTORY_MAX; dt++) {
index 8a7ce84..f7c20db 100644 (file)
@@ -133,6 +133,7 @@ struct ExecContext {
         char **environment;
         char **environment_files;
         char **pass_environment;
+        char **unset_environment;
 
         struct rlimit *rlimit[_RLIMIT_MAX];
         char *working_directory, *root_directory, *root_image;
index 94f3d65..94e2939 100644 (file)
@@ -35,6 +35,7 @@ $1.UMask,                        config_parse_mode,                  0,
 $1.Environment,                  config_parse_environ,               0,                             offsetof($1, exec_context.environment)
 $1.EnvironmentFile,              config_parse_unit_env_file,         0,                             offsetof($1, exec_context.environment_files)
 $1.PassEnvironment,              config_parse_pass_environ,          0,                             offsetof($1, exec_context.pass_environment)
+$1.UnsetEnvironment,             config_parse_unset_environ,         0,                             offsetof($1, exec_context.unset_environment)
 $1.DynamicUser,                  config_parse_bool,                  true,                          offsetof($1, exec_context.dynamic_user)
 $1.StandardInput,                config_parse_exec_input,            0,                             offsetof($1, exec_context)
 $1.StandardOutput,               config_parse_exec_output,           0,                             offsetof($1, exec_context)
index 7fa1baf..cec9c60 100644 (file)
@@ -2174,16 +2174,17 @@ int config_parse_environ(const char *unit,
         }
 }
 
-int config_parse_pass_environ(const char *unit,
-                              const char *filename,
-                              unsigned line,
-                              const char *section,
-                              unsigned section_line,
-                              const char *lvalue,
-                              int ltype,
-                              const char *rvalue,
-                              void *data,
-                              void *userdata) {
+int config_parse_pass_environ(
+                const char *unit,
+                const char *filename,
+                unsigned line,
+                const char *section,
+                unsigned section_line,
+                const char *lvalue,
+                int ltype,
+                const char *rvalue,
+                void *data,
+                void *userdata) {
 
         const char *whole_rvalue = rvalue;
         char*** passenv = data;
@@ -2238,6 +2239,85 @@ int config_parse_pass_environ(const char *unit,
         return 0;
 }
 
+int config_parse_unset_environ(
+                const char *unit,
+                const char *filename,
+                unsigned line,
+                const char *section,
+                unsigned section_line,
+                const char *lvalue,
+                int ltype,
+                const char *rvalue,
+                void *data,
+                void *userdata) {
+
+        _cleanup_strv_free_ char **n = NULL;
+        const char *whole_rvalue = rvalue;
+        size_t nlen = 0, nbufsize = 0;
+        char*** unsetenv = data;
+        Unit *u = userdata;
+        int r;
+
+        assert(filename);
+        assert(lvalue);
+        assert(rvalue);
+        assert(data);
+
+        if (isempty(rvalue)) {
+                /* Empty assignment resets the list */
+                *unsetenv = strv_free(*unsetenv);
+                return 0;
+        }
+
+        for (;;) {
+                _cleanup_free_ char *word = NULL, *k = NULL;
+
+                r = extract_first_word(&rvalue, &word, NULL, EXTRACT_QUOTES);
+                if (r == 0)
+                        break;
+                if (r == -ENOMEM)
+                        return log_oom();
+                if (r < 0) {
+                        log_syntax(unit, LOG_ERR, filename, line, r,
+                                   "Trailing garbage in %s, ignoring: %s", lvalue, whole_rvalue);
+                        break;
+                }
+
+                if (u) {
+                        r = unit_full_printf(u, word, &k);
+                        if (r < 0) {
+                                log_syntax(unit, LOG_ERR, filename, line, r,
+                                           "Failed to resolve specifiers, ignoring: %s", word);
+                                continue;
+                        }
+                } else {
+                        k = word;
+                        word = NULL;
+                }
+
+                if (!env_assignment_is_valid(k) && !env_name_is_valid(k)) {
+                        log_syntax(unit, LOG_ERR, filename, line, 0,
+                                   "Invalid environment name or assignment %s, ignoring: %s", lvalue, k);
+                        continue;
+                }
+
+                if (!GREEDY_REALLOC(n, nbufsize, nlen + 2))
+                        return log_oom();
+
+                n[nlen++] = k;
+                n[nlen] = NULL;
+                k = NULL;
+        }
+
+        if (n) {
+                r = strv_extend_strv(unsetenv, n, true);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
 int config_parse_ip_tos(const char *unit,
                         const char *filename,
                         unsigned line,
index ec338cc..49b3b40 100644 (file)
@@ -79,6 +79,7 @@ int config_parse_syscall_archs(const char *unit, const char *filename, unsigned
 int config_parse_syscall_errno(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata);
 int config_parse_environ(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata);
 int config_parse_pass_environ(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata);
+int config_parse_unset_environ(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata);
 int config_parse_unit_slice(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata);
 int config_parse_cpu_weight(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata);
 int config_parse_cpu_shares(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata);
index c024f64..40f1d74 100644 (file)
@@ -641,7 +641,7 @@ int bus_append_unit_property_assignment(sd_bus_message *m, const char *assignmen
 
                 r = sd_bus_message_append(m, "v", "i", (int32_t) q);
 
-        } else if (STR_IN_SET(field, "Environment", "PassEnvironment")) {
+        } else if (STR_IN_SET(field, "Environment", "UnsetEnvironment", "PassEnvironment")) {
                 const char *p;
 
                 r = sd_bus_message_open_container(m, 'v', "as");
@@ -668,6 +668,11 @@ int bus_append_unit_property_assignment(sd_bus_message *m, const char *assignmen
                                         log_error("Invalid environment assignment: %s", word);
                                         return -EINVAL;
                                 }
+                        } else if (streq(field, "UnsetEnvironment")) {
+                                if (!env_assignment_is_valid(word) && !env_name_is_valid(word)) {
+                                        log_error("Invalid environment name or assignment: %s", word);
+                                        return -EINVAL;
+                                }
                         } else {  /* PassEnvironment */
                                 if (!env_name_is_valid(word)) {
                                         log_error("Invalid environment variable name: %s", word);