Skip to content

“In-place” editing of files

Now this is a real FAQ.

"How can I edit a file in-place using sed/awk/perl/whatever?"

or:

"I know that using >> I can append text to a file. How do I prepend text to a file?"

What these people usually mean is:

"How do I change/edit a file without having to create a temporary file?" (for some unknown reason)

Let's try to see what "in-place editing" really means, and why using temporary files (implicitly or explicitly) is the only way to do that reliably. Here we will limit our analysis to a pure shell environment using the commonly used filters, or stream-editing tools (sed, perl, awk and similar), which is a relatively common situation. Things start to change if we allow programming languages or special tools.

Note: almost every operating system, even when using "unbuffered" functions, maintains a low-level disk cache or buffer, so even when data is written to a file, it may not hit the disk immediately; similarly, when data is read, it may be coming from the buffer rather than disk. While this low-level OS caching is certainly something to be aware of, for the purposes of the following discussion it is entirely transparent and irrelevant to the points made, so it will be ignored here.

In-place?

Strictly speaking, "in-place" would really mean that: literally editing the very same file (the same inode). This can be done in principle, but:

  • The program or utility must be designed to do that, meaning that it should arrange for the event that the file size increases, shrinks, or stays the same. Also, it must arrange things so data that hasn't yet been read is never overwritten. None of the usual text processing tools or filters is designed for this;
  • It's dangerous: if something goes wrong in the middle of the edit (crash, disk full, etc.), the file is left in an inconsistent state.

None of the usual tools or editors do this; even when they seem to do so, they actually create a temporary file behind the scenes. Let's look at what sed and perl (two tools which are often said to be able to do "in-place" editing) do when the option -i is used.

sed

Sed has the -i switch for "in-place" editing. First of all, only some implementations of sed (GNU sed and BSD sed AFAIK) support -i. It's a nonstandard extension, and as such not universally available.
According to the documentation (at least GNU sed's), what sed does when -i is specified is create a temporary file, send output to that file, and at the end, that file is renamed to the original name. This can be verified with strace; even without strace, a simple "ls -i" of the file before and after sed operates will show two different inode numbers.
If you do use -i with sed, make sure you specify a backup extension to save a copy of the original file in case something goes wrong. Only after you're sure everything was changed correctly, you can delete the backup. The BSD sed (used on Mac OS X as well) does not accept -i without a backup extension, which is good, although it can be fooled by supplying an empty string (eg -i "").

Perl

Perl, similar to sed, has a -i switch to edit "in-place". And like sed, it creates a temporary file. However, the way Perl creates the temporary file is different. Perl opens and immediately unlink()s the original file, then opens a new file with the same name (new file descriptor and inode), and sends output to this second file; at the end, the old file is closed and thus deleted because it was unlinked, and what's left is a changed file with the same name as the original. This is more dangerous than sed, because if the process is interrupted halfway, the original file is lost (whereas in sed it would still be available, even if no backup extension was specified). Thus, it's even more important to supply a backup extension to Perl's -i, which results in the original file being rename()d rather than unlink()ed.

Another false in-place

By the way, here is a solution which is often described as "in-place" editing:

$ { rm file; command > file; } < file

("command" is a generic command that edits the file, typically a filter or a stream editor)

This works because, well, it's cheating. It really involves two files: the outer file is not really deleted by the rm command, as it's still open by virtue of the outer input redirection. The inner output redirection then really writes to a different disk file, although the operating system allows you to use the same file name because it's no longer "officially" in use at that point. When the whole thing completes, the original file (which was surviving anonymously for the duration of the processing, feeding command's standard input) is finally deleted from disk.
So, this kludge still needs the same additional disk space you'd need if you used a temporary file (ie, roughly the size of the original file). It basically replicates what Perl does with -i when no backup extension is supplied, including keeping the original file in the risky "open-but-deleted" state for the duration of the operation. So, if one must use this method, at least they should do

$ { mv file file.bak; command > file; } < file

But then, doing this is hardly different from using an explicit temporary file, so why not do that? And so...

Using an explicit temporary file

So, generally speaking, to accomplish almost any editing task on a file, temporary files should be used. Sure, if the file is big, creating a temporary file becomes more and more inefficient, and requires that an amount of available free space roughly the same size of the original file is available. Nonetheless, it's by far the only right and sane way to do the job. Modern machines should have no disk space problems.

The general method to edit a file, assuming command is the command that edits the file, is something along these lines:

$ command file > tempfile && mv tempfile file
# or, depending on how "command" reads its input
$ command < file > tempfile && mv tempfile file

To prepend data to a file, similarly do:

$ { command; cat file; } > tempfile && mv tempfile file

where command is the command that produces the output that should be prepended to the file.

The use of "&&" ensures that the original file is overwritten only if no errors occurred during the processing. That is to safeguard the original data in case something goes wrong. If preserving the original inode number (and thus permissions and other metadata) is a concern, there are various ways, here are two:

$ command file > tempfile && cat tempfile > file && rm tempfile
# or
$ cp file tempfile && command tempfile > file && rm tempfile

These commands are slightly less efficient than the previous methods, as they do two passes over the files (adding the cat in the first method and the cp in the second). In most cases, the general method works just fine and you don't need these latter methods. If you're concerned about the excruciating details of these operations, this page on pixelbeat lists many more methods to replace a file using temporary files, both preserving and not preserving the metadata, with a description of the pros and cons of each.

In any case, for our purposes the important thing to remember of these methods is that the old file stays around (whether under its original name or a different one) until the new one has been completely written, so errors can be detected and the old file rolled back. This makes them the preferred method for changing a file safely.

Sponges and other tricks

There are alternatives to the explicit temporary file, however they are somewhat inferior in the writer's opinion. On the upside, they have the advantage of (generally) preserving the inode and other file metadata.

One such tool is sponge, from the moreutils package. Its use is very simple:

command file | sponge file

As the man page says, what sponge does is "reads standard input and writes it out to the specified file. Unlike a shell redirect, sponge soaks up all its input before opening the output file. This allows for constructing pipelines that read from and write to the same file".
So, sponge accumulates output coming from command (in memory or, when it grows too much, guess where? in a temporary file), and does not open file again for writing until it has received EOF on input. When the incoming stream is over, it opens file for writing and writes the new data into it (if it had to use a temp file, it just rename()s that to file which is more efficient, although this results in changing the file's inode).

A barebone implementation of a sponge-like program in Perl would be like

#!/usr/bin/perl 
# sponge.pl
while(<STDIN>) {
  push @a, $_;
}
# EOF here
open(OUT, ">", $ARGV[0]) or die "Error opening $ARGV[0]: $!";
print OUT @a;
close(OUT);

This keeps everything in memory; it can be extended to use a temporary file (and, for that matter, it can likely be extended to also perform whatever job the filter that feeds its input does, but then we are leaving the domain of this article).
With this, one could do

command file | sponge.pl file

A similar functionality can be implemented using awk:

# sponge.awk
BEGIN {
  outfile = ARGV[1]
  ARGC--
}
{ a[NR] = $0 }
END {
  for(i=1;i<=NR;i++)
    print a[i] > outfile
}

These methods work, and they do edit the same file (inode), however they have the disadvantage that if the amount of data is huge, there is a moderately long period of time (while data is being written back to the file) during which part of the data is only in memory, so if the system crashes it will be lost.

The good old ed

If the editing to be done is not too complex, another alternative is the good old ed editor. A peculiarity of ed is that it reads its editing commands, rather than the data, from standard input. For example, to prepend "XXX" to each line in the file, It can be used as follows:

printf '%s\n' ',s/^/XXX/' w q | ed -s file

(the -s switch is to prevent ed from printing information on how many bytes it read/wrote; there's no harm in omitting it)

At least in most implementations, ed does create a temporary file, which it uses as support for the editing operations; when it is asked to save the changes, it writes them back to the original file. This way of working is mandated by the POSIX standard, that says that

The ed utility shall operate on a copy of the file it is editing; changes made to the copy shall have no effect on the file until a w (write) command is given. The copy of the text is called the buffer.

So, it should be clear that ed presents the same shortcomings of the sponge-like methods; in particular, when it's requested to perform a write (the "w" command), ed truncates the original file and writes the contents of the buffer into it. If the amount of data is huge, this means that there's a moderately long time window during which the file is in an inconsistent state, until ed has written back the whole data (and no other copy of the original data exists). Consider this if you're worried about unexpected things happening in the middle of the process.

"But I don't want to use a temp file!"

Ok. Having said all this, we still see that, for some mysterious reasons, people still try to do away with temporary files, and come up with "creative" solutions. Here are some of them. They are all broken and must not be used for any reason. "Kids, don't do this at home".

The pipe of death

People sometimes try this:

$ cat file | command > file     # doesn't work

or also

$ command file | cat > file     # doesn't work

Obviously none of these work, because the file is truncated by the shell as soon as the last part of the pipeline is run (for any practical purpose this means "immediately"). But, after thinking a bit about that, something "clicks" in the mind of whoever is writing the code, which generally leads to the following "clever" hack:

$ command file | { sleep 10; cat > file; }    # DO NOT DO THIS

And that indeed appears to work. Except it's utterly wrong, and may bite you when you least expect it, with very bad consequences (things that seem to work are almost always much worse and dangerous than things that patently don't, because they can give a false sense of security). So, what's wrong with it?

The idea behind the hack is "let's sleep 10 seconds, so the command can read the whole file and do its job before the file is truncated and the fresh stuff coming from the pipe can be written to it". Let's ignore the fact that 10 seconds may or may not be appropriate (and the same goes for whatever value you choose to use). There's something much more seriously, fundamentally wrong there. Let's see what happens if the file is even moderately big. The right hand side of the pipeline will not consume any data coming from the pipe for 10 seconds (or however many seconds). This means that whatever command outputs, goes into the pipe and just sits there, at least until sleep is finished. But of course, a pipe cannot hold an infinite amount of data; rather, its size is usually fairly limited (like some tens of kilobytes, although it's implementation-dependent). Now, what happens if the output of command fills the pipe before sleep has finished? It happens that at some point a write() performed by command will block. If command is like most programs, that means that command itself will block. In particular, it will not read anything else from file. So it's entirely possible, especially if the input file is moderately large, and the output is accordingly large, that command will block without having read the input file fully. And it will remain blocked until sleep ends.

When that finally happens, there are at least two possible outcomes, depending on how exactly command reads its input and writes its output, the system's stdio buffering, the process scheduler, the shell and possibly some other factor (more on stdio buffering later).
If you're lucky (yes), you will end up writing a pipe's worth of output data into file and nothing more (of course losing its original contents, and the subsequent output that would have come from command). This is if you're lucky. Another, much worse, possibility is that command is unblocked when some of its output has already been written to file by the output redirection. What happens in that case is that the pipeline will enter an endless self-feeding loop, whereby cat writes the output of command to the file, but immediately after that command reads that same data again as its input, over and over. This causes file to grow without bounds as much as it can, possibly filling all the available space in the filesystem.

An alternative way of writing the same bad code, which probably makes the problem more evident is

$ cat file | { sleep 10; command > file; }    # DO NOT DO THIS

Again, cat will block if file is big and the pipe is filled before the 10 seconds have passed.

It probably helps to state it more clearly: the above code has the potential to completely trash your system and render it unusable. Do NOT use it, for any reason. If you don't believe that and want to see it for yourself, try this on a filesystem that you can fill (a loopback filesystem is strongly suggested here):

