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, notbubble, 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. […]
editmaintains 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. 1
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 <pid> 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/<user>/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. […]
editmaintains 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.
