path_helper prepends not appends system paths

Originator:alexisgallagher
Number:rdar://14630658 Date Originated:2013-08-02
Status:Open Resolved:
Product:OS X Product Version:10.8.4
Classification:Other Bug Reproducible:Always
 
Summary:

According to its man page, the /usr/libexec.path_helper program "reads the contents of the files in the directories /etc/paths.d and /etc/manpaths.d and appends their contents to the PATH and MANPATH environment variables respectively".

This is either a bug in the utility or its docs because its actual behavior is quite different. Speaking only of PATH for instance, what it does is:

1. read the list of paths in the file /etc/paths
2. APPEND onto it the lists of paths in the files in the directory /etc/paths.d
3. mutate the PATH variable to remove any items in the list
4. APPEND onto the list the value of the PATH variable
5. Save this list as the new PATH

To rephrase this in pseudocode, what it's doing is:

1. NEWPATH = Read(/etc/paths)
2. NEWPATH = $NEWPATH +append+ Read(/etc/paths.d/*) 
3. PATH        = $PATH -minus- $NEWPATH
4. NEWPATH = $NEWPATH +append+ $PATH
5. PATH        = $NEWPATH

In other words, the man page says path_helper is appending /etc/paths.d onto the PATH. But actually, path_helper is APPENDing /etc/paths.d onto the list from /etc/paths and then effectively PREPENDing that result onto actual pre-existing PATH (as well as purging duplicates).

Steps to Reproduce:
In numbered format, detail the exact steps taken to produce the bug.
1. launch Terminal.app
2. $ export PATH=/Users/foo/bin:$PATH
3. $ eval `/usr/libexec/path_helper -s` 
4. echo $PATH

Expected Results:
Expect to see /Users/foo/bin as the first item in the path, since the man page since path_helper generates bash code that will APPEND the system paths.

Actual Results:
Actually see /Users/foo/bin behind all of the system-wide paths, because path_helper actually generates bash code that PREPENDS the system paths.

Regression:
I believe path_helper has never worked as documented.

Notes:

Why is this a problem? Obviously it is different from the man page so that is confusing.

But the real problem is that *prepending* onto PATH is a bug. Since items early in the PATH override items later in the PATH, and since the paths in /etc/paths and /etc/paths.d are system-wide configurations, prepending these values onto PATH has the effect of causing the system-wide PATH components to override any user-supplied PATH components.

For instance, suppose you want /Users/myself/bin to override /usr/bin so you define PATH=/Users/foo/bin:$PATH. After running eval `path_helper -s`, your PATH has been re-arranged so that PATH=...:/usr/bin:...:/Users/foo/bin.

This is especially a problem with the built-in zsh shell, because of OS X's default /etc/shenv. The factory-supplied /etc/zshenv sources calls path_helper. zsh always calls /etc/zshev, before it calls other startup scripts like .profile or .zshrc.  If a user is starting a login zsh, then zsh calls .profile, which sets up the path manually and probably fixes changes introduced by /etc/zshenv. But if a user is starting a non-login zsh (for instance, by lauching a subshell from the command line, or by launching zshell from inside another application like emacs), then .profile is not called, and that zsh instance inherits the PATH of its parent strangely mangled by the call to path_helper in /etc/zshev.

Steps to Reproduce (interaction with zsh):
1. open a Terminal
2. $ sudo chsh -s /bin/zsh
3. $ export PATH=/Users/foo/bin:$PATH
4. $ zsh
5  $ echo $PATH. 

Expected Results  (interaction with zsh):
Expect to see /Users/foo/bin as the first item in the path, since a subshell should inherit the PATH of its parent shell.

Actual Results  (interaction with zsh):
Actually see /Users/foo/bin behind all of the system-wide paths, since zsh startup calls /etc/zshenv which calls path_helper, which PREPENDS the system paths.

For the problem with zsh, one workaround is, at the end of .profile, to save the final computed PATH into a variable FINALPATH, and in the beginning of .zshrc to restore PATH to FINALPATH. Since FINALPATH will be properly inherited by subshells without being changed by path_helper, .zshrc will restore the correct PATH.
Summary:

According to its man page, the /usr/libexec.path_helper program "reads the contents of the files in the directories /etc/paths.d and /etc/manpaths.d and appends their contents to the PATH and MANPATH environment variables respectively".

This is either a bug in the utility or in its docs, because its actual behavior is quite different. Speaking only of PATH for instance, what it does is:

1. read the list of paths in the file /etc/paths
2. APPEND onto it the lists of paths in the files in the directory /etc/paths.d
3. mutate the PATH variable to remove any items in the list
4. APPEND onto the list the value of the PATH variable

To rephrase this in pseudocode, what it's doing is:

1. SYSTEMPATH = Read(/etc/paths)
2. SYSTEMPATH = $SYSTEMPATH +append+ Read(/etc/paths.d/*) 
3. PATH       = $PATH -minus- $SYSTEMPATH
3. PATH       = SYSTEMPATH +append+ $PATH

In other words, the man page says path_helper is appending /etc/paths.d onto the PATH. But actually, path_helper is APPENDing /etc/paths.d onto the list from /etc/paths and then effectively PREPENDing that result onto actual pre-existing PATH (as well as purging duplicates).

Steps to Reproduce (Basic behavior):
In numbered format, detail the exact steps taken to produce the bug.
1. open a Terminal
2. $ export PATH=/Users/foo/bin:$PATH
3. $ eval `/usr/libexec/path_helper -s` 
4. echo $PATH

Expected Results  (Basic behavior):
Expect to see /Users/foo/bin as the first item in the path, since the man page since path_helper generates bash code that will APPEND the system paths.

Actual Results  (Basic behavior):
Actually see /Users/foo/bin behind all of the system-wide paths, because path_helper actually generates bash code that PREPENDS the system paths.


Regression:
Describe circumstances where the problem occurs or does not occur, such as software versions and/or hardware configurations.

Notes:

Why is this a problem? Obviously it is different from the man page so that is confusing.

But the real problem is that *prepending* onto PATH is a bug. Since items early in the PATH override items later in the PATH, and since the paths in /etc/paths and /etc/paths.d are system-wide configurations, prepending these values onto PATH has the effect of causing the system-wide PATH components to override any user-specific PATH configurations.

For instance, you want /Users/myself/bin to override /usr/bin so you define PATH=/Users/foo/bin:/usr/bin. After running eval `path_helper -s`, your PATH has been re-arranged so that PATH=...:/usr/bin:...:/Users/foo/bin.

This is notably a problem with the built-in zsh shell. The factory-supplied /etc/zshenv sources calls path_helper. zsh always calls /etc/zshev, before it calls other startup script like .profile or .zshrc.  If a user is starting a login zsh, then zsh calls .profile, which sets up the path manually and fixes changes introduced by path_helper. But if a user is starting a non-login zsh (for instance, by lauching a subshell from the command line, or by launching zshell from inside another application like emacs), then .profile is not called, and that zsh instance inherits the PATH of its parent strangely mangled by the call to path_helper in /etc/zshev.

Steps to Reproduce (interaction with zsh):
In numbered format, detail the exact steps taken to produce the bug.
1. open a Terminal
2. $ sudo chsh -s /bin/zsh
3. $ export PATH=/Users/foo/bin:$PATH
4. $ zsh  # launches a subshell
5  $ echo $PATH. 

Expected Results  (interaction with zsh):
Expect to see /Users/foo/bin as the first item in the path, since a subshell should inherit the PATH of its parent shell

Actual Results  (interaction with zsh):
Actually see /Users/foo/bin behind all of the system-wide paths, since zsh startup calls /etc/zshenv which calls path_helper, which PREPENDS the system paths.

For the problem with zsh, one workaround is, in .profile, to save the final computed PATH into a variable FINALPATH, and in zshrc to set PATH equal to FINALPATH.  FINALPATH will be properly inherited by subshells without being mangled by path_helper and then .zshrc will restore the correct PATH. But this should not be necessary.

Comments


Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!