Relinking an anonymous (unlinked but open) file
Asked Answered
R

5

43

In Unix, it's possible to create a handle to an anonymous file by, e.g., creating and opening it with creat() and then removing the directory link with unlink() - leaving you with a file with an inode and storage but no possible way to re-open it. Such files are often used as temp files (and typically this is what tmpfile() returns to you).

My question: is there any way to re-attach a file like this back into the directory structure? If you could do this it means that you could e.g. implement file writes so that the file appears atomically and fully formed. This appeals to my compulsive neatness. ;)

When poking through the relevant system call functions I expected to find a version of link() called flink() (compare with chmod()/fchmod()) but, at least on Linux this doesn't exist.

Bonus points for telling me how to create the anonymous file without briefly exposing a filename in the disk's directory structure.

Radom answered 13/11, 2010 at 8:47 Comment(0)
C
44

A patch for a proposed Linux flink() system call was submitted several years ago, but when Linus stated "there is no way in HELL we can do this securely without major other incursions", that pretty much ended the debate on whether to add this.

Update: As of Linux 3.11, it is now possible to create a file with no directory entry using open() with the new O_TMPFILE flag, and link it into the filesystem once it is fully formed using linkat() on /proc/self/fd/fd with the AT_SYMLINK_FOLLOW flag.

The following example is provided on the open() manual page:

    char path[PATH_MAX];
    fd = open("/path/to/dir", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR);

    /* File I/O on 'fd'... */

    snprintf(path, PATH_MAX,  "/proc/self/fd/%d", fd);
    linkat(AT_FDCWD, path, AT_FDCWD, "/path/for/file", AT_SYMLINK_FOLLOW);

Note that linkat() will not allow open files to be re-attached after the last link is removed with unlink().

Consanguinity answered 13/11, 2010 at 19:13 Comment(8)
Ta. He proposes a solution which should also work, mind you. Although for full compulsive neatness you probably also need a way to call creat() on a directory so that it creates the file and inode but not the directory entry, so that it's never linked in the first place.Radom
The update is full of win. I can't +2 you but I would if I could.Radom
Confusingly, linkat() gives ENOENT on attempts to re-attach a normal open-but-unlinked file. (with either AT_SYMLINK_FOLLOW or AT_EMPTY_PATH)Mesopotamia
I posted a perl wrapper (which isn't actually useful, since you still can't re-link files with no existing links) as a separate answer.Mesopotamia
Can linkat also be used on a system that doesn't have /proc (e.g. macOS)? If so, what would the first path argument be?Vinificator
@Vinificator /dev/fd/fd. But it doesn't (seem) to work, as it gives a Cross-device link error.Wittman
@Wittman That is because you are trying to hardlink the /dev/fd/X symlink itself. For ln you need to specify -L to hardlink a target of this link (giving ENOENT Peter Cordes mentions)Carnot
@PeterCordes I agree it is confusing. I would expect EPERM. But that is what git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/… does. Credits go to rustyx at SF question linked by cdhowie below.Carnot
M
4

Thanks to @mark4o posting about linkat(2), see his answer for details.

I wanted to give it a try to see what actually happened when trying to actually link an anonymous file back into the filesystem it is stored on. (often /tmp, e.g. for video data that firefox is playing).


As of Linux 3.16, there still appears to be no way to undelete a deleted file that's still held open. Neither AT_SYMLINK_FOLLOW nor AT_EMPTY_PATH for linkat(2) do the trick for deleted files that used to have a name, even as root.

The only alternative is tail -c +1 -f /proc/19044/fd/1 > data.recov, which makes a separate copy, and you have to kill it manually when it's done.


Here's the perl wrapper I cooked up for testing. Use strace -eopen,linkat linkat.pl - </proc/.../fd/123 newname to verify that your system still can't undelete open files. (Same applies even with sudo). Obviously you should read code you find on the Internet before running it, or use a sandboxed account.

#!/usr/bin/perl -w
# 2015 Peter Cordes <[email protected]>
# public domain.  If it breaks, you get to keep both pieces.  Share and enjoy

# Linux-only linkat(2) wrapper (opens "." to get a directory FD for relative paths)
if ($#ARGV != 1) {
    print "wrong number of args.  Usage:\n";
    print "linkat old new    \t# will use AT_SYMLINK_FOLLOW\n";
    print "linkat - <old  new\t# to use the AT_EMPTY_PATH flag (requires root, and still doesn't re-link arbitrary files)\n";
    exit(1);
}

# use POSIX qw(linkat AT_EMPTY_PATH AT_SYMLINK_FOLLOW);  #nope, not even POSIX linkat is there

require 'syscall.ph';
use Errno;
# /usr/include/linux/fcntl.h
# #define AT_SYMLINK_NOFOLLOW   0x100   /* Do not follow symbolic links.  */
# #define AT_SYMLINK_FOLLOW 0x400   /* Follow symbolic links.  */
# #define AT_EMPTY_PATH     0x1000  /* Allow empty relative pathname */
unless (defined &AT_SYMLINK_NOFOLLOW) { sub AT_SYMLINK_NOFOLLOW() { 0x0100 } }
unless (defined &AT_SYMLINK_FOLLOW  ) { sub AT_SYMLINK_FOLLOW  () { 0x0400 } }
unless (defined &AT_EMPTY_PATH      ) { sub AT_EMPTY_PATH      () { 0x1000 } }