# create a 100MB file
# dd if=/dev/zero of=loop.img bs=1M count=100
100+0 records in
100+0 records out
104857600 bytes (105 MB) copied, 2.58083 s, 40.6 MB/s
# make a filesystem on it
# mke2fs loop.img
mke2fs 1.41.9 (22-Aug-2009)
loop.img is not a block special device.
Proceed anyway? (y,n) y
...
# mount it
# mount -o loop loop.img /mnt/temp
# cd /mnt/temp
# Just create a moderately big file
# seq 1 200000 > file
# Here we go
# sed 's/^/XXX/' file | { sleep 10; cat > file; }    # DO NOT DO THIS
cat: write error: No space left on device
# ls -l
total 97611
-rw-r--r-- 1 root root 99549184 2010-04-02 20:08 file
drwx------ 2 root root    12288 2010-04-02 18:48 lost+found
# tail -n 3 file
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX8483
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX8484
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX#
# Uh-oh...

As the whole thing is completely nondeterministic, you might not get the same result (I had to repeatedly run it a few times myself and on different systems before getting the error). Nonetheless, you'll still have problems; if you don't enter the loop, then you'll end up with lots of missing data. Again: do NOT do the above for any reason. Imagine what could happen if this dangerous code is run as root by some cron job every night on an important server (hint: nothing good).

Buffers and descriptors

Another "solution" that is seen from time to time is something like

$ awk 'BEGIN{print "This is a prepended line"}{print}' file 1<>file     # DO NOT DO THIS

This prevents the file from being truncated as it uses the <> notation which opens the file for reading and writing. So it would seem that this is the holy grail of in-place editing. But is it?

To understand why this "works" and why (you guessed it) it must not be used, let's approach the topic from a general point of view.

In general, during the editing or changing of the file, the overall amount of data that has to be written can be smaller, the same size, or larger than the data that it is supposed to replace. This poses problems.

Let's start with the case where the new data is the same size as the old, which is also the only one that can be made to work (although, again, it's not recommended). For example, with the code

awk '{gsub(/foo/, "bar"); print}'

the old data and the new data are all three characters; we also know that the old data is read before the new data is written out, so we may be able to do real "in-place" editing by doing something like

awk '{gsub(/foo/, "bar"); print}' file 1<>file     # DO NOT DO THIS

this works because the "1<>file" syntax opens the file in read/write mode, and thus it's not truncated to zero length. Obviously, if the file is 1GB and the system crashes at some point in between, the data will be inconsistent. But it should be clear by now that we are already deep in the "don't do this" zone.

Let's see what happens if the replacement data is smaller than the original data.

$ cat file
100
200
300
400
500
$ sed 's/00/A/' file 1<>file
$ cat file
1A
2A
3A
4A
5A

500

This is expected. Once the replacement data has been written back, what was in the original file past that point is left there, and sed (or other similar utilities) does not invoke truncate() or ftruncate(), because they are not designed to be used this way (and rightly so). So this can't work.

Now let's look at the most dangerous case: the changed data is longer than the original. It's the most dangerous because, unlike the previous one, sometimes it works and could lead people to mistakenly think that it can be safely done.
In theory, this shouldn't even work; after all, the first time a chunk of data that is bigger than the original data is written back, some data that has not been read yet will be overwritten, leading to data corruption at a minimum. However, there are some circumstances that may make it look as if it worked, although (did I say that already?) it shouldn't really be done as it's quite risky. The following analysis was performed on Linux and is thus specific to that system, but the concepts are general.

The first thing to observe is that, when using filters or streaming editors, reads happen before writes (obviously). So, say, the program might read 10 bytes, and write back 20 bytes, or so. This doesn't seem to help much, but the second thing to observe is that I/O operations are usually buffered; that is, most programs use buffered I/O calls, like fread() or fwrite(). These calls don't read and write directly to the file (as read() and write() would), but instead use some internal buffers (usually implemented by the C library) whose purpose is to "accumulate" data; when the application fread()s, say, 10 bytes, 4096 bytes are read instead and put in the read buffer (and 10 are returned to the application); when the application fwrite()s 20 bytes, these are written into an output buffer, and only when this buffer is full (again, perhaps 4096 bytes) it is written to the actual file. If the application's standard I/O descriptors are not connected to a terminal (and if the application does not call read()/write() directly, of course), I/O for the program will be buffered.
We can confirm that this is indeed the case when, for example looking at the output of strace, we see that reads and writes happen in big chunks which do not correspond to the expected usage pattern of the application. For example, on this system the buffer size seems to be 4096 bytes. How does this matter for our problem? It matters because this buffering, specifically output buffering, is what makes the "write back more than was read" case work in some cases (but which is, instead, is a recipe for disaster).

So how does output buffering help? Let's go through the awk example we started from:

$ cat file
This is line1
This is line2
This is line3
This is line4
This is line5
$ awk 'BEGIN{print "This is a prepended line"}{print}' file 1<>file     # DO NOT DO THIS
$ cat file
This is a prepended line
This is line1
This is line2
This is line3
This is line4
This is line5

This apparently miraculous outcome is possible because of I/O buffers. Let's have a look at the output of strace:

$ strace awk 'BEGIN{print "This is a prepended line"}{print}' file 1<>file
...
open("file", O_RDONLY)              = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=70, ...}) = 0
ioctl(3, SNDCTL_TMR_TIMEBASE or TCGETS, 0x7fff555abe30) = -1 ENOTTY (Inappropriate ioctl for device)
fstat(3, {st_mode=S_IFREG|0644, st_size=70, ...}) = 0
fcntl(3, F_SETFD, FD_CLOEXEC)           = 0
read(3, "This is line1\nThis is line2\nThis"..., 70) = 70
read(3, "", 70)                         = 0
close(3)                                = 0
write(1, "This is a prepended line\nThis is li"..., 95) = 95
exit_group(0)

There's something strange there: why did read() happen before write(), even if in the awk code there is a print statement right in the BEGIN block which should clearly be executed before any data is read? As we said, I/O is buffered, so even if the application writes, data isn't really written to the file until there's enough of it in the buffer, or the file is closed or flushed. So the awk code print "This a prepended line" ends up putting the string into some C library write buffer, not on the file. This is not apparent from the strace output, as it happens entirely in user space without system calls. Then the execution continues, and awk enters its main loop, which requires reading the file. Now, the buffered I/O tries to read a whole chunk of data (the bolded read() above), which in this case is the whole file, and this is stored in some input buffer. Then awk executes the main body of its code, which simply copies its input to its output. Both operations are buffered, so reading reads from the read buffer, and writing writes to the output buffer (which already contains the line printed in the BEGIN block, so further output is appended to that). Nothing of this appears in strace, as it's all in userspace. Finally, the file is closed (because the program terminates), and descriptor 1 is flushed and write() is finally invoked (in bold above). The result of all this is that the output buffer at the time of write()ing contains exactly the line we wanted to prepend, plus the original lines in the file, so that's what's written back to the file.

Let's use ltrace, which can show library calls as well as system calls, to confirm our guesses (the output has been cleaned up in some places for clarity):

$ ltrace -S -n3 awk 'BEGIN{print "This is a prepended line"}{print}' file 1<>file
...
   121	   fileno(0x7f06e270b780)                        = 1
...
   213	   fwrite("This is a prepended line", 1, 24, 0x7f06e270b780 <unfinished ...>
   214	      SYS_fstat(1, 0x7fff663f4050)               = 0
   215	      SYS_mmap(0, 4096, 3, 34, 0xffffffff)       = 0x7f06e2daa000
   216	   <... fwrite resumed> )                        = 24
   217	   __errno_location()                            = 0x7f06e2d9a6a8
   218	   fwrite("\n", 1, 1, 0x7f06e270b780)            = 1
...
   226	   open("file", 0, 0666 <unfinished ...>
   227	      SYS_open("file", 0, 0666)                  = 3
   228	   <... open resumed> )                          = 3
...
   246	   read(3,  <unfinished ...>
   247	      SYS_read(3, "This is line1\nThis is line2\nThis"..., 70) = 70
   248	   <... read resumed> "This is line1\nThis is line2\nThis"..., 70) = 70
...
   254	   fwrite("This is line1", 1, 13, 0x7f06e270b780) = 13
   255	   __errno_location()                            = 0x7f06e2d9a6a8
   256	   fwrite("\n", 1, 1, 0x7f06e270b780)            = 1
   257	   _setjmp(0x64d650, 0x1d64ceb, 0x1d635b0, 0, 0) = 0
   258	   __errno_location()                            = 0x7f06e2d9a6a8
   259	   fwrite("This is line2", 1, 13, 0x7f06e270b780) = 13
   260	   __errno_location()                            = 0x7f06e2d9a6a8
   261	   fwrite("\n", 1, 1, 0x7f06e270b780)            = 1
   262	   _setjmp(0x64d650, 0x1d64cf9, 0x1d635b0, 0, 0) = 0
   263	   __errno_location()                            = 0x7f06e2d9a6a8
   264	   fwrite("This is line3", 1, 13, 0x7f06e270b780) = 13
   265	   __errno_location()                            = 0x7f06e2d9a6a8
   266	   fwrite("\n", 1, 1, 0x7f06e270b780)            = 1
   267	   _setjmp(0x64d650, 0x1d64d07, 0x1d635b0, 0, 0) = 0
   268	   __errno_location()                            = 0x7f06e2d9a6a8
   269	   fwrite("This is line4", 1, 13, 0x7f06e270b780) = 13
   270	   __errno_location()                            = 0x7f06e2d9a6a8
   271	   fwrite("\n", 1, 1, 0x7f06e270b780)            = 1
   272	   _setjmp(0x64d650, 0x1d64d15, 0x1d635b0, 0, 0) = 0
   273	   __errno_location()                            = 0x7f06e2d9a6a8
   274	   fwrite("This is line5", 1, 13, 0x7f06e270b780) = 13
   275	   __errno_location()                            = 0x7f06e2d9a6a8
   276	   fwrite("\n", 1, 1, 0x7f06e270b780)            = 1
   277	   read(3,  <unfinished ...>
   278	      SYS_read(3, "", 70)                        = 0
   279	   <... read resumed> "", 70)                    = 0
   280	   __errno_location()                            = 0x7f06e2d9a6a8
...
   284	   close(3 <unfinished ...>
   285	      SYS_close(3)                               = 0
   286	   <... close resumed> )                         = 0
   287	   free(0x1d64cd0)                               = <void>
   288	   __errno_location()                            = 0x7f06e2d9a6a8
   289	   fflush(0x7f06e270b780 <unfinished ...>
   290	      SYS_write(1, "This is a prepended line\nThis is"..., 95) = 95
   291	   <... fflush resumed> )                        = 0
   292	   fflush(0x7f06e270b860)                        = 0
   293	   exit(0 <unfinished ...>
   294	      SYS_exit_group(0 <no return ...>
   295	+++ exited (status 0) +++

Awk uses buffered I/O (ie, fread()/fwrite()), and in line 121 the actual file descriptor corresponding to the object at address 0x7f06e270b860 (presumably a pointer to a FILE object for stdout) is obtained, which is 1 (ie, standard output).
Lines 213-218 are where the print statement in the BEGIN block is executed; note that no write system call is performed, so data is written to the C library buffer, not to the file. Lines 226-228 open the file for reading, as part of awk's normal processing before starting its loop, and lines 246-248 read the contents of the file (since input is buffered, the call to fread() triggers a read() system call that reads the whole file in the input buffer). Line 254 and following is where the main body of the awk program (ie, "{print}") is executed: again, all the data goes into the C library buffer, which already contained the line printed in the BEGIN block.
Line 284 closes the file descriptor used to read the file. Up to here, the file is still unchanged. Then at line 289, standard output is flushed, and only now data is written to the file (line 290).

So the output buffer effectively saves our bacon here. As a further test, let's run the command again but with output buffering disabled (using the neat stdbuf utility from GNU coreutils):

$ stdbuf -o0 awk 'BEGIN{print "This is a prepended line"}{print}' file 1<>file
# hangs, press ctrl-C
^C
$ ls -l file
-rw-r--r-- 1 waldner waldner 10298496 Jan 28 15:04 file
$ head -n 20 file
This is a prepended line
This is a prepended line
e2
This is line3
This is line4
This is line5
s is line4
This is line5
s is line4
This is line5
s is line4
This is line5
s is line4
This is line5
s is line4
This is line5
s is line4
This is line5
s is line4
This is line5

So this finally shows that (as expected) writing more than is read can't work, and it's only because of I/O buffering that it sometimes appears to work. And obviously, it's not known a priori whether I/O will be buffered (depends on the actual program code, and other things). Even if the POSIX standard requires that some functions use buffered I/O if they don't refer to "an interactive device", there's no guarantee that the application will use those functions (eg, fread() or fwrite()). The application may very well use read() and write() directly, which of course are not buffered. Even if the buffered functions are used, nothing prevents the application from calling fflush() whenever it wishes, or even from disabling buffering entirely. If that happens, again hell breaks loose.

But if the above is not enough, let's continue this wicked game, and let's assume that we can rely on output buffering. Even in this case, we soon run into trouble.

Obviously, write buffering only provides a temporary storage for an amount of data that is less than or equal to the buffer size itself (eg, 4096 bytes). When the buffer is full, it is written out to the file. This means that the (already poor) protection provided by output buffering vanishes as soon as the size difference between read data and written data becomes greater than the buffer size. At that point, the output buffer is written to disk, and overwrites data that has not been read yet, thus disaster ensues again (data loss at a minimum, and potential endless loop with the file growing, depending on how the program exactly transforms the data). It's easy to verify; sticking to awk again,

# Let's prepend more than 4096 bytes to our file
$ awk 'BEGIN{for(i=1;i<=1100;i++)print i}1' file 1<>file
# after a while...
awk: write error: No space left on device
# Let's recreate the file
$ printf 'This is line 1\nThis is line 2\nThis is line 3\nThis is line 4\nThis is line 5\n' > file
# Let's try writing 5000 bytes at once now
$ awk 'BEGIN{printf "%05000d\n", 1}1' file 1<>file
$ cat file
[snip]
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
$ wc -l file
2 file

As can be seen above, whether the outcome is endless loop or "just" data corruption depends on how the program transforms the data.

A hall of shame

Now, knowing how it works, just for completeness, here is a little hall of shame that combines "ideas" from the bad techniques just described. It's provided to clearly state that these commands (and similar ones) must never ever be used.

$ sed 's/foo/longer/g' file 1<>file   # DO NOT DO THIS

# prepend data to a file. Some smart cats detect this and complain
$ { command; cat file; } 1<>file   # DO NOT DO THIS

# let's throw pipes into the mix

# prepend a file, bash
$ cat <(cat file1 file2) 1<>file2   # DO NOT DO THIS

# prepend a file, POSIX sh
$ cat file1 file2 | cat 1<>file2   # DO NOT DO THIS

# prepend text, "fooling" cat
$ { command; cat file; } | cat 1<>file   # DO NOT DO THIS

Those using pipes are even more dangerous (if possible), as they introduce concurrency, which make the outcome even more unpredictable (process substitution in bash is implemented using a pipe, although it's not apparent from the above). Depending on how the processes are scheduled and where the data is buffered, the result can vary from success (unlikely), to self-feeding loop, to corrupted data. Again, try it for yourself a few times and you'll see. As an example, here's what happens with the last command above to prepend text to a file:

$ seq 100000 105000 > file
$ wc -l file
5001 file
$ { seq 1 2000; cat file; } | cat 1<>file
$ wc -l file
208229 file       # should be 7001
$ seq 100000 105000 > file
$ wc -l file
5001 file
$ { seq 1 2000; cat file; } | cat 1<>file
$ wc -l file
194630 file       # should be 7001
$ seq 100000 105000 > file
$ wc -l file
5001 file
# now let's add more data
$ { seq 1 20000; cat file; } | cat 1<>file
^C
$ ls -l file
-rw-r--r-- 1 waldner users 788046226 2010-05-09 15:26 file
# etc.

Conclusions

The bottom line of all this is that, to perform almost any editing/changing task on a file, you must use a temporary file, and for very good reasons. Also, it's much better if that file is explicit.

Update 28/12/2012:

It was brought to my attention that there is another way to write to the file without creating a temporary file. Before showing it, let me repeat that this is a bad idea, unless you REALLY know what you're doing (and even then, think many times about it before doing it).

So, at least with bash, the various expansions that the shell performs (variable expansion, command substitution, etc.) happen before redirections are set up; this makes sense, as one could do

mycommand > "$somefile"

so the variable $somefile needs to be expanded before the redirection can be set up. How can this be exploited for in-place editing (true in-place, in this case)? Simple, by dong this:

printf '%s\n' "$(sed 's/foo/bar/g' file)" > file   # another one for the hall of shame

Of course, the output of the command substitution is temporarily stored in memory, so if the file is big, one may get errors like:

$ printf '%s\n' "$(sed 's/foo/bar/g' bigfile)" > bigfile
-bash: xrealloc: ../bash/subst.c:658: cannot allocate 378889344 bytes (1308979200 bytes allocated)
Connection to piggy closed.

Which, must be admitted, isn't as bad as some of the methods previously described because in this case, at least, the file isn't touched, that is, it's still as it was before running the command, rather than some intermediate inconsistent state.

Another, perhaps less obvious, problem with that approach is that (again, at least with bash) literal strings (such as the second argument to printf in the example) cannot contain ASCII NULs, so if the output of command substitution contains them, they will be missing in the result.

Update 2 23/02/2013:

For those who want real in-place editing, the Tie::File module of Perl is a way to do true in-place editing (same file, same inode) which also takes care of doing all the dirty work of expanding/shrinking the file. Basically, it presents the file as an array, and the code just has to modify the array; the changes are then converted to actual file changes on disk. Of course, all the caveats apply (file is inconsistent while it's being operated on) and, on top of that, performance will degrade as the file size (or amount of changes) increase. As they say, you can’t have your cake and eat it too.

Nevertheless, considering what it has to do, the Tie::File module is a really awesome piece of software.

As an example of a very basic usage, here are some simple operations (but there's no limit to the possibilities).

#!/usr/bin/perl
 
use Tie::File;
use warnings;
use strict;
 
my $filename = $ARGV[0];
my @array;
 
tie @array, 'Tie::File', $filename or die "Cannot tie $filename";
 
$array[9] = 'newline10';      # change value of line 10
splice (@array, 0, 5);        # removes first 5 lines
for (@array) {
  s/foo/longerbar/g;         # sed-like replacement
}
 
# etcetera; anything that can be done with an array can be done
# (but see the CAVEATS section in the documentation)
 
untie @array;

Sample run:

$ cat -n file.txt 
     1	this line will be deleted 1
     2	this line will be deleted 2
     3	this line will be deleted 3
     4	this line will be deleted 4
     5	this line will be deleted 5
     6	this line will not be deleted foo
     7	foo foo abc def
     8	hello world
     9	something normal
    10	something weird
    11	something foobar
$ ls -i file.txt 
11672298 file.txt
$ ./tiefile_test.pl file.txt 
$ cat -n file.txt 
     1	this line will not be deleted longerbar
     2	longerbar longerbar abc def
     3	hello world
     4	something normal
     5	newline10
     6	something longerbarbar
$ ls -i file.txt 
11672298 file.txt

Yes, it really is that simple. Now don't complain that it's slow or a crash messed up things.

Update 3 17/03/2015:

If (and only if)

  • the replacement is exactly the same length of the part to be replaced
  • you know exactly the position in the file where the replacement should be written
  • you're felling brave

another possibility is the venerable dd program. The trick is that it's possible to tell dd to not delete the output file, using the conv=notrunc option. So if we know that the text we want to replace starts at byte 200, we can do:

$ printf "newtext" | dd of=myfile seek=199 bs=1 conv=notrunc
7+0 records in
7+0 records out
7 bytes (7 B) copied, 3.4565e-05 s, 86.8 kB/s

and have the original file overwritten just where it needs to be. The reason for the "same length" requirement should hopefully be obvious.
No need to say that it's quite easy to screw up, but depending on the exact use case (eg binary editing), this might be a viable solution.

Print lines in reverse order

On a popular Linux magazine, one of the writers asked people to come up with different ways to print the lines of a file in reverse order, ie like what the program tac does.

So here are some ways to do that, almost all of which are silly, useless, crazy, dangerous and inefficient. You have been warned.

Some can work with both standard input and a real file, some only work with a real file (the examples all use a file). The shell examples use some bash-specific features for convenience, although they could all be rewritten to use only standard constructs (except the one that uses arrays).

No explanations to avoid spoiling the fun (some are obvious though).

tac file
cat -n file | sort -k1,1rn | cut -f 2-
awk '{a[NR]=$0} END {for(i=NR;i>0;i--)print a[i]}' file
perl -e 'print reverse <>' file
perl -ne 'push @a,$_; print reverse @a if eof' file
sed '1!G;h;$!d' file
awk '{out = $0 s out; s = RS} END {print out}' file
i=0
while IFS= read -r arr[i]; do ((i++)); done < file
for((j=i-1;j>=0;j--)); do printf "%s\n" "${arr[$j]}"; done
sed ':a;$!{N;ba;}; s/$/\x1/; 
     :b; s/\n\{0,1\}\([^\n]*\)\x1\(.*\)/\x1\2\n\1/; /^\x1/!bb; 
     s/\x1\n//' file
i=0
while true; do
  if gawk -v n="$i" 'NR==n{exit}END{print;exit NR-1}' file; then
    break
  else
    i=$?
  fi
done
# NEVER do this
touch outfile
while IFS= read -r line; do
  { rm outfile; { printf "%s\n" "$line"; cat; } > outfile; } < outfile
done < file
cat outfile
rm outfile
i=0
while IFS= read -r line; do
  ((i++))
  printf "%s\n" "$line" > "$i".txt
done < file

while [ $i -gt 0 ]; do
  cat $i.txt
  ((i--))
done

rm *.txt
# same idea as above, unnecessarily extra-complicated...
shift_names() {
  n=$1
  while [ $n -gt 0 ]; do
    mv "$n.txt" "$((n+1)).txt"
    ((n--))
  done
}

i=0
while IFS= read -r line; do
  shift_names $i
  printf "%s\n" "$line" > 1.txt
  ((i++))
done < file

for((j=1;j<=i;j++)); do
  cat $j.txt
done

rm *.txt
{
printf '<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body>
    <a:sort
         soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" 
         xmlns:a="http://linuxtoolbox.kwfgrid.net">  
      <a:params>-k1,1nr</a:params>
      <a:value>'

recode utf8..xml < file | sed = | sed 'N;s/\n/ /' | awk -v ORS='&#xa;' 1

printf '</a:value>
    </a:sort>
  </soapenv:Body>
</soapenv:Envelope>'
} | curl -s -o - -d @- \
-H 'Content-Type: application/soap+xml;charset=utf-8' \
-H 'SOAPAction: ""' \
'http://www.gridworkflow.org:8080/linuxtoolbox/services/Sort' |\
xmlstarlet sel -T -t -v '//sortReturn' | perl -pe 's/^\d+ //'

If you can think of other ways (for example based on different ideas, more convoluted, or using other programming languages, etc.) contributions are welcome.

Update 02/01/2011 - here's another one:

LC_ALL=C awk -v x="0" '{y += length($0) + 1; x = y RS y FS x}END{print substr(x, index(x, RS) + 1)}' file | \
while IFS=" " read -r a b; do
  dd skip=$b count=$((a - b)) bs=1 2>/dev/null < file
done

Joining files with awk

Here the task is: given a number of input files (for example comma-separated), produce an output where each line is composed by the string concatenation of the corresponding lines from each input file. "Corresponding" means that lines in the input files have a key, and output lines should be joined based on that key. An example will make things clear.

$ cat input1.csv
1,line1
2,line2
3,line3
4,line4
$ cat input2.csv
1,aaaa
2,bbbb
3,cccc
4,dddd
6,eeee
7,ffff
$ cat input3.csv
2,xxxx
4,yyyy
6,zzzz

Data here is bogus, but the important points are:

  • The key is field 1. Here it's a number, but it can be anything (although that will affect output ordering). If there's no explicit key, line numbers may be used as an implicit key
  • Not all keys are present in all files. When a key was missing from an input file, the output should have an empty field.

Based on the above, the desired output is one of the following variations (or some modification thereof):

line1,aaaa,
line2,bbbb,xxxx
line3,cccc,
line4,dddd,yyyy
,eeee,zzzz
,ffff,

or, with keys prepended,

1,line1,aaaa,
2,line2,bbbb,xxxx
3,line3,cccc,
4,line4,dddd,yyyy
6,,eeee,zzzz
7,,ffff,

Essentially the output should be a matrix of M columns x N rows, where M is the number of input files, and N is the number of keys found across all input files. A variation may be introduced when the key is numeric, as one may or may not want to "fill in" the gaps between keys that don't exist in the input by creating lines of empty fields. In the above example, a line for key 5 could be created:

5,,,

How to accomplish this or not will be discussed later.

Generic method

This technique reads all the input files into an associative array, tracking M and N, and prints the array at the end.

# join1.awk
BEGIN { FS="," }
FNR==1 {
  # track number of input files (columns)
  ncols++
}
{
  # store data ($2) in the array
  data[$1,ncols] = $2
  keys[$1]
}
END {
  for(key in keys) {
    outline=key
    for(col=1;col<=ncols;col++) {
      outline = outline "," data[key,col]
    }
    print outline
  }
}

The above program loops over the keys using the for(key in keys) construct, which returns keys in random order. Here is a test run over the sample input:

$ awk -f join1.awk *.csv
4,line4,dddd,yyyy
6,,eeee,zzzz
7,,ffff,
1,line1,aaaa,
2,line2,bbbb,xxxx
3,line3,cccc,

(in this and the following examples, output lines have their keys prepended; modifying the code to not print keys is trivial).

This is appropriate for the general case, but may not give the desired result if keys are numeric and ordering is desired. GNU awk users can set WHINY_USERS to enforce key ordering, but let's look at a more general solutions.

Numeric keys, filling the gaps

So we want to print keys in order, and create any key that may be missing. The following code accomplishes that:

# join2.awk
BEGIN { FS="," }
FNR==1 {
  # track number of input files (columns)
  ncols++
}
{
  # store data ($2) in the array
  data[$1,ncols] = $2
  if($1>maxkey) maxkey=$1
}
END {
  for(key=1;key<=maxkey;key++) {
    outline=key
    for(col=1;col<=ncols;col++) {
      outline = outline "," data[key,col]
    }
    print outline
  }
}

Note that now it is not necessary to store keys in an array; since keys are numeric, it's enough to track the maximum value seen (and perhaps the minimum, if not known in advance - here we assume 1, but again it's easy to modify the code to track the minimum).

$ awk -f join2.awk *.csv
1,line1,aaaa,
2,line2,bbbb,xxxx
3,line3,cccc,
4,line4,dddd,yyyy
5,,,
6,,eeee,zzzz
7,,ffff,

Numeric keys, no added keys

If we don't want extra keys to be created, we need to reintroduce tracking of keys, and avoid printing a given key if it didn't exist in the input:

# join3.awk
BEGIN { FS="," }
FNR==1 {
  # track number of input files (columns)
  ncols++
}
{
  # store data ($2) in the array
  data[$1,ncols] = $2
  keys[$1]
  if($1>maxkey) maxkey=$1
}
END {
  for(key=1;key<=maxkey;key++) {
    if(key in keys) {
      outline=key
      for(col=1;col<=ncols;col++) {
        outline = outline "," data[key,col]
      }
      print outline
    }
  }
}
$ awk -f join3.awk *.csv
1,line1,aaaa,
2,line2,bbbb,xxxx
3,line3,cccc,
4,line4,dddd,yyyy
6,,eeee,zzzz
7,,ffff,

DNSSEC verification with dig

DNSSEC is the (not so) new technology that will finally fix the trust problems inherent in the DNS, making spoofing and forging much harder or impossible. Or at least, that's what everyone hopes.

Unfortunately, although the idea behind the technology is simple (it basically boils down to using well-known public key cryptography techniques to verify signatures attached to DNS replies), the details are scaringly complex, and comprehension is not helped by the fact that the final specification in use today was changed a few times since the first proposal.

Without having to set up a real DNS server, it's possible to observe the verification process by using only the dig utility. As the man page reminds, to use it for DNSSEC validation, dig must have been built with the -DDIG_SIGCHASE option. Luckily, all the major distributions seem to have a DNSSEC-enabled dig.

The example here will verify the A record for the domain name www.eurid.eu. But really, this is just a more-or-less randomly picked example; as more and more TLD are being signed, it should be possible to verify any name that has a trust of chain up to the root. For more information on the status of which TLD domains are signed, this ICANN page provides up-to-date information. As shown there, the eu. TLD is signed and has DS records in the root, as do many others. At the time of this writing, the net. and com. zones are scheduled to be signed in the first months of 2011. That will probably be the time when DNSSEC really starts to take off.

Root keys

Before starting, dig needs to be given a trust anchor (which is a fancy name for one or more keys that are trusted by definition and do not need to be verified further. The role of these keys is similar to the root SSL certificates installed in the browser: when something is signed with them, the verification ends successfully).
The perfect keys to use (and, in an ideal world where DNSSEC is fully deployed, the only needed) are the root keys, that is, the keys found in the "." zone. Since in DNSSEC keys are stored in DNSKEY records, here's how to download them with dig:

$ dig . DNSKEY | grep -Ev '^($|;)' > root.keys
# check
$ cat root.keys 
.			86352	IN	DNSKEY	256 3 8 AwEAAcAPhPM4CQHqg6hZ49y2P3IdKZuF44QNCc50vjATD7W+je4va6dj Y5JpnNP0pIohKNYiCFap/b4Y9jjJGSOkOfkfBR8neI7X5LisMEGUjwRc rG8J9UYP1S1unTNqRcWyDYFH2q3KnIO08zImh5DiFt8yfCdKoqZUN1du p5hy0UWz
.			86352	IN	DNSKEY	257 3 8 AwEAAagAIKlVZrpC6Ia7gEzahOR+9W29euxhJhVVLOyQbSEW0O8gcCjF FVQUTf6v58fLjwBd0YI0EzrAcQqBGCzh/RStIoO8g0NfnfL2MTJRkxoX bfDaUeVPQuYEhg37NZWAJQ9VnMVDxP/VHL496M/QZxkjf5/Efucp2gaD X6RS6CXpoY68LsvPVjR0ZSwzz1apAzvN9dlzEheX7ICJBBtuA6G3LQpz W5hOA2hzCTMjJPJ8LbqF6dsV6DoBQzgul0sGIcGOYl7OyQdXfZ57relS Qageu+ipAdTTJ25AsRTAoub8ONGcLmqrAmRLKBP1dfwhYB4N7knNnulq QxA+Uk1ihz0=

Here is a clearer query that shows the root keys with their tags:

$ dig +multiline . DNSKEY

; <<>> DiG 9.7.2-P2 <<>> +multiline . DNSKEY
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 53322
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;.			IN DNSKEY

;; ANSWER SECTION:
.			86400 IN DNSKEY	256 3 8 (
				AwEAAcAPhPM4CQHqg6hZ49y2P3IdKZuF44QNCc50vjAT
				D7W+je4va6djY5JpnNP0pIohKNYiCFap/b4Y9jjJGSOk
				OfkfBR8neI7X5LisMEGUjwRcrG8J9UYP1S1unTNqRcWy
				DYFH2q3KnIO08zImh5DiFt8yfCdKoqZUN1dup5hy0UWz
				) ; key id = 40288
.			86400 IN DNSKEY	257 3 8 (
				AwEAAagAIKlVZrpC6Ia7gEzahOR+9W29euxhJhVVLOyQ
				bSEW0O8gcCjFFVQUTf6v58fLjwBd0YI0EzrAcQqBGCzh
				/RStIoO8g0NfnfL2MTJRkxoXbfDaUeVPQuYEhg37NZWA
				JQ9VnMVDxP/VHL496M/QZxkjf5/Efucp2gaDX6RS6CXp
				oY68LsvPVjR0ZSwzz1apAzvN9dlzEheX7ICJBBtuA6G3
				LQpzW5hOA2hzCTMjJPJ8LbqF6dsV6DoBQzgul0sGIcGO
				Yl7OyQdXfZ57relSQageu+ipAdTTJ25AsRTAoub8ONGc
				LmqrAmRLKBP1dfwhYB4N7knNnulqQxA+Uk1ihz0=
				) ; key id = 19036

;; Query time: 109 msec
;; SERVER: 172.27.20.21#53(172.27.20.21)
;; WHEN: Mon Nov 15 23:41:59 2010
;; MSG SIZE  rcvd: 439

Here we see that the root keys that we put in the trusted keys file have id 40288 and 19036. This will become important later.
(Note: strictly speaking, only the key with id 19036 - the KSK - is necessary, because that's the one that ultimately signs the root DNSKEY RRset, but having both can't hurt.)
Getting the root keys using dig is good enough for this example; in practice, serious validating resolvers will obtain the keys in a secure way, or the keys will come bundled with the software, similar to the built-in SSL root certificates that are bundled with browsers.

Obviously since the keys are rolled periodically, the keys above will not be valid for long, and will need to be downloaded again if the experiment is to be repeated in the future. The key numbers shown here are those in effect at the time of writing, and will make no sense at all in some weeks, when the keys are rolled.

Also note that for this and the following commands the DNS server used by dig (ie, the one found in /etc/resolv.conf) needs to support DNSSEC (ie, returning signatures if asked) and the EDNS0 extensions which are mandatory when using DNSSEC.

Bottom-up verification

As said, let's verify the A record for www.eurid.eu (line numbers in the output added for the following explanation):

$ dig +sigchase +trusted-key=./root.keys www.eurid.eu. A | cat -n
     1	;; RRset to chase:
     2	www.eurid.eu.		558	IN	A	195.234.53.204
     3	
     4	
     5	;; RRSIG of the RRset to chase:
     6	www.eurid.eu.		558	IN	RRSIG	A 7 3 600 20101201125038 20101101115329 62990 eurid.eu. cq4Sh8HJkFt1VPM/p+1IGEvwMiw9KRG+2GPnfCvquJNfKioB+MpV21E8 h7uV25V/kndXGEh+r27FRBCmsMAftdcrTtB+5NCwcfP17LKOxvDc3Mg1 p4UvVjMK//xiT+yJoM61qsLLbbfFEoCYtz0fZee4Hf7FLlvyYtXNXQG9 P34=
     7	
     8	
     9	
    10	Launch a query to find a RRset of type DNSKEY for zone: eurid.eu.
    11	
    12	;; DNSKEYset that signs the RRset to chase:
    13	eurid.eu.		86366	IN	DNSKEY	257 3 7 AwEAAbu3N0HTlocsABTY4SmW5Et6kAwa1BHg2Jmjcy87VIvKrubLCjeO FLbC2hklqnlZvlyUI5DzmS3YW/1iGNQJ+u9Rdv63BWq1HPCimkxJasSQ vIff1zTYDujCucJgnn5Y3nVnJYaRvn3pmaQYmVA4jL/b3vuOmCI1jNxU NKnfxYXntYBEvfU2C824Bsv+ngKwAVIW/+3dtDhCsHfYzN8lIRHXR9yi G3/sLvFUDUH4n9qIwYGFQ00Kiv/j35VGWwruLS2nj98tw2zEgKg9otcu natVSltUnajuauaTamSTDU/cKk45QHumbQxqxQ6CMv3irDsfVYh85tUe MtiXTb4Sn7s=
    14	eurid.eu.		86366	IN	DNSKEY	256 3 7 AwEAAecbZhciFc12GqKd3NSd40FpZ4PyXwfvnQ7makOUaY+McBWnkK6h j/6TvR+/UafBjxPcL6Dmv9UG+FFdiJJtoukC7IapbPkquA8ItnQrJp0R xWAAkJq5MlftKUAJfZGUgSlRHc6UBCHGjrJnf2QHN/NIFz5BzSPrhLiP 1/2wbDEF
    15	
    16	
    17	;; RRSIG of the DNSKEYset that signs the RRset to chase:
    18	eurid.eu.		86366	IN	RRSIG	DNSKEY 7 2 86400 20101123113942 20101116113254 34023 eurid.eu. TNQhHOH+TdsOzWglBBOUL5gUM7oHZbeFBis9e3PbDXN+wsD2yrOwek2A XczHFrtWoQp6i3eDWOvLwF4DltAjCiVKU+oPAbTJLnVldhWKA4Q+loK2 hl9NE8Txf8lUdRnSlqnewxkbdh3Ml7zUfPgKYWpYxd6/oE6SvOGLvNB5 i6y2lIXqgNyIwd7xL7Y1d+Jvto3U308c/jcws3xWvGkKVoqDBTH3+aaL n+g7VzcnKKusSAPr/aG+LBjXcnL46kWsPnSafFU4Qnw0V+ls8CpL0do7 TbGLVNWu5NVTLCC92orTIYOPRigV/Yg2f+hIiQiwoMIJssJQBQ8drCB+ yc01lg==
    19	eurid.eu.		86366	IN	RRSIG	DNSKEY 7 2 86400 20101123113942 20101116113254 62990 eurid.eu. CdldAM01iqI684Cwe2AF6ZDh4J8ODkbM9Cey+1jUMbAgnORW/WONAUMl 7DFOzchltbBo/5s7kqzoBMUEp8P41dkuLancH9/dHI7pUUNnzprdFFQV 9G+4gIwD3as4og17oX+b1gkf8VyYx8qBEDtIxT9DPHYZ1FwUTX2mBlNf zck=
    20	
    21	
    22	
    23	Launch a query to find a RRset of type DS for zone: eurid.eu.
    24	
    25	;; DSset of the DNSKEYset
    26	eurid.eu.		86366	IN	DS	34023 7 1 C0C4ABA58090643AE17BA8493C4AD2295D4D1376
    27	
    28	
    29	;; RRSIG of the DSset of the DNSKEYset
    30	eurid.eu.		86366	IN	RRSIG	DS 7 2 86400 20101122031929 20101115023628 37319 eu. rPrB+fdy1/oBoDosNKQhvVrTI3VeOkYVcNZgthsqt7DwlWNJ5NrRKfnF KbVzuiMpAHCBT+dWb9SRimBqCYGHHxSXym6gkWlAA0qJLV9HHqKZ7RF8 Ogro4mrknmxIKjgh/SNWZ4u9AN7rzeA9vuJHeYV6S3UefQMlSh9Par2Y WeE=
    31	
    32	
    33	
    34	
    35	;; WE HAVE MATERIAL, WE NOW DO VALIDATION
    36	;; VERIFYING A RRset for www.eurid.eu. with DNSKEY:62990: success
    37	;; OK We found DNSKEY (or more) to validate the RRset
    38	;; Now, we are going to validate this DNSKEY by the DS
    39	;; OK a DS valids a DNSKEY in the RRset
    40	;; Now verify that this DNSKEY validates the DNSKEY RRset
    41	;; VERIFYING DNSKEY RRset for eurid.eu. with DNSKEY:34023: success
    42	;; OK this DNSKEY (validated by the DS) validates the RRset of the DNSKEYs, thus the DNSKEY validates the RRset
    43	;; Now, we want to validate the DS :  recursive call
    44	
    45	
    46	Launch a query to find a RRset of type DNSKEY for zone: eu.
    47	
    48	;; DNSKEYset that signs the RRset to chase:
    49	eu.			86366	IN	DNSKEY	257 3 7 AwEAAc8eyl1THSdL4ZHXK4i5q7OfkxnbY6pA3vzs1O4CeF8wkR5yIHmd xhXfP17uIj73Fpr4ZU+5mK4N3vmJKtWV0ML3ieO1bXvPpuNEEvXmkNOK EUSAfnk9CT8AlS5jiJz6hpzkYd6OFIrnQVgIqGWOqRdx+1sMXBO+IuKh gvYLunsSZyBTWftiHy11NeGMNPA4QO4fcS/IgJIjvpYtr0lhlmwiyS5k c6fz8CD1YmiSzIAIcrqBfOi6/VCarEFxZsEhRi+UFp3ipUz6s8zZ+T8x lDxgRyScApiudiZKor/omLn+JUo4hAaJFo0R2EbnZY7hiK71DkTA2yY+ 3VKuizDV0Kk=
    50	eu.			86366	IN	DNSKEY	256 3 7 AwEAAb7UvT6q/qhscgKJPNDxrnB0Y8mMUqWMD1E69J4vzBc6dqKMckyC H49Sq//3+5mVBshLV3EZORl5guWDIcwJtWeGIgpzRjqDIgkJrDy4mPq1 4qv4mR3gvpsEKCKdSTR8FH+rcKd3aB7SYQLaNF2gqaZloAqjMBnffVd2 xlc4bWVP
    51	
    52	
    53	;; RRSIG of the DNSKEYset that signs the RRset to chase:
    54	eu.			86366	IN	RRSIG	DNSKEY 7 1 86400 20101119124925 20101112114925 37319 eu. vafh1utSPWOs4EZ9OS0KCeZxxTJEPge+LTjcdR9aQUcxdqVHceqr48RK mKuwi6U+qzZ0mXYz06V3Gn2apqayWOxNg/geIVf+5DsKNwkgaaHr1zHi sCxKcLikSevxTH2g3PwbAq1PBPillnNpmasKUwJ97MRyhTT0kj9wUx56 tUs=
    55	eu.			86366	IN	RRSIG	DNSKEY 7 1 86400 20101119124925 20101112114925 61179 eu. UrsoJdsyKuxhClgR8IiRN5iq/IDrOp5ElZ9PKyA0wULZD3aQtHB4USSB t4fJzgN6KbaCBwBSSsG0eWi5U+krquvkzGxqlYJD+9+Gtm4HlZpxATQ5 2rIytsR+vtCmeu2YrC42lGa4iF0oQ2rfzQvonGezWtdPUDI6Z9VdLeFu muw3BQO87NIrWF00VaVVXZotdwRFH7EA29v7snbLL5BpSaiSBlGCALhk 2dEleVbg8YcKTg/cgr2fasHrN0lGc2Dv8l5Ph2U6GMIb2DBAC22RqYk8 HhrbTXDacgbMVtcYIBprQ9JN6jZSXxmzl5RDedeoUQJgRR7QQ4cOiRYZ MUJm4g==
    56	
    57	
    58	
    59	Launch a query to find a RRset of type DS for zone: eu.
    60	
    61	;; DSset of the DNSKEYset
    62	eu.			58008	IN	DS	61179 7 1 87E2B3544884B45F36A0DA72DADCB0239C4D73D4
    63	eu.			58008	IN	DS	61179 7 2 3B526BCC354AE085AD9984C9BE73D271411023EFF421EF184BCE41AC E3DE9F8B
    64	
    65	
    66	;; RRSIG of the DSset of the DNSKEYset
    67	eu.			58008	IN	RRSIG	DS 8 1 86400 20101122000000 20101114230000 40288 . oePYMoFkBkYf7fh2hLam99KyzlRddeh1zsIGtrfk30u074wP3Z2WdLUm FPF8LbNXLAAIo+XtKYH4LK8h36L44FhEtBoS2GL4GGIZasenjBHMEDx0 z6A3+lOED50djuoOBWCsRenS54KHXrNJ8cTS9D/Yg60/GMToMHLyAYUJ eMU=
    68	
    69	
    70	
    71	
    72	;; WE HAVE MATERIAL, WE NOW DO VALIDATION
    73	;; VERIFYING DS RRset for eurid.eu. with DNSKEY:37319: success
    74	;; OK We found DNSKEY (or more) to validate the RRset
    75	;; Now, we are going to validate this DNSKEY by the DS
    76	;; OK a DS valids a DNSKEY in the RRset
    77	;; Now verify that this DNSKEY validates the DNSKEY RRset
    78	;; VERIFYING DNSKEY RRset for eu. with DNSKEY:61179: success
    79	;; OK this DNSKEY (validated by the DS) validates the RRset of the DNSKEYs, thus the DNSKEY validates the RRset
    80	;; Now, we want to validate the DS :  recursive call
    81	
    82	
    83	Launch a query to find a RRset of type DNSKEY for zone: .
    84	
    85	;; DNSKEYset that signs the RRset to chase:
    86	.			86093	IN	DNSKEY	256 3 8 AwEAAcAPhPM4CQHqg6hZ49y2P3IdKZuF44QNCc50vjATD7W+je4va6dj Y5JpnNP0pIohKNYiCFap/b4Y9jjJGSOkOfkfBR8neI7X5LisMEGUjwRc rG8J9UYP1S1unTNqRcWyDYFH2q3KnIO08zImh5DiFt8yfCdKoqZUN1du p5hy0UWz
    87	.			86093	IN	DNSKEY	257 3 8 AwEAAagAIKlVZrpC6Ia7gEzahOR+9W29euxhJhVVLOyQbSEW0O8gcCjF FVQUTf6v58fLjwBd0YI0EzrAcQqBGCzh/RStIoO8g0NfnfL2MTJRkxoX bfDaUeVPQuYEhg37NZWAJQ9VnMVDxP/VHL496M/QZxkjf5/Efucp2gaD X6RS6CXpoY68LsvPVjR0ZSwzz1apAzvN9dlzEheX7ICJBBtuA6G3LQpz W5hOA2hzCTMjJPJ8LbqF6dsV6DoBQzgul0sGIcGOYl7OyQdXfZ57relS Qageu+ipAdTTJ25AsRTAoub8ONGcLmqrAmRLKBP1dfwhYB4N7knNnulq QxA+Uk1ihz0=
    88	
    89	
    90	;; RRSIG of the DNSKEYset that signs the RRset to chase:
    91	.			86093	IN	RRSIG	DNSKEY 8 0 86400 20101124235959 20101110000000 19036 . SmZKqR1XNc73PUkVI6YRlXwcxeLfUmvLpikKaxTg+HmYCyWrmxzmYv/1 onFpCl8EZ75sGwIM78QCIekH+V1Azoc29fOx/NyWh86wc4bUqouoLXCH 99rEz2024pu8ZaC7ly1/HO+SCCV0ALT03/UvdwloEBl60bqBXZvN44K7 RIBqeLFOdXMHa5G8dNlopZaKsV5+0CGD28CgkC07A5//sumpEzGDmeq2 9gAGHk4WIpBL2EzUtToJRfdygpelA4akf+c32PkQGEEkjeDCEi1XCiwn lTCBiGrDAqYW5ivUb26h0twVH7vJlDWCR57/Q7SroV4oljK1tXkSL8bL 9beusw==
    92	
    93	
    94	
    95	Launch a query to find a RRset of type DS for zone: .
    96	;; NO ANSWERS: no more
    97	
    98	;; WARNING There is no DS for the zone: .
    99	
   100	
   101	
   102	;; WE HAVE MATERIAL, WE NOW DO VALIDATION
   103	;; VERIFYING DS RRset for eu. with DNSKEY:40288: success
   104	;; OK We found DNSKEY (or more) to validate the RRset
   105	;; Ok, find a Trusted Key in the DNSKEY RRset: 40288
   106	;; Ok, find a Trusted Key in the DNSKEY RRset: 19036
   107	;; VERIFYING DNSKEY RRset for . with DNSKEY:19036: success
   108	
   109	;; Ok this DNSKEY is a Trusted Key, DNSSEC validation is ok: SUCCESS
   110	

Good, the verification succeeded. But what happened? Let's go through the steps.

First, the RRset that needs to be verified is identified (a single item RRset here): quite obviously, that is (line 2)

www.eurid.eu.		558	IN	A	195.234.53.204

The signature for that record is given in line 6:

www.eurid.eu.		558	IN	RRSIG	A 7 3 600 20101201125038 20101101115329 62990 eurid.eu. cq4Sh8HJkFt1VPM/p+1IGEvwMiw9KRG+2GPnfCvquJNfKioB+MpV21E8 h7uV25V/kndXGEh+r27FRBCmsMAftdcrTtB+5NCwcfP17LKOxvDc3Mg1 p4UvVjMK//xiT+yJoM61qsLLbbfFEoCYtz0fZee4Hf7FLlvyYtXNXQG9 P34=

The interesting part of the signature is that it was created using the key with tag 62990, and the signer name is eurid.eu. (this will be useful in a second).

Since signatures are created using a private key, to verify the signature we need to have the corresponding public key, and that's what dig requests and shows in lines 13 and 14:

eurid.eu.		86366	IN	DNSKEY	257 3 7 AwEAAbu3N0HTlocsABTY4SmW5Et6kAwa1BHg2Jmjcy87VIvKrubLCjeO FLbC2hklqnlZvlyUI5DzmS3YW/1iGNQJ+u9Rdv63BWq1HPCimkxJasSQ vIff1zTYDujCucJgnn5Y3nVnJYaRvn3pmaQYmVA4jL/b3vuOmCI1jNxU NKnfxYXntYBEvfU2C824Bsv+ngKwAVIW/+3dtDhCsHfYzN8lIRHXR9yi G3/sLvFUDUH4n9qIwYGFQ00Kiv/j35VGWwruLS2nj98tw2zEgKg9otcu natVSltUnajuauaTamSTDU/cKk45QHumbQxqxQ6CMv3irDsfVYh85tUe MtiXTb4Sn7s=
eurid.eu.		86366	IN	DNSKEY	256 3 7 AwEAAecbZhciFc12GqKd3NSd40FpZ4PyXwfvnQ7makOUaY+McBWnkK6h j/6TvR+/UafBjxPcL6Dmv9UG+FFdiJJtoukC7IapbPkquA8ItnQrJp0R xWAAkJq5MlftKUAJfZGUgSlRHc6UBCHGjrJnf2QHN/NIFz5BzSPrhLiP 1/2wbDEF

Of these two keys, the first is a so-called Key Signing Key (KSK), and the second is a Zone Signing Key (ZSK). This is encoded in the fifth field: in that value, bit 0 is called the SEP (Secure Entry Point) and is set for the KSK, and not set for the ZSK (hence they usually have the two values 257 and 256 respectively). As a related note, the KSK is longer than the ZSK; this is usually the case, because the KSK is rolled less frequently (as changing it needs to involve the parent zone, while rolling the ZSK can be done independently and thus more often).

Although dig doesn't show the tags above, the KSK has tag 34023, and the ZSK has tag 62990. To check that, we can run something like this:

$ dig +multiline eurid.eu. DNSKEY

; <<>> DiG 9.7.2-P2 <<>> +multiline eurid.eu. DNSKEY
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 36198
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;eurid.eu.		IN DNSKEY

;; ANSWER SECTION:
eurid.eu.		86400 IN DNSKEY	257 3 7 (
				AwEAAbu3N0HTlocsABTY4SmW5Et6kAwa1BHg2Jmjcy87
				VIvKrubLCjeOFLbC2hklqnlZvlyUI5DzmS3YW/1iGNQJ
				+u9Rdv63BWq1HPCimkxJasSQvIff1zTYDujCucJgnn5Y
				3nVnJYaRvn3pmaQYmVA4jL/b3vuOmCI1jNxUNKnfxYXn
				tYBEvfU2C824Bsv+ngKwAVIW/+3dtDhCsHfYzN8lIRHX
				R9yiG3/sLvFUDUH4n9qIwYGFQ00Kiv/j35VGWwruLS2n
				j98tw2zEgKg9otcunatVSltUnajuauaTamSTDU/cKk45
				QHumbQxqxQ6CMv3irDsfVYh85tUeMtiXTb4Sn7s=
				) ; key id = 34023
eurid.eu.		86400 IN DNSKEY	256 3 7 (
				AwEAAecbZhciFc12GqKd3NSd40FpZ4PyXwfvnQ7makOU
				aY+McBWnkK6hj/6TvR+/UafBjxPcL6Dmv9UG+FFdiJJt
				oukC7IapbPkquA8ItnQrJp0RxWAAkJq5MlftKUAJfZGU
				gSlRHc6UBCHGjrJnf2QHN/NIFz5BzSPrhLiP1/2wbDEF
				) ; key id = 62990

;; Query time: 42 msec
;; SERVER: 172.27.20.21#53(172.27.20.21)
;; WHEN: Tue Nov 16 21:04:17 2010
;; MSG SIZE  rcvd: 450

So, as expected, our signature on the A record was created with the ZSK (normally the KSK is only used to sign DNSKEY RRsets, although DNSSEC does not enforce that), tag 62990. So far so good.

But then, the DNSKEY records are themselves a RRset, and, like any other RRset, that has to be signed. We are walking the trust of chain: to verify the A record, we must verify its signature; to verify that signature, we need to have the public key that created it; that key is part of a RRset, and to verify it we need to verify its signature (and we're not done yet). That signature is exactly what we find in lines 18 and 19:

eurid.eu.		86366	IN	RRSIG	DNSKEY 7 2 86400 20101123113942 20101116113254 34023 eurid.eu. TNQhHOH+TdsOzWglBBOUL5gUM7oHZbeFBis9e3PbDXN+wsD2yrOwek2A XczHFrtWoQp6i3eDWOvLwF4DltAjCiVKU+oPAbTJLnVldhWKA4Q+loK2 hl9NE8Txf8lUdRnSlqnewxkbdh3Ml7zUfPgKYWpYxd6/oE6SvOGLvNB5 i6y2lIXqgNyIwd7xL7Y1d+Jvto3U308c/jcws3xWvGkKVoqDBTH3+aaL n+g7VzcnKKusSAPr/aG+LBjXcnL46kWsPnSafFU4Qnw0V+ls8CpL0do7 TbGLVNWu5NVTLCC92orTIYOPRigV/Yg2f+hIiQiwoMIJssJQBQ8drCB+ yc01lg==
eurid.eu.		86366	IN	RRSIG	DNSKEY 7 2 86400 20101123113942 20101116113254 62990 eurid.eu. CdldAM01iqI684Cwe2AF6ZDh4J8ODkbM9Cey+1jUMbAgnORW/WONAUMl 7DFOzchltbBo/5s7kqzoBMUEp8P41dkuLancH9/dHI7pUUNnzprdFFQV 9G+4gIwD3as4og17oX+b1gkf8VyYx8qBEDtIxT9DPHYZ1FwUTX2mBlNf zck=

We have two signatures here. One was created using the key with tag 34023 (the KSK), and the other was created using the key with tag 62990 (the ZSK). Both are signatures of the DNSKEY RRset; this RRset is, effectively, self-signed. How can we proceed further in the verification then? We need to have something in the parent zone (eu. here) that links into the eurid.eu. zone and allows us to climb further up in the chain of trust towards the root.

That "something" is called a DS record, which essentially is a cryptographic hash of the child zone's KSK, signed by the parent zone (ie, with the parent zone's key); if we trust the parent zone (we don't yet; we're still climbing the chain), we can then trust the KSK in the child zone. The DS record is shown in line 26:

eurid.eu.		86366	IN	DS	34023 7 1 C0C4ABA58090643AE17BA8493C4AD2295D4D1376

This record comes from the eu. zone, the parent of eurid.eu. As the fifth field shows, this is a hash of the child zone's KSK (tag 34023), which we encountered previously.
I said earlier that the DS record is signed (it must be, to be any good); and indeed, line 30 has the signature:

eurid.eu.		86366	IN	RRSIG	DS 7 2 86400 20101122031929 20101115023628 37319 eu. rPrB+fdy1/oBoDosNKQhvVrTI3VeOkYVcNZgthsqt7DwlWNJ5NrRKfnF KbVzuiMpAHCBT+dWb9SRimBqCYGHHxSXym6gkWlAA0qJLV9HHqKZ7RF8 Ogro4mrknmxIKjgh/SNWZ4u9AN7rzeA9vuJHeYV6S3UefQMlSh9Par2Y WeE=

This tells us that the signature was generated with the key with tag 37319 and the signer name is eu. . Let's see:

$ $ dig +multiline eu. DNSKEY

; <<>> DiG 9.7.2-P2 <<>> +multiline eu. DNSKEY
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 42913
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;eu.			IN DNSKEY

;; ANSWER SECTION:
eu.			86400 IN DNSKEY	256 3 7 (
				AwEAAb7UvT6q/qhscgKJPNDxrnB0Y8mMUqWMD1E69J4v
				zBc6dqKMckyCH49Sq//3+5mVBshLV3EZORl5guWDIcwJ
				tWeGIgpzRjqDIgkJrDy4mPq14qv4mR3gvpsEKCKdSTR8
				FH+rcKd3aB7SYQLaNF2gqaZloAqjMBnffVd2xlc4bWVP
				) ; key id = 37319
eu.			86400 IN DNSKEY	257 3 7 (
				AwEAAc8eyl1THSdL4ZHXK4i5q7OfkxnbY6pA3vzs1O4C
				eF8wkR5yIHmdxhXfP17uIj73Fpr4ZU+5mK4N3vmJKtWV
				0ML3ieO1bXvPpuNEEvXmkNOKEUSAfnk9CT8AlS5jiJz6
				hpzkYd6OFIrnQVgIqGWOqRdx+1sMXBO+IuKhgvYLunsS
				ZyBTWftiHy11NeGMNPA4QO4fcS/IgJIjvpYtr0lhlmwi
				yS5kc6fz8CD1YmiSzIAIcrqBfOi6/VCarEFxZsEhRi+U
				Fp3ipUz6s8zZ+T8xlDxgRyScApiudiZKor/omLn+JUo4
				hAaJFo0R2EbnZY7hiK71DkTA2yY+3VKuizDV0Kk=
				) ; key id = 61179

;; Query time: 128 msec
;; SERVER: 172.27.20.21#53(172.27.20.21)
;; WHEN: Tue Nov 16 21:14:16 2010
;; MSG SIZE  rcvd: 444

Sure enough, the ZSK with tag 37319 is there.

At this point, dig has enough data to perform the first round of verifications (lines 35-43), which is successful. But we're not done yet: to verify the signature on the DS, we need to trust the key that was used to generate that signature (ie, key 37319). So dig goes out again and fetches the DNSKEY RRset for eu. (lines 49-50):

eu.			86366	IN	DNSKEY	257 3 7 AwEAAc8eyl1THSdL4ZHXK4i5q7OfkxnbY6pA3vzs1O4CeF8wkR5yIHmd xhXfP17uIj73Fpr4ZU+5mK4N3vmJKtWV0ML3ieO1bXvPpuNEEvXmkNOK EUSAfnk9CT8AlS5jiJz6hpzkYd6OFIrnQVgIqGWOqRdx+1sMXBO+IuKh gvYLunsSZyBTWftiHy11NeGMNPA4QO4fcS/IgJIjvpYtr0lhlmwiyS5k c6fz8CD1YmiSzIAIcrqBfOi6/VCarEFxZsEhRi+UFp3ipUz6s8zZ+T8x lDxgRyScApiudiZKor/omLn+JUo4hAaJFo0R2EbnZY7hiK71DkTA2yY+ 3VKuizDV0Kk=
eu.			86366	IN	DNSKEY	256 3 7 AwEAAb7UvT6q/qhscgKJPNDxrnB0Y8mMUqWMD1E69J4vzBc6dqKMckyC H49Sq//3+5mVBshLV3EZORl5guWDIcwJtWeGIgpzRjqDIgkJrDy4mPq1 4qv4mR3gvpsEKCKdSTR8FH+rcKd3aB7SYQLaNF2gqaZloAqjMBnffVd2 xlc4bWVP

(btw, these are the same keys we got separately a moment ago to check the key tags). At this point, the process should be clear: dig requests the signatures for the above RRset (lines 54 and 55), which are self-signed, and thus it needs to get the DS record from the parent of eu., which finally is the root zone: lines 62 and 63 are the DS RRset for eu. in the root zone, and the signature is at line 67, created with key 40288, which is the root's ZSK (dig hasn't got it yet, but if you go back to the paragraph where we downloaded the root keys you'll see it).

After another round of verifications (lines 72-80), dig goes on to verify the signatures on the DS records, so it requests the DNSKEY RRset for the root zone (lines 86 and 87) and the associated signature (line 91). Note that this signature is created with key 19036 which is the root's KSK.

But key 19036 is also one of the keys we originally put in the root.keys file (along with key 40288, as said). After dig tries to fetch a DS record for the root which does not exist, it finally realizes that the keys it wants to verify are trusted because they are in the supplied trusted key file (lines 105 and 106). At this stage, dig has successfully verified the whole chain of trust from www.eurid.eu up to the root, and declares success (line 109).

As mentioned previously, since ultimately it is the KSK that is used to create the topmost signature, the verification would have been successful even if the trusted key file had contained only the KSK (tag 19036). It would have failed, instead, if the only trusted key had been the ZSK (40288).

Top-down verification

The validation we just performed is a so-called "bottom-up" validation, similar to what happens when SSL certificates are verified: every stage validates the previous one, going up until a trusted key is found, at which point the whole chain (not yet validated until then) suddenly is declared valid by the transitive property of trust. In the DNSSEC world, this means starting from the domain name to verify, and going up towards the root.

But given the way trust chains are built in DNSSEC, it should also be possible to perform a "top-down" validation, starting from the root and going down towards the domain name that needs to be verified. Indeed, dig permits to run a "top-down" validation, using the +topdown option.

$ dig +sigchase +topdown +trusted-key=./root.keys www.eurid.eu. A | cat -n
     1	ns name: 198.41.0.4
     2	ns name: 192.228.79.201
     3	ns name: 192.33.4.12
     4	ns name: 128.8.10.90
     5	ns name: 192.203.230.10
     6	ns name: 192.5.5.241
     7	ns name: 192.112.36.4
     8	ns name: 128.63.2.53
     9	ns name: 192.36.148.17
    10	ns name: 192.58.128.30
    11	ns name: 193.0.14.129
    12	ns name: 199.7.83.42
    13	ns name: 202.12.27.33
    14	
    15	Launch a query to find a RRset of type A for zone: www.eurid.eu. with nameservers:
    16	.			518385	IN	NS	a.root-servers.net.
    17	.			518385	IN	NS	b.root-servers.net.
    18	.			518385	IN	NS	c.root-servers.net.
    19	.			518385	IN	NS	d.root-servers.net.
    20	.			518385	IN	NS	e.root-servers.net.
    21	.			518385	IN	NS	f.root-servers.net.
    22	.			518385	IN	NS	g.root-servers.net.
    23	.			518385	IN	NS	h.root-servers.net.
    24	.			518385	IN	NS	i.root-servers.net.
    25	.			518385	IN	NS	j.root-servers.net.
    26	.			518385	IN	NS	k.root-servers.net.
    27	.			518385	IN	NS	l.root-servers.net.
    28	.			518385	IN	NS	m.root-servers.net.
    29	
    30	no response but there is a delegation in authority section:eu.
    31	
    32	
    33	Launch a query to find a RRset of type DNSKEY for zone: .
    34	
    35	;; DNSKEYset:
    36	.			86400	IN	DNSKEY	256 3 8 AwEAAcAPhPM4CQHqg6hZ49y2P3IdKZuF44QNCc50vjATD7W+je4va6dj Y5JpnNP0pIohKNYiCFap/b4Y9jjJGSOkOfkfBR8neI7X5LisMEGUjwRc rG8J9UYP1S1unTNqRcWyDYFH2q3KnIO08zImh5DiFt8yfCdKoqZUN1du p5hy0UWz
    37	.			86400	IN	DNSKEY	257 3 8 AwEAAagAIKlVZrpC6Ia7gEzahOR+9W29euxhJhVVLOyQbSEW0O8gcCjF FVQUTf6v58fLjwBd0YI0EzrAcQqBGCzh/RStIoO8g0NfnfL2MTJRkxoX bfDaUeVPQuYEhg37NZWAJQ9VnMVDxP/VHL496M/QZxkjf5/Efucp2gaD X6RS6CXpoY68LsvPVjR0ZSwzz1apAzvN9dlzEheX7ICJBBtuA6G3LQpz W5hOA2hzCTMjJPJ8LbqF6dsV6DoBQzgul0sGIcGOYl7OyQdXfZ57relS Qageu+ipAdTTJ25AsRTAoub8ONGcLmqrAmRLKBP1dfwhYB4N7knNnulq QxA+Uk1ihz0=
    38	
    39	
    40	;; RRSIG of the DNSKEYset:
    41	.			86400	IN	RRSIG	DNSKEY 8 0 86400 20101124235959 20101110000000 19036 . SmZKqR1XNc73PUkVI6YRlXwcxeLfUmvLpikKaxTg+HmYCyWrmxzmYv/1 onFpCl8EZ75sGwIM78QCIekH+V1Azoc29fOx/NyWh86wc4bUqouoLXCH 99rEz2024pu8ZaC7ly1/HO+SCCV0ALT03/UvdwloEBl60bqBXZvN44K7 RIBqeLFOdXMHa5G8dNlopZaKsV5+0CGD28CgkC07A5//sumpEzGDmeq2 9gAGHk4WIpBL2EzUtToJRfdygpelA4akf+c32PkQGEEkjeDCEi1XCiwn lTCBiGrDAqYW5ivUb26h0twVH7vJlDWCR57/Q7SroV4oljK1tXkSL8bL 9beusw==
    42	
    43	;; Ok, find a Trusted Key in the DNSKEY RRset: 40288
    44	;; Ok, find a Trusted Key in the DNSKEY RRset: 19036
    45	;; VERIFYING DNSKEY RRset for . with DNSKEY:19036: success
    46	
    47	;; DSset:
    48	eu.			86400	IN	DS	61179 7 1 87E2B3544884B45F36A0DA72DADCB0239C4D73D4
    49	eu.			86400	IN	DS	61179 7 2 3B526BCC354AE085AD9984C9BE73D271411023EFF421EF184BCE41AC E3DE9F8B
    50	
    51	
    52	;; RRSIGset of DSset
    53	eu.			86400	IN	RRSIG	DS 8 1 86400 20101123000000 20101115230000 40288 . H6evquVeN2Nb6MZacZn0Bt64ViDiOcFcu9hpTJs6f4Q3iLjs6X6f4/wi p2SJOeD3VJ78E7PD8wOdzbFfjm8/MnQd5wZNavaH+Jegj1pb9q1GzBfr qM9Oq6gjnCLK3TiYh9WKlz4q0OQPBUQBvO293/R0bQu6hCCA2DHF16qw bkM=
    54	
    55	;; VERIFYING DS RRset for eu. with DNSKEY:40288: success
    56	ns name: 194.0.1.19
    57	ns name: 195.47.235.130
    58	ns name: 193.2.221.60
    59	ns name: 217.29.76.13
    60	ns name: 91.200.16.100
    61	ns name: 194.146.106.90
    62	ns name: 195.66.241.178
    63	
    64	Launch a query to find a RRset of type A for zone: www.eurid.eu. with nameservers:
    65	eu.			172800	IN	NS	x.nic.eu.
    66	eu.			172800	IN	NS	p.nic.eu.
    67	eu.			172800	IN	NS	l.eu.dns.be.
    68	eu.			172800	IN	NS	m.nic.eu.
    69	eu.			172800	IN	NS	a.nic.eu.
    70	eu.			172800	IN	NS	y.nic.eu.
    71	eu.			172800	IN	NS	l.nic.eu.
    72	
    73	no response but there is a delegation in authority section:eurid.eu.
    74	
    75	
    76	Launch a query to find a RRset of type DNSKEY for zone: eu.
    77	
    78	;; DNSKEYset:
    79	eu.			86400	IN	DNSKEY	257 3 7 AwEAAc8eyl1THSdL4ZHXK4i5q7OfkxnbY6pA3vzs1O4CeF8wkR5yIHmd xhXfP17uIj73Fpr4ZU+5mK4N3vmJKtWV0ML3ieO1bXvPpuNEEvXmkNOK EUSAfnk9CT8AlS5jiJz6hpzkYd6OFIrnQVgIqGWOqRdx+1sMXBO+IuKh gvYLunsSZyBTWftiHy11NeGMNPA4QO4fcS/IgJIjvpYtr0lhlmwiyS5k c6fz8CD1YmiSzIAIcrqBfOi6/VCarEFxZsEhRi+UFp3ipUz6s8zZ+T8x lDxgRyScApiudiZKor/omLn+JUo4hAaJFo0R2EbnZY7hiK71DkTA2yY+ 3VKuizDV0Kk=
    80	eu.			86400	IN	DNSKEY	256 3 7 AwEAAb7UvT6q/qhscgKJPNDxrnB0Y8mMUqWMD1E69J4vzBc6dqKMckyC H49Sq//3+5mVBshLV3EZORl5guWDIcwJtWeGIgpzRjqDIgkJrDy4mPq1 4qv4mR3gvpsEKCKdSTR8FH+rcKd3aB7SYQLaNF2gqaZloAqjMBnffVd2 xlc4bWVP
    81	
    82	
    83	;; RRSIG of the DNSKEYset:
    84	eu.			86400	IN	RRSIG	DNSKEY 7 1 86400 20101119124925 20101112114925 37319 eu. vafh1utSPWOs4EZ9OS0KCeZxxTJEPge+LTjcdR9aQUcxdqVHceqr48RK mKuwi6U+qzZ0mXYz06V3Gn2apqayWOxNg/geIVf+5DsKNwkgaaHr1zHi sCxKcLikSevxTH2g3PwbAq1PBPillnNpmasKUwJ97MRyhTT0kj9wUx56 tUs=
    85	eu.			86400	IN	RRSIG	DNSKEY 7 1 86400 20101119124925 20101112114925 61179 eu. UrsoJdsyKuxhClgR8IiRN5iq/IDrOp5ElZ9PKyA0wULZD3aQtHB4USSB t4fJzgN6KbaCBwBSSsG0eWi5U+krquvkzGxqlYJD+9+Gtm4HlZpxATQ5 2rIytsR+vtCmeu2YrC42lGa4iF0oQ2rfzQvonGezWtdPUDI6Z9VdLeFu muw3BQO87NIrWF00VaVVXZotdwRFH7EA29v7snbLL5BpSaiSBlGCALhk 2dEleVbg8YcKTg/cgr2fasHrN0lGc2Dv8l5Ph2U6GMIb2DBAC22RqYk8 HhrbTXDacgbMVtcYIBprQ9JN6jZSXxmzl5RDedeoUQJgRR7QQ4cOiRYZ MUJm4g==
    86	
    87	;; OK a DS valids a DNSKEY in the RRset
    88	;; Now verify that this DNSKEY validates the DNSKEY RRset
    89	;; VERIFYING DNSKEY RRset for eu. with DNSKEY:61179: success
    90	
    91	;; DSset:
    92	eurid.eu.		86400	IN	DS	34023 7 1 C0C4ABA58090643AE17BA8493C4AD2295D4D1376
    93	
    94	
    95	;; RRSIGset of DSset
    96	eurid.eu.		86400	IN	RRSIG	DS 7 2 86400 20101122031929 20101115023628 37319 eu. rPrB+fdy1/oBoDosNKQhvVrTI3VeOkYVcNZgthsqt7DwlWNJ5NrRKfnF KbVzuiMpAHCBT+dWb9SRimBqCYGHHxSXym6gkWlAA0qJLV9HHqKZ7RF8 Ogro4mrknmxIKjgh/SNWZ4u9AN7rzeA9vuJHeYV6S3UefQMlSh9Par2Y WeE=
    97	
    98	;; VERIFYING DS RRset for eurid.eu. with DNSKEY:37319: success
    99	ns name: 91.200.16.100
   100	ns name: 195.66.241.178
   101	ns name: 195.47.235.130
   102	
   103	Launch a query to find a RRset of type A for zone: www.eurid.eu. with nameservers:
   104	eurid.eu.		86400	IN	NS	a.nic.eu.
   105	eurid.eu.		86400	IN	NS	l.nic.eu.
   106	eurid.eu.		86400	IN	NS	p.nic.eu.
   107	
   108	
   109	
   110	Launch a query to find a RRset of type DNSKEY for zone: eurid.eu.
   111	
   112	;; DNSKEYset:
   113	eurid.eu.		86400	IN	DNSKEY	257 3 7 AwEAAbu3N0HTlocsABTY4SmW5Et6kAwa1BHg2Jmjcy87VIvKrubLCjeO FLbC2hklqnlZvlyUI5DzmS3YW/1iGNQJ+u9Rdv63BWq1HPCimkxJasSQ vIff1zTYDujCucJgnn5Y3nVnJYaRvn3pmaQYmVA4jL/b3vuOmCI1jNxU NKnfxYXntYBEvfU2C824Bsv+ngKwAVIW/+3dtDhCsHfYzN8lIRHXR9yi G3/sLvFUDUH4n9qIwYGFQ00Kiv/j35VGWwruLS2nj98tw2zEgKg9otcu natVSltUnajuauaTamSTDU/cKk45QHumbQxqxQ6CMv3irDsfVYh85tUe MtiXTb4Sn7s=
   114	eurid.eu.		86400	IN	DNSKEY	256 3 7 AwEAAecbZhciFc12GqKd3NSd40FpZ4PyXwfvnQ7makOUaY+McBWnkK6h j/6TvR+/UafBjxPcL6Dmv9UG+FFdiJJtoukC7IapbPkquA8ItnQrJp0R xWAAkJq5MlftKUAJfZGUgSlRHc6UBCHGjrJnf2QHN/NIFz5BzSPrhLiP 1/2wbDEF
   115	
   116	
   117	;; RRSIG of the DNSKEYset:
   118	eurid.eu.		86400	IN	RRSIG	DNSKEY 7 2 86400 20101123113942 20101116113254 34023 eurid.eu. TNQhHOH+TdsOzWglBBOUL5gUM7oHZbeFBis9e3PbDXN+wsD2yrOwek2A XczHFrtWoQp6i3eDWOvLwF4DltAjCiVKU+oPAbTJLnVldhWKA4Q+loK2 hl9NE8Txf8lUdRnSlqnewxkbdh3Ml7zUfPgKYWpYxd6/oE6SvOGLvNB5 i6y2lIXqgNyIwd7xL7Y1d+Jvto3U308c/jcws3xWvGkKVoqDBTH3+aaL n+g7VzcnKKusSAPr/aG+LBjXcnL46kWsPnSafFU4Qnw0V+ls8CpL0do7 TbGLVNWu5NVTLCC92orTIYOPRigV/Yg2f+hIiQiwoMIJssJQBQ8drCB+ yc01lg==
   119	eurid.eu.		86400	IN	RRSIG	DNSKEY 7 2 86400 20101123113942 20101116113254 62990 eurid.eu. CdldAM01iqI684Cwe2AF6ZDh4J8ODkbM9Cey+1jUMbAgnORW/WONAUMl 7DFOzchltbBo/5s7kqzoBMUEp8P41dkuLancH9/dHI7pUUNnzprdFFQV 9G+4gIwD3as4og17oX+b1gkf8VyYx8qBEDtIxT9DPHYZ1FwUTX2mBlNf zck=
   120	
   121	;; OK a DS valids a DNSKEY in the RRset
   122	;; Now verify that this DNSKEY validates the DNSKEY RRset
   123	;; VERIFYING DNSKEY RRset for eurid.eu. with DNSKEY:34023: success
   124	;; VERIFYING A RRset for www.eurid.eu. with DNSKEY:62990: success
   125	
   126	;; The Answer:
   127	www.eurid.eu.		600	IN	A	195.234.53.204
   128	
   129	
   130	;; FINISH : we have validate the DNSSEC chain of trust: SUCCESS
   131	
   132	;; cleanandgo

Again, success. But the route to that result was different this time. To better follow the events, it helps (more than in the bottom-up case) to capture the network traffic with tcpdump or wireshark while the top-down verification is being performed.
Some detail in the following explanation are not evident from the output of dig, and have been gathered by looking at the captured traffic.

Here, dig starts out by looking for the root zone name servers (lines 1-13). Then it picks one of the root servers previously obtained, and asks for tha A record of www.eurid.eu. (lines 15-28). This looks like the beginning of what normal DNS servers do when resolving a name that they don't know: query the root servers. (Spoiler: this is indeed what dig will do, this being a top-down validation; but in the process, dig will also look for the relevant DNSSEC records and perform validation as it goes down the chain of referrals.)

Obviously, the chosen root server knows nothing about the requested A record, but it returns a referral to the eu. name servers (line 30). A non-DNSSEC resolver would just follow the referral at this stage and query one of the referred-to servers.

But before doing that and leaving the root zone, dig makes a last request for the root zone DNSKEY records and associated signatures (lines 35-41). After that, dig sees (lines 43 and 44) that the keys it just received are trusted (because they are in the trusted key file specified on the command line), and thus declares the signature created with them valid (line 45).
The previously obtained referral also included, in the authority section, the DS records of the eu. zone, along wth its signature, which are shown in lines 48-49 and 53 respectively. Since the DS signature was generated with the ZSK (40288), and dig is trusting that key already (because it is in the trusted key file, and even if it wasn't, because it was signed with the KSK, which is trusted), dig declares the DS record valid (line 55). Note that the DS records hashes the eu. KSK, tag 61179.

Now dig is finally ready to follow the referral it was given, and thus it picks one of the servers it was referred to (lines 56-62) and repeats the original query (lines 64-71). Again, no direct answer but a referral to the eurid.eu. nameservers is returned (line 73). Similarly to what it did for the root, before following the referral dig requests the DNSKEY records and associated signatures for eu. (lines 78-85), and the authority section of the reply will contain, along with the actual nameservers for eurid.eu., the signed DS records for eurid.eu. (lines 91-96).

Now dig finds that the KSK key mentioned in the parent DS record (61179) is indeed part of the DNSKEY RRset for eu., and its hash matches that in the DS (line 87). This makes it trusted, because the parent vouched for its validity by means of the DS record it signed. Since this key is now trusted, dig can verify the signature for the eu. DNSKEY RRset that was created with key 61179 (lines 88-89). Now an important point is that the signature covers the whole DNSKEY RRset, and that RRset also includes the ZSK (37319). Since the RRset has been verified, key 37319 also becomes trusted.

From the last referral, dig also has the DS data for eurid.eu. (it was in the authority section of the reply), which is signed with the eu.'s ZSK (37319), which has just become trusted. So, dig can now successfully verify the signature on the DS record (line 98). The DS references key 34023 in the child (eurid.eu.).

At this point, dig leaves eu. and follows the referral to eurid.eu.. The process repeats one more time. However, this time the queried server is authoritative for eurid.eu., and has the answer.
Lines 99-101 show the available servers for eurid.eu., and dig queries one of them in lines 103-106. The server returns a proper answer (notice there's no mention of referrals, unlike the previous queries). However, to verify the answer, dig needs to have the zone keys, and it asks for them and their signature in lines 113-114 (DNSKEY) and lines 118-119 (signatures). As it did previously, first it checks that the key 34023 that was mentioned at the parent DS is contained in the DNSKEY RRset and hashes to the value found in the DS (line 121). With that, it can verify that the DNSKEY signature created with that key is valid (line 123), and this also makes the ZSK (62990) valid because it's in the same RRset covered by the verified signature. But the ZSK, 62990, is also the key that was used to sign the A record for www.eurid.eu., which dig can thus now validate (line 124). This is the last, definitive validation step; it is successful, and dig eventually declares success (line 130).

Conclusion

Hopefully this analysis shows that the DNSSEC verification process isn't too complicated; it's just a bit involved, due to the way the chain of trust is built in DNSSEC. Readers are invited to report any error or inaccuracy they may find.

The two verification methods differ in that in the bottom-up verification dig always queries the same server (the one it finds in resolv.conf), while in the top-down verification it uses that server only to get the initial list of root servers, but after that it autonomously queries the authoritative servers for each zone it traverses.

A good DNSSEC debugging tool, which seems to use the top-down approach, is at this page provided by VeriSign. Another debugger, which offers graphical diagrams of the chain of trust, is here. However, both tools seems to validate only A/AAAA records.

Awk pitfall: string concatenation

This is a bit of a dark corner of awk. This beast is string concatenation.
There is a classical example in the awk FAQ number 28:

$ awk 'BEGIN { print 6 " " -22 }'
6-22

Where did the space go? First, " " -22 is evaluated, in numeric context, yielding 0-22 = -22. This is then concatenated with 6, producing the output that we see.

But recently another case was presented on the mailing list. Here is a minimal example that demonstrates the problem:

$ awk 'BEGIN { print "Hello " ++count }'
Hello 1
$ awk 'BEGIN{ msg = "Hello "; print msg ++count }'
0

These should do the same thing, but...what's going on in the second example? Turns out that the culprit is the seemingly innocuous string concatenation:

print msg ++count

Believe it or not, that is parsed by awk as

print (msg++) count

Obviously, "msg" is a string, so to apply the postincrement operator it must be converted to a number, and that number is 0. "count" is not touched at all; to awk, it's still in the default state (dual empty string/numeric 0; in string context like here, the empty string value is used). The concatenation of 0 with an empty string gives 0, which is the result we see.

But...but...why the does the first version with the literal string work then? Simple: because a literal string can't be postincremented, so the "++" is parsed as preincrement for "count". (Well, simple, yes, even obvious, once somebody tells you.)

So here's the test case again:

$ awk 'BEGIN{ msg = "Hello "; print msg             ++count }'
0

This is purposely exaggerated to show that the amount of spaces before the "++" is completely irrelevant; it will still be applied to "msg". As I've been reminded, the awk grammar states that "A <blank> shall have no effect, except to delimit lexical tokens or within STRING or ERE tokens", which is exactly the point.

It's not difficult to make up other cases involving string concatenation where the results differ from what one may expect.

Now it should be apparent that the problem is not something that is likely happen all the time; in fact, depending on the programmer's coding style, the specific task to be solved, and other elements, it may even remain unknown to many and never show up. But when one happens to trigger it, it may be difficult to understand what's going on.

How to avoid this problem then? To quote the GNU awk manual:

when doing concatenation, parenthesize. Otherwise, you're never quite sure what you'll get.

In our examples, parentheses will indeed produce the intended results:

$ awk 'BEGIN { print 6 " " (-22) }'
6 -22
$ awk 'BEGIN { msg = "Hello "; print msg (++count) }'
Hello 1

And so on. Better clutter the code with some extra few parentheses than leave the outcome at the mercy of awk's grammar.

Here is the whole thread where this was discussed.