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. […]
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. 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. […]
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.