💾 Archived View for thrig.me › blog › 2023 › 04 › 12 › unchecked-chdir-considered-harmful.gmi captured on 2024-05-10 at 11:32:28. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2023-11-14)

-=-=-=-=-=-=-

Unchecked chdir Considered Harmful

Too often one will see code along the lines of

    #!/bin/sh
    cd somewhere
    something

At best this is a code smell, you'll maybe get some errors and go "whoops, my bad" while at worst it can be a total disaster.

The problem is that cd is not checked for errors, and something may run in the wrong directory. In other words, it's like saying "move the can of gasoline, then always play with the matches". Instead, use

    #!/bin/sh
    cd somewhere && something

or

    #!/bin/sh
    cd somewhere || exit 1
    something

which is "move the gasoline, and only if that works play with the matches". There is also

    #!/bin/sh
    set -e
    cd somewhere
    something

but some folks recommend against -e in scripts, especially longer ones.

http://mywiki.wooledge.org/BashFAQ/105

I use -e in short scripts, where short means a couple of lines and when there is nothing tricky like an if statement or a while loop.

/blog/2022/10/25/shell-while-loop-considered-harmful.gmi

The logic for the shell is maybe a bit weird in that 0 is success, so probably test this to get the logic aright.

    $ false ; echo $?
    1
    $ true ;echo $?
    0
    $ false && echo okay
    $ true && echo okay
    okay

The && and || are just funny ways to say "and" and "or". Blame C, or maybe something earlier. The $? variable is a mangled form of the exit status word; blame sh for that.

/tech/exit-status-word.gmi

In other languages, be sure to check the documentation to see exactly what happens so you can handle it.

http://man.openbsd.org/man2/chdir.2

https://perldoc.perl.org/functions/chdir

    $ cfu 'if (chdir("/nowhere") == -1) exit(1); printf("okay\n")'
    $ cfu 'if (chdir("/etc") == -1) exit(1); printf("okay\n")'
    okay
    $ perl -E 'chdir "/etc" and say "okay"'
    okay
    $ perl -E 'chdir "/nowhere" and say "okay"'
    $ perl -E 'chdir "/nowhere" or die "nope: $!\n"; say "okay"'
    nope: No such file or directory

It is also nice to include the name of the directory that caused the failure so that someone does not need to process trace your code to figure out what directory is failing.

    $ perl -E 'chdir "/x" or die "chdir failed '\''/x'\'': $!\n"; say "okay"'
    chdir failed '/x': No such file or directory

But that's getting a bit long for a one-liner, and probably should be uplifted to a script. Anywhere the shell quoting starts getting in the way, or the line is too long...

And here's something with an exception instead of a return value. Or rather the return value is fed into a condition system.

    (require 'sb-posix)

    (defun saydir (dir)
      (handler-case
          (progn
            (sb-posix:chdir dir)
            (format t "~a~&" (sb-posix:getcwd)))
        (error (cond)
          (format *error-output* "~a - ~a~&" cond dir))))

    (saydir "/etc")
    (saydir "/nowhere")

https://gigamonkeys.com/book/beyond-exception-handling-conditions-and-restarts.html

Or how about block-scoped working directories--the working directory is global to a process, but you can move it around automatically based on the scope of a variable.

    #!/usr/bin/env perl
    use 5.36.0;
    use warnings;
    use Cwd 'getcwd';
    use File::chdir;  # $CWD

    $CWD = '/etc';    # chdir /etc (unchecked! but throws an error)
    say getcwd;
    eval {
        local $CWD = '/tmp';
        say getcwd;   # now in /tmp ...
    };
    say getcwd;       # we're back to /etc here

https://metacpan.org/pod/File::chdir

Dress Code

Directories can vanish or never be created for all sorts of reasons. Or maybe the code has a bug and has got the directory wrong. Or the permissions got borked after a restore from backups, or the filesystem is busy corrupting itself, or the hardware is failing. Do you continue assuming everything is okay, or stop and throw a noisy error that hopefully someone will see? Philosophies vary here.

Code gets copied and pasted, so you probably want the correct form in as many places as possible so there are better odds that the next intern copies something good. Therefore I always check my chdir calls, even if the subsequent commands are pretty harmless.

Sysadmin Stuff

One approach is to have a build or configuration management system ensure that the necessary directories exist with the right permissions, so that chdir calls are less likely to fail. For example using Ansible you might ensure that /var/lock/mailman exists, as mailman may behave very poorly otherwise.

    - name: create directory /var/lock/mailman
      file: path=/var/lock/mailman state=directory owner=root group=mailman mode=2775

A chaos monkey might remove random directories that software needs (maybe on a test system?) to see how well the software behaves and reports errors. If the software behaves poorly, then maybe it could be improved with better error messages and directory checks, or configuration management used to ensure that the environment is better known to be good.

tags #unix