GNU ed Ate My Homework


If you have ever ventured into the archives of old UNIX books and mailing lists, you will have undoubtedly come across the legend of ed. ed (pronounced /iː diː/) is a text editor just like vim and emacs. However, contrary to its counterparts, ed comes with what an interface that could be best summarised as…

$ ed
q
?
q
?
q
?
qqqqqqqqqqqqqqqqqq
?
q
?
q

?
q
$

In spite of all of this, legend has it: “ed is the standard text editor”. So much so that most Linux distributions set /bin/ed to a Priority of -100.

$ sudo update-alternatives --config editor
There are 5 choices for the alternative editor (providing ∕usr∕bin∕editor).

  Selection    Path               Priority   Status
------------------------------------------------------------
* 0            ∕usr∕bin∕vim.gtk3   50        auto mode
  1            ∕bin∕ed            -100       manual mode
  2            ∕bin∕nano           40        manual mode
  3            ∕usr∕bin∕code       0         manual mode
  4            ∕usr∕bin∕vim.gtk3   50        manual mode
  5            ∕usr∕bin∕vim.tiny   15        manual mode

Jokes aside, despite being a frequent micro and Visual Studio Code user, I find myself toying around with ed and its source code at times. Most notably the GNU implementation of ed.

This is the story of a bug feature in GNU ed I stumbled across last year while reviewing the source code which could result in one losing their work.

A brief history in time

To understand the significance of this design flaw, we must briefly venture back to the origin of the GNU ed editor. In 1976, Brian Kernighan and P.J. Plauger published a book titled “Software Tools in Pascal” which explored writing good code using the Pascal programming language. This book contains brilliant passages such as:

“In real life, by the way, you would certainly name the routine sort, not bubble, so you could change the algorithm without upsetting users.”

— Brian Kernighan & P.J. Plauger, Software Tools in Pascal in reference to “Bubble Sort”

Why is this book of any significance? As stated in GNU’s info page on ed, the GNU implementation of ed drew inspiration from Chapter 6, “Editing”. The first GNU implementation was designed according to Kernighan and Plauger’s specification.

ed-0.1 ❯ cat THANKS
[...]

GNU ed originated with the editor algorithm from Brian W. Kernighan & P.
J. Plauger's wonderful book "Software Tools in Pascal," Addison-Wesley,
1981. [...]

While reading the book, this passage in particular stood out to me (emphasis mine):

“Error recovery is a second major influence on the design of the editor. […] edit maintains precious files, so it must be cautious. […] It must recover gracefully, for otherwise some trifling mistake could cause the loss of valuable information.

— Brian Kernighan & P.J. Plauger, Software Tools in Pascal

Keep this quote in mind. We will come back to this passage in just a bit.

Straight to the source

To examine how GNU ed works under the hood, grab a copy of the latest GNU release (version 1.18 as of writing this blog post). The source code is easy to navigate with only 8 C files.

❯ find . -name "*.c"
./buffer.c
./io.c
./carg_parser.c
./main_loop.c
./main.c
./signal.c
./global.c
./regex.c

GNU ed has a SIGHUP handler in signal.c which generates a backup file whenever the ed process encounters a SIGHUP signal. This ensures one does not lose their work if the ed process crashes. This is similar to vim swap files.

I have taken the liberty of including my own comments in the sighup_handler() function below to help guide the reader.

static void sighup_handler(int signum)
{
  // [...]

  // Listen for SIGHUP signal
  if (mutex)
  {
    sighup_pending = true;
    return;
  }
  sighup_pending = false;

  // Hardcode backup filename
  const char hb[] = "ed.hup";

  // If there are unsaved changes, write current buffer to ed.hup
  // in current working directory
  if (last_addr() <= 0 || !modified() ||
      write_file(hb, "w", 1, last_addr()) >= 0)
    exit(0); // Exit here if write was successful

  // Otherwise continue executing code and attempt to write to home directory

  // Get home directory location from $HOME environment variable
  char *const s = getenv("HOME");

  // Check if $HOME environment variable is set
  if (!s || !s[0])
    exit(1);

  // Get length of $HOME path
  const int len = strlen(s);

  // Does $HOME end with a forward slash? 1 if it does, 0 else
  const int need_slash = s[len - 1] != '/';

  // Construct full path for backup file
  char *const hup = (len + need_slash + (int)sizeof hb < path_max(0)) ? (char *)malloc(len + need_slash + sizeof hb) : 0;

  // Throw error if path construction fails
  if (!hup)
    exit(1);

  memcpy(hup, s, len);

  // Append forward slash to $HOME if missing final forward slash
  if (need_slash)
    hup[len] = '/';

  // Copy backup file contents to
  memcpy(hup + len + need_slash, hb, sizeof hb);

  // Write contents to
  if (write_file(hup, "w", 1, last_addr()) >= 0)
    exit(0);
  exit(1); /* hup file write failed */
}

