[patch v3] truncate.2: EINVAL is returned for non regular files except directories

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



truncate(2) returns EINVAL if the file argument is a socket, a FIFO or
a character or block device. The current man page indicates that
ftruncate() returns EINVAL for an fd that does not reference a regular
file, but for truncate() the only reason given for returning EINVAL is
that the length is invalid.

In the Linux source code in fs/open.c, vfs_truncate(), called from do_sys_truncate() starts
with the following checks:
        /* For directories it's -EISDIR, for other non-regulars - -EINVAL */
        if (S_ISDIR(inode->i_mode))
                return -EISDIR;
        if (!S_ISREG(inode->i_mode))
                return -EINVAL;

and do_sys_truncate() contains:
        error = -EINVAL;
        if (!S_ISREG(inode->i_mode) || !(f.file->f_mode & FMODE_WRITE))
                goto out_putf;
with no check for S_IS_DIR(inode-i_mode).

truncate() therefore returns EISDIR for a directory, and EINVAL for any other non-regular
file, whereas ftruncate() returns EINVAL for any non-regular file.

The following test program demonstrates the errors returned by truncate() and ftruncate():
=====================================================
 #define _GNU_SOURCE

 #include <err.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <sys/stat.h>
 #include <sys/sysmacros.h>
 #include <sys/types.h>
 #include <unistd.h>

struct {
    const char *fname;
    mode_t mode;
    unsigned dev_maj;
    unsigned dev_min;
} nodes[] = {
    {"/tmp/trunc_file", S_IFREG | 0666, 0, 0},
    {"/tmp/trunc_fifo", S_IFIFO | 0666, 0, 0},
    {"/tmp/trunc_socket", S_IFSOCK | 0666, 0, 0},
    {"/tmp/trunc_char_dev", S_IFCHR | 0666, 10, 7},  // Second Amiga mouse, /dev/amigamouse1
    {"/tmp/trunc_blk_dev", S_IFBLK | 0666, 13, 3},  // Was XT disk /dev/xd3
    {"/tmp/trunc_dir", 0666, 0, 0},
};

int
main(void)
{
    int  ret;
    int  fd;
    const char *fname;

    for (size_t n = 0; n < sizeof(nodes) / sizeof(nodes[0]); n++) {
        fname = nodes[n].fname;

        /* Create the node */
        if (!(nodes[n].mode & S_IFMT))
            ret = mkdir(fname, nodes[n].mode);
        else
            ret = mknod(fname, nodes[n].mode,
                        makedev(nodes[n].dev_maj, nodes[n].dev_min));

        if (ret == -1) {
            warn("mknod(\"%s\") %#m", fname);
            continue;
        }

        /* Returns EINVAL for IFSOCK, IFIFO, S_IFBLK, S_IFCHR, EISDIR for a directory */
        errno = 0;
        ret = truncate(fname, 0);
        warn("truncate(\"%s\"): %#m", fname);

	/* We cannot open device nodes when they are not real,
	 * so replace character device with /dev/null.
	 * We don't want to mess with real block devices! */
	if ((nodes[n].mode & S_IFMT) == S_IFCHR)
	    fname = "/dev/null";

	/* We cannot open a directory for writing. The ftruncate() call will
	 * therefore return EINVAL since the fd is not open for writing. */
        fd = open(fname, !(nodes[n].mode & S_IFMT) ? O_RDONLY : O_RDWR);
        if (fd == -1)
            warn("open(\"%s\"): %#m", fname);
        else {
            errno = 0;
            ftruncate(fd, 0);
            warn("ftruncate(\"%s\"): %#m", fname);

            if (close(fd) == -1)
                warn("close(\"%s\"): %#m", fname);
        }

        /* Remove the nodes */
        ret = (nodes[n].mode & S_IFMT) ? unlink(nodes[n].fname)
                                       : rmdir(nodes[n].fname);
        if (ret == -1)
            warn("unlink(\"%s\"): %#m", nodes[n].fname);

	fprintf(stderr, "\n");
    }
}
=====================================================
Compile the program and run it as user root.

The output (if program name is trunc) should be:

trunc: truncate("/tmp/trunc_file"): 0: Success
trunc: ftruncate("/tmp/trunc_file"): 0: Success

trunc: truncate("/tmp/trunc_fifo"): EINVAL: Invalid argument
trunc: ftruncate("/tmp/trunc_fifo"): EINVAL: Invalid argument

trunc: truncate("/tmp/trunc_socket"): EINVAL: Invalid argument
trunc: open("/tmp/trunc_socket"): ENXIO: No such device or address

trunc: truncate("/tmp/trunc_char_dev"): EINVAL: Invalid argument
trunc: ftruncate("/dev/null"): EINVAL: Invalid argument

trunc: truncate("/tmp/trunc_blk_dev"): EINVAL: Invalid argument
trunc: open("/tmp/trunc_blk_dev"): EACCES: Permission denied

trunc: truncate("/tmp/trunc_dir"): EISDIR: Is a directory
trunc: ftruncate("/tmp/trunc_dir"): EINVAL: Invalid argument

The patch below has different wording for EINVAL for truncate() and ftruncate() because they
return different errors for directories. It also removes the first description for EBADF or
EINVAL for ftruncate() since a more complete description is given two entries later.

Signed-off-by: Quentin Armitage <quentin@xxxxxxxxxxxxxxx>

diff --git a/man2/truncate.2 b/man2/truncate.2
index 703f598b3..2eb4557cc 100644
--- a/man2/truncate.2
+++ b/man2/truncate.2
@@ -112,7 +112,9 @@ and
 .B EINVAL
 The argument
 .I length
-is negative or larger than the maximum file size.
+is negative or larger than the maximum file size,
+or the named file is a socket, a FIFO,
+or a block or character device.
 .TP
 .B EIO
 An I/O error occurred updating the inode.
@@ -160,10 +162,6 @@ we now have things that can be wrong with the file descriptor,
 .I fd
 is not a valid file descriptor.
 .TP
-.BR EBADF " or " EINVAL
-.I fd
-is not open for writing.
-.TP
 .B EINVAL
 .I fd
 does not reference a regular file or a POSIX shared memory object.




[Index of Archives]     [Kernel Documentation]     [Netdev]     [Linux Ethernet Bridging]     [Linux Wireless]     [Kernel Newbies]     [Security]     [Linux for Hams]     [Netfilter]     [Bugtraq]     [Yosemite News]     [MIPS Linux]     [ARM Linux]     [Linux RAID]     [Linux Admin]     [Samba]

  Powered by Linux