Specify the inode of a file?

Is there any way to create a file on Solaris 10 (ZFS preferably, but UFS would be helpful as well) with a specific inode number? I need to create a file with a large inode, greater than a 32bit integer.

I am trying to test a piece of software which may be incorrectly truncating large inodes down to 32-bit numbers.

How is the application getting the inode of a file? Is it done through stat system call?

I'm not sure, I don't work with any of the code unfortunately.

Put this into "fake_inode.d":

syscall::stat64:entry, syscall::lstat64:entry
/strstr(copyinstr(arg0), "passwd") != NULL/
{
  self->statptr = arg1;
}

syscall::stat64:return, syscall::lstat64:return
/self->statptr != NULL && curpsinfo->pr_dmodel == PR_MODEL_ILP32/
{
  self->st64_32 = (struct stat64_32 *)copyin(self->statptr, sizeof(struct stat64_32));
  self->st64_32->st_ino = 100000000000;
  copyout(self->st64_32, self->statptr, sizeof(struct stat64_32));
}

syscall::stat64:return, syscall::lstat64:return
/self->statptr != NULL && curpsinfo->pr_dmodel == PR_MODEL_LP64/
{
  self->st64 = (struct stat64 *)copyin(self->statptr, sizeof(struct stat64));
  self->st64->st_ino = 100000000000;
  copyout(self->st64, self->statptr, sizeof(struct stat64));
}

Replace "passwd" with whatever filename you want to fake inode for. Then run:

dtrace -w -s fake_inode.d

In the other terminal run:

ls -li /etc/passwd

Change the filename of the file that you are checking with "ls", if you changed the filename in the "fake_inode.d" script. This should produce:

# ls -li /etc/passwd
100000000000 -rw-r--r--   1 root     sys         1972 Jun 13  2013 /etc/passwd
4 Likes

Thank you. I changed the filename to "passwd2" because I wasn't sure what impact this would have on my /etc/passwd file.

Was I supposed to run fake_inode.d from within /etc? I created fake_inode.d and passwd2 inside /data, and ran the script from there. It has been saying this for a while:

[root@sol10: /data]# dtrace -w -s fake_inode.d
dtrace: script 'fake_inode.d' matched 6 probes
dtrace: allowing destructive actions

The dtrace script has hopefully no impact in the target file.

From anywhere you like.

This is the expected output. You should leave the script running for the inode renumbering hack to persist.
As Bartus11 already stated, just use another terminal to experiment with you program expecting a large inode number.

1 Like

Thank you for the clarification!

It looks like when I run

ls -li

it shows the real inode, but

ls -li <filename>

is showing the fake (large) inode:

[root@sol10: /data/xx]# ls -li
total 770488
  52000532 -rw-r--r--   1 root     root           0 Mar 24 10:52 asdf
  68687063 -rw-r--r--   1 root     root           0 Mar 25 17:18 passwd2

[root@sol10: /data/xx]# ls -li /data/xx/passwd2
100000000000 -rw-r--r--   1 root     root           0 Mar 25 17:18 /data/xx/passwd2

:confused:

That's the reason why Bartus11 asked you about what your application uses to read the inode number. "ls -li file" uses stat64 which is temporarily "patched" with the dtrace script, "ls -li" alone uses getdents which is not.

You should trace your application to figure out what it uses. Use the truss command to do it.

1 Like

Ah, I see thank you. I ran my command with truss, and greped the output for both stat64 and getdents. Grep for stat64 returned many results including:

[root@sol10: /test]# cat truss.log |grep stat64
...
fstat64(3, 0xFFBFF810)                          = 0
fstat64(2, 0xFFBFE2D8)                          = 0
stat64("./libc.so.1", 0xFFBFD328)               Err#2 ENOENT
fstat64(4, 0xFFBFD160)                          = 0
fstat64(4, 0xFFBFD008)                          = 0
fstat64(4, 0xFFBFD3B8)                          = 0
fstat64(4, 0xFFBFD260)                          = 0
/1:     lstat64("/test", 0xFFBFD600)                    = 0
/1:     fstat64(4, 0xFFBFD358)                          = 0
/1:     lstat64("/test", 0xFFBFD340)                    = 0
/1:     lstat64("/test/..", 0xFFBFD340)                 = 0
/1:     lstat64("/test/passwd2", 0xFFBFD340)            = 0
/1:     lstat64("/test/fake_inode.d", 0xFFBFD340)       = 0
/1:     lstat64("/test/truss.log", 0xFFBFD340)          = 0
/1:     fstat64(5, 0xFFBFD368)                          = 0
/1:     fstat64(5, 0xFFBFD210)                          = 0

And grep for getdents returns:

[root@sol10: /test]# cat truss.log |grep getdents
/1:     getdents64(4, 0xFE994000, 8192)                 = 144
/1:     getdents64(4, 0xFE994000, 8192)                 = 0