Unfortunately, as indicated by my code comment, the backup file will always be named ed.hup which leaves much to be desired.

const char hb[] = "ed.hup";

There are so many reasons why this may cause issues. If GNU ed cannot write to either the home directory or the current directory’s ed.hup file, the user will lose all their work when ed crashes. To explore why this is a bad idea, I will use a scenario.

Sudo make me a sandwich

Since this is not a tutorial on how to use ed, briefly: the way ed works is you can append lines to a buffer using the a command and then write to a file using the w command. There is more to it but this should be enough for the reader to understand everything outlined below.

ed is launched in the home directory using sudo and some text is written to a buffer.

❯ ls
❯ sudo ed
a
Writing some text to a buffer using sudo
After the single '.', this process will experience
a SIGHUP signal from a different terminal window
.
141

This process experiences a SIGHUP signal called using sudo kill -SIGHUP in another terminal window. ed writes the contents of the buffer to a ed.hup file in the current working directory (i.e. $HOME).

❯ ls -l
total 4
-rw-r--r-- 1 root        wheel    141 Feb  7 13:07 ed.hup
❯ cat ed.hup
Writing some text to a buffer using sudo
After the single '.', this process will experience
a SIGHUP signal from a different terminal window

Now we navigate to a subdirectory (e.g. demo) and repeat the same process using sudo and cause ed to encounter a SIGHUP signal. Since ed favours the current working directory over the $HOME directory, the ed.hup will be written to the current working directory.

demo ❯ ls
total 4
-rw-r--r-- 1 root        wheel    141 Feb  7 13:07 ed.hup
demo ❯ cat ed.hup
Writing some text to a buffer using sudo
After the single '.', this process will experience
a SIGHUP signal from a different terminal window

Our current directory structure can be illustrated as follows by running tree in the home directory.

❯ tree
.
├── ed.hup
└── demo
    └── ed.hup

With this setup, we can finally demonstrate how ed could end up eating your homework. In the subdirectory, launch ed without sudo and cause the process to crash.

demo ❯ ed
a
This is my homework for the assignment on Tuesday!
I hope this doesn't get lost. :(
.
ed.hup: Permission denied
/Users//ed.hup: Permission denied
84
demo ❯ ls -l
total 4
-rw-r--r-- 1 root wheel 141 Feb  7 13:07 ed.hup
demo ❯ cat ed.hup
Writing some text to a buffer using sudo
After the single '.', this process will experience
a SIGHUP signal from a different terminal window
demo ❯ cd $HOME
❯ cat ed.hup
Writing some text to a buffer using sudo
After the single '.', this process will experience
a SIGHUP signal from a different terminal window

We just lost all our homework. ed failed to write to the ed.hup files in both the current working directory and the home directory because the process lacked the required privileges.

The passage from Kernighan and Plauger’s seems very fitting here.

“Error recovery is a second major influence on the design of the editor. […] edit maintains precious files, so it must be cautious. […] It must recover gracefully, for otherwise some trifling mistake could cause the loss of valuable information.

— Brian Kernighan & P.J. Plauger, Software Tools in Pascal

This is a lesson on why not to use static backup filenames.


Update (Apr, 2022)

  • @qrs notified me on Twitter that a SIGHUP
    is sent when the connection drops; not during an ed crash. I have
    updated the blog post accordingly.
  • Hacker News user
    projektfu noted
    users should consider using “sudo -e instead of sudo $EDITOR when editing
    something important with root permissions”.





Source link