how does setenv and unsetenv change the stack?

Viewed 390

I'm reading a textbook on several functions for manipulating the environment array:

If the environment array contains a string of the form name=oldvalue, then unsetenv deletes it and setenv replaces oldvalue with newvalue, but only if overwrite is nonzero. If name does not exist, then setenv adds name=newvalue to the array

int setenv(const char *name, const char *newvalue, int overwrite);

void unsetenv(const char *name);

and below is a picture that depicts the organization of the user stack when a new program starts. enter image description here

My questions are:

Q1- let's say we use unsetenv delete one element of envp, isn't it going to create a gap in envp by replacing original value with NULL? since the value is NULL, any subsequent environment elements after it will be ingored?

Q2-if we use setenv to replace a oldvalue with newvalue, if the lengh of newvalue is greater than oldvalue, then it is not going to work if setenv just modify the exiting section of "Null-terminated environment variable strings". So my guessing is, there will be a new element pushed to the stack, and the corrsponding pointer element in the envp wiill be changed accordingly, is my understanding correct?

2 Answers

below is a picture that depicts the organization of the user stack when a new program starts

That's correct but the assumption that the env APIs are restricted to manipulating just that memory is not correct. Implementations are free to make a copy of the environment variables and indeed most implementations do that. Here is an example of a libc implementation that does exactly that.

https://code.woboq.org/userspace/glibc/stdlib/setenv.c.html#__add_to_environ

/* This function is used by `setenv' and `putenv'.... */
__add_to_environ (const char *name, const char *value, const char *combined,
                  int replace)
{  
    ....
    new_environ = (char **) realloc (last_environ,
                                   (size + 2) * sizeof (char *));      
    ....
    if (__environ != last_environ)
        memcpy ((char *) new_environ, (char *) __environ,
                size * sizeof (char *));
    ....
    last_environ = __environ = new_environ;

setenv and unsetenv are not part of the C standard; that standard only provides getenv, which "searches an environment list provided by the host environment", with no specification as to how that list is created. The functions for manipulating the environment list will be found in the Posix standard (and other places for implementations which don't pretend to conform to Posix).

Posix also specifies the global variable environ, which points to the current environment list. When main() is entered:

the following variable, which must be declared by the user if it is to be used directly:

extern char **environ;

is initialized as a pointer to an array of character pointers to the environment strings.

As it happens, many operating systems put the initial environment list on the stack before calling main(), although this is not required by any standard. Moreover, the value of environ is not fixed:

Applications can change the entire environment in a single operation by assigning the environ variable to point to an array of character pointers to the new environment strings.

But that freedom comes with a lot of caveats:

After assigning a new value to environ, applications should not rely on the new environment strings remaining part of the environment, as a call to getenv(), putenv(), setenv(), unsetenv(), or any function that is dependent on an environment variable may, on noticing that environ has changed, copy the environment strings to a new array and assign environ to point to it.

Furthermore:

Any application that directly modifies the pointers to which the environ variable points has undefined behavior.

In fact, some implementations do take advantage of that flexibility, so those warnings need to be taken into account. That makes it tricky to manage dynamic memory if you create your own environ, so you might have to take a relaxed attitude to possible memory leaks (although the standard library itself probably manages its own memory more carefully).

On a Linux or BSD system, you should be able to find out a lot more locally-specific information by using the man environ command; the Posix standard itself includes the information in the description of the exec* interfaces. Also worth reading is the "Rationale" section of the getenv() description.

With fifty years of hindsight, it's easy to criticise this design. All the same, it has basically served the world pretty well, and it is so entrenched that it's hard to see how it can be changed. So like it or not, there it is.

Related