sub my_linkat ($$$$$) {
    # tmp copies: perl doesn't know that the string args won't be modified.
    my ($oldp, $newp, $flags) = ($_[1], $_[3], $_[4]);
    return !syscall(&SYS_linkat, fileno($_[0]), $oldp, fileno($_[2]), $newp, $flags);
}

sub linkat_dotpaths ($$$) {
    open(DOTFD, ".") or die "open . $!";
    my $ret = my_linkat(DOTFD, $_[0], DOTFD, $_[1], $_[2]);
    close DOTFD;
    return $ret;
}

sub link_stdin ($) {
    my ($newp, ) = @_;
    open(DOTFD, ".") or die "open . $!";
    my $ret = my_linkat(0, "", DOTFD, $newp, &AT_EMPTY_PATH);
    close DOTFD;
    return $ret;
}

sub linkat_follow_dotpaths ($$) {
    return linkat_dotpaths($_[0], $_[1], &AT_SYMLINK_FOLLOW);
}


## main
my $oldp = $ARGV[0];
my $newp = $ARGV[1];

# link($oldp, $newp) or die "$!";
# my_linkat(fileno(DIRFD), $oldp, fileno(DIRFD), $newp, AT_SYMLINK_FOLLOW) or die "$!";

if ($oldp eq '-') {
    print "linking stdin to '$newp'.  You will get ENOENT without root (or CAP_DAC_READ_SEARCH).  Even then doesn't work when links=0\n";
    $ret = link_stdin( $newp );
} else {
    $ret = linkat_follow_dotpaths($oldp, $newp);
}
# either way, you still can't re-link deleted files (tested Linux 3.16 and 4.2).

# print STDERR 
die "error: linkat: $!.\n" . ($!{ENOENT} ? "ENOENT is the error you get when trying to re-link a deleted file\n" : '') unless $ret;

# if you want to see exactly what happened, run
# strace -eopen,linkat  linkat.pl
Mesopotamia answered 21/2, 2015 at 22:55 Comment(2)
tail /proc thing saved my day. One just needs to use proper process id and file descriptor. Thanks!Mycenaean
@4LegsDrivenCat: Yup, I've used it myself sometimes after an accidental rm. Useful tools for finding the right PID and fd might include lsof, or once you have a PID, ls -l /proc/<PID>/fd will shows you useful info via the symlink target names even for deleted files (like the original name). And /proc/<PID>/fdinfo/* can tell you the file positions and stuff in case that helps.Mesopotamia
S
2

My question: is there any way to re-attach a file like this back into the directory structure? If you could do this it means that you could e.g. implement file writes so that the file appears atomically and fully formed. This appeals to the my compulsive neatness. ;)

If this is your only goal, you can achieve this in a much simpler and more widely used manner. If you are outputting to a.dat:

  1. Open a.dat.part for write.
  2. Write your data.
  3. Rename a.dat.part to a.dat.

I can understand wanting to be neat, but unlinking a file and relinking it just to be "neat" is kind of silly.

This question on serverfault seems to indicate that this kind of re-linking is unsafe and not supported.

Slowworm answered 13/11, 2010 at 8:54 Comment(6)
cdhowie is right that just writing to a temporary file is much better. Note that the question you link to basically says it can't be done though: you can't hardlink from /proc to another filesystem.Vocational
@Vocational Somehow I missed that. Switched links to a more appropriate question on serverfault.Slowworm
The difference is that in the serverfault question the program is an opaque thing (being a sysadmin forum and all - here I'm talking about actually having the file handle about to play with programatically from within the process. If you can categorically exclude that too, we have an answer ;)Radom
Also, I don't think it is silly. The file has all the advantages of a temp file - basically not present, guaranteed one writer/reader, etc. - until you decide it's time to give it to the user. And it goes away if your program crashes, too, rather than being a corrupt half-written file.Radom
It's not inherently silly to want to do it, but it's a bit silly to want to do it given the Unix model and APIs we work with today.Vocational
@Radom Agreed, a file that is not present until ready is not silly. Quoting manpages.debian.org/bullseye/manpages-dev/linkat.2.en.html : This will generally not work if the file has a link count of zero (files created with O_TMPFILE and without O_EXCL are an exception) - i.e. if you create your file like that it will work.Carnot
C
0

Clearly, this is possible -- fsck does it, for example. However, fsck does it with major localized file system mojo and will clearly not be portable, nor executable as an unprivileged user. It's similar to the debugfs comment above.

Writing that flink(2) call would be an interesting exercise. As ijw points out, it would offer some advantages over current practice of temporary file renaming (rename, note, is guaranteed atomic).

Cling answered 13/11, 2010 at 14:49 Comment(0)
U
-2

Kind of late to the game but I just found http://computer-forensics.sans.org/blog/2009/01/27/recovering-open-but-unlinked-file-data which may answer the question. I haven't tested it, though, so YMMV. It looks sound.

Unprovided answered 30/8, 2013 at 21:8 Comment(1)
As I expected, that's just cat /proc/<pid>/fd/N > newfile. Neat if you didn't know about /proc/fd, but not the answer to this question. Further changes to the deleted file won't be reflected after the snapshot you get with cp or cat. (tail -c +1 -f /proc/<pid>/fd/N > newfile should leave you with a copy of the contents, if the process writing it only appends, though.)Mesopotamia

© 2022 - 2024 — McMap. All rights reserved.