edit: Is there a way to change the inode of a file permanently? Or specify the inode when creating a new file?

As far as I'm aware, it is not possible (or very hard to do). Did you confirm that the DTrace script did not change the inode reported by your application?

Ah darn, okay. Unfortunately I did not get my expected results while running the script. Thank you for the help though!

It is possible that the application is using "fstat*" system calls to get inode numbers. I can prepare a new version of the DTrace script tomorrow, if you are interested.

I'm actually going to be out of town for a few days starting tomorrow so I won't be able to look at this issue until I return, but if is still unresolved when I get back I will attempt to bump the thread. Thanks again!

Kind of. Create more than four billion empty files and the next ones will have an inode not fitting in a 32 bit integer. That might be a lenghty task though ...

I believe that this might be possible (for ufs filesystems) via a combination of fsdb and fsck. I don't have a test system available so I am not going to try it. The general flow would be:

  1. create a filesystem with enough inodes but do not mount it
  2. use fsdb to increment the link count of the desired inode
  3. use fsck to find what now looks like an orphaned file and reattach it to lost+found

I have never tried this because I have always been content with the inode number I get. My feeling is that I could make it work... but I would not be real surprised if I trash the file system a few times during the attempt. I definately would not try this on a filesystem that I cared about.

If you try this and get it to work, please post the fsdb commands used to increment the link count on your chosen inode number.

1 Like

There are only a few ways to get an inode number. Offhand, I know of getdents(), stat(), and fstat(). Oh, and getdents64(), stat64(), and fstat64().

You can truss the failing process and see what it uses:

truss -f -a -vall -o /truss/output/file command args...

FWIW, I'd bet quite a sum that your program is compiled as a 32-bit process and is using standard calls such as stat(), open(), etc. Read the man page for types.h. Note that an ino_t is an unsigned long, which in an ILP32 environment such as a 32-bit Solaris application is a 32-bit value. But in the LP64 environment of a 64-bit Solaris application an ino_t is 64 bit value. If you need the 64-bit value in a 32-bit application you need to use the open64(), stat64(), etc functions throughout your code.

Read and heed the man pages for lfcompile, lfcompile64, and lf64.

Another thing to watch out for: NFSv2. NFSv2 does not handle 64-bit values. And most NFS mounts are set to autonegotiate. And guess what? The negotiation is done via UDP. Sometimes that fails and you don't get your desired NFSv4 or NFSv3 mount and you get a fallback to an NFSv2 mount. If you have large files, or files with large inode values, you will see some really strange results. Run nfsstat, and if your v2 stats aren't all zero, you're in trouble. So always, always, always specify "vers=3" or "vers=4" in your mounts, or set the minimum version your server will serve to 3. (It's in a config file somewhere, that I'm too lazy to look up right now.)

I've modified the DTrace script to handle "fstat64" as well:

BEGIN
{
  filename = "passwd";
}

syscall::stat64:entry, syscall::lstat64:entry
/strstr(copyinstr(arg0), filename) != NULL/
{
  self->statptr = arg1;
}

syscall::fstat64:entry
/strstr(fds[arg0].fi_name, filename) != NULL/
{
  self->statptr = arg1;
}

syscall::stat64:return, syscall::lstat64:return
/self->statptr != NULL && curpsinfo->pr_dmodel == PR_MODEL_ILP32/
{
  self->st64_32 = (struct stat64_32 *)copyin(self->statptr, sizeof(struct stat64_32));
  self->st64_32->st_ino = 100000000000;
  copyout(self->st64_32, self->statptr, sizeof(struct stat64_32));
}

syscall::stat64:return, syscall::lstat64:return
/self->statptr != NULL && curpsinfo->pr_dmodel == PR_MODEL_LP64/
{
  self->st64 = (struct stat64 *)copyin(self->statptr, sizeof(struct stat64));
  self->st64->st_ino = 100000000000;
  copyout(self->st64, self->statptr, sizeof(struct stat64));
}

syscall::fstat64:return
/self->statptr != NULL && curpsinfo->pr_dmodel == PR_MODEL_ILP32/
{
  self->st64_32 = (struct stat64_32 *)copyin(self->statptr, sizeof(struct stat64_32));
  self->st64_32->st_ino = 100000000000;
  copyout(self->st64_32, self->statptr, sizeof(struct stat64_32));
}

syscall::fstat64:return
/self->statptr != NULL && curpsinfo->pr_dmodel == PR_MODEL_LP64/
{
  self->st64 = (struct stat64 *)copyin(self->statptr, sizeof(struct stat64));
  self->st64->st_ino = 100000000000;
  copyout(self->st64, self->statptr, sizeof(struct stat64));
}

Simple test to see if it is really working:

# perl -le 'open $fh, "<", "/etc/passwd";print ((stat $fh)[1])' 
100000000000

BTW, when faking the inode for "/etc/passwd", I noticed that I can not login to the server by SSH, so it is probably better to use some other file name for your tests :wink: