When the loop exits, $fh is out of scope, and the
filehandle it contains is garbage-collected, closing the file.

"Duh."

Several people suggested that it was because open files are not
preserved across an exec, or because the meaning of
/proc/self would change after an exec, perhaps because the
command was being run in a separate process; this is mistaken. There
is only one process here. The exec call does not create a
new process; it reuses the same one, and it does not affect open
files, unless they have been flagged with FD_CLOEXEC.

Abhijit Menon-Sen ran a slightly different test than I did:

% z cat foo.gz bar.gz
cat: /proc/self/fd/3: No such file or directory
cat: /proc/self/fd/3: No such file or directory

As he said, this makes it completely obvious what is wrong, since
the two files are both represented by the same file descriptor.