Search code examples
linuxpython-3.xread-eval-print-loopioctl

Why does Python perform an `TIOCGWINSZ` ioctl call when opening a character device file?


I'm currently developing a Linux device driver and am currently putting the whole character device infrastructure business in place; mostly boring stuff, populate file_operations structure with handler functions and in parallel I'm writing a small test suite in Python.

Relevant code kernel side (not much to see here really)

#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 32)
/* We try to keep the preprocessor #if/#endif mayhem to a minimum, but
 * this is one of the few places where there's no way around it, other
 * than obfuscating the function definitions behind proprocessor mayhem
 * at a different place.
 *
 * The following two helper macros abstract away the kernel version specific
 * ioctl function prototypes and access to the file pointer inode. */

#define IOCTL_FUNC(name, _inode, _filp, _cmd, _args) \
    int name(struct inode *_inode, struct file  *_filp, unsigned int _cmd, unsigned long _args)
#define IOCTL_INODE(_filp, _inode) \
    (void)_inode;
#else
#define IOCTL_FUNC(name, _inode, _filp, _cmd, _args) \
    long name(struct file  *_filp, unsigned int _cmd, unsigned long _args)
#  define IOCTL_INODE(_filp, _inode) \
    struct inode *_inode = file_inode(_filp); \
    (void)_inode;
#endif

static
int dwdsys_dev_from_inode_or_file(
    struct inode *inode,
    struct file *filp,
    struct dwddev **out_dwd )
{
    int rc = -ENODEV;
    struct dwddev *dwd = NULL;
    struct dwdsys_linux *dsl;
    list_for_each_entry( dsl, &dwdsys_list, entry ){
        if( MAJOR(asl->devno_base) == MAJOR(inode->i_rdev) ){
            unsigned const i_board = MINOR(inode->i_rdev);
            if( i_board < dsl->ds.n_boards ){
                dwd = dsl->ds.board[i_board];
                rc= 0;
                break;
            }
        }
    }

    /* XXX: cache dwd in either filp->private_data or inode->i_private */
    if( !rc ){
        if( out_dwd ){ *out_dwd = dwd; }
    }
    return rc;
}

static IOCTL_FUNC(dwdsys_chrdev_ioctl, inode, filp, cmd, args)
{
    int rc;
    struct dwddev *dwd= NULL;
    IOCTL_INODE(filp, inode);

    rc= dwdsys_dev_from_inode_or_file(inode, filp, &dwd);
    if( rc ){ goto fail; }
    switch( cmd ){
    default:
        rc= -EINVAL;
        break;

    case ...:
        /* ... */
    }

fail:
    DWD__TRACE_FUNCTION(dwd, rc, "cmd=0x%08x, args=%p", cmd, (void*)args); 
    return rc;
}

static struct file_operations dwdsys_chrdev_fops = {
    .owner      = THIS_MODULE,
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 30)
    .ioctl          = dwdsys_chrdev_ioctl,
#else
    .unlocked_ioctl = dwdsys_chrdev_ioctl,
#endif
};

And I just noticed that when I open the driver's device node within the Python interactive REPL the driver will report an unsupported ioctl call, command code 0x5413 which translates into TIOCSWINSZ. That would be ioctl that's being used on VTs/PTYs to set the window size. I can see why the Python REPL would do that ioctl on, say, stdio. But it seems an odd thing to do unconditionally.

Here's what I do in the Python REPL

>>> dwd = open("/dev/dwd0a", "r")

That's it. This will make my driver spit a warning into the kernel log, that an unsupported ioctl was called.

So the question is: Is this intended, specified behaviour? Or is this something unintended, they maybe should be reported as a bug?

Update due to request in comments / console output of strace-ed session

dw@void: ~/dwd/src/linux master ⚡
$ python
Python 3.5.2 (default, Oct 19 2016, 17:19:49) 
[GCC 4.9.4] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> dwd = open("/dev/dwd0a", "r")
[1]  + 1906 suspended  python

At this point, before hitting enter I suspended the Python REPL process, and attached strace to it, before foregrounding it again…

dw@void: ~/dwd/src/linux master ⚡
$ sudo strace -p 1906 &
[2] 1922
strace: Process 1906 attached                                                                                      
--- stopped by SIGTSTP ---
 
dw@void: ~/dwd/src/linux master ⚡
$ fg
[1]  - 1906 continued  python
--- SIGCONT {si_signo=SIGCONT, si_code=SI_USER, si_pid=738, si_uid=1000} ---
select(1, [0], NULL, NULL, NULL)        = 1 (in [0])
rt_sigaction(SIGWINCH, {0x7fd1524463e0, [], SA_RESTORER|SA_RESTART, 0x7fd152fbcbef}, {0x7fd152668980, [], SA_RESTORER, 0x7fd152fbcbef}, 8) = 0
read(0, "\n", 1)                        = 1
writev(1, [{iov_base="", iov_len=0}, {iov_base="\n", iov_len=1}], 2) = 1
ioctl(0, SNDCTL_TMR_STOP or TCSETSW, {B38400 opost isig icanon echo ...}) = 0
rt_sigaction(SIGWINCH, {0x7fd152668980, [], SA_RESTORER, 0x7fd152fbcbef}, {0x7fd1524463e0, [], SA_RESTORER|SA_RESTART, 0x7fd152fbcbef}, 8) = 0
open("/dev/dwd0a", O_RDONLY|O_CLOEXEC)  = 3
fcntl(3, F_SETFD, FD_CLOEXEC)           = 0
fstat(3, {st_mode=S_IFCHR|0666, st_rdev=makedev(240, 0), ...}) = 0
ioctl(3, TIOCGWINSZ, 0x7fffb1291f00)    = -1 EINVAL (Invalid argument)
lseek(3, 0, SEEK_CUR)                   = -1 ESPIPE (Invalid seek)
ioctl(3, TIOCGWINSZ, 0x7fffb1291eb0)    = -1 EINVAL (Invalid argument)
getcwd("/home/dw/dwd/src/linux", 1024) = 31
stat("/home/dw/dwd/src/linux", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
stat("/usr/lib/python3.5", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
stat("/usr/lib/python3.5/_bootlocale.py", {st_mode=S_IFREG|0644, st_size=1301, ...}) = 0
stat("/usr/lib/python3.5/_bootlocale.py", {st_mode=S_IFREG|0644, st_size=1301, ...}) = 0
open("/usr/lib/python3.5/__pycache__/_bootlocale.cpython-35.pyc", O_RDONLY|O_CLOEXEC) = 4
fcntl(4, F_SETFD, FD_CLOEXEC)           = 0
fstat(4, {st_mode=S_IFREG|0644, st_size=1028, ...}) = 0
lseek(4, 0, SEEK_CUR)                   = 0
fstat(4, {st_mode=S_IFREG|0644, st_size=1028, ...}) = 0
read(4, "\26\r\r\n5\253\7X\25\5\0\0\343\0\0\0\0\0\0\0\0\0\0\0\0\v\0\0\0@\0\0"..., 1029) = 1028
read(4, "", 1)                          = 0
close(4)                                = 0
lseek(3, 0, SEEK_CUR)                   = -1 ESPIPE (Invalid seek)
brk(0x562c9d5c0000)                     = 0x562c9d5c0000
ioctl(0, TIOCGWINSZ, {ws_row=45, ws_col=115, ws_xpixel=809, ws_ypixel=589}) = 0
ioctl(1, TIOCGWINSZ, {ws_row=45, ws_col=115, ws_xpixel=809, ws_ypixel=589}) = 0
ioctl(0, TIOCGWINSZ, {ws_row=45, ws_col=115, ws_xpixel=809, ws_ypixel=589}) = 0
ioctl(0, TIOCSWINSZ, {ws_row=45, ws_col=115, ws_xpixel=809, ws_ypixel=589}) = 0
ioctl(0, TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(0, SNDCTL_TMR_STOP or TCSETSW, {B38400 opost isig -icanon -echo ...}) = 0
writev(1, [{iov_base=">>> ", iov_len=4}, {iov_base=NULL, iov_len=0}], 2>>> ) = 4

Solution

  • I've some similar problems when calling an ioctl of a (custom) linux driver. I found out that using os.open solves my problem. In the description of os.open they say that it is intended for low-level I/O. So maybe you shouldn't open device nodes with build-in open() even if everybody does it? If it wouldn't be a custom driver with an error message about unknown ioctls I would have never recognised that there are some other ioctls anyway.

    Using built-in open():

    with open('/dev/hdmi_0_0_0', 'r') as fd:
        fcntl.ioctl(fd, 0x40084814, reg_acc)
    

    =>

    openat(AT_FDCWD, "/dev/hdmi_0_0_0", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3
    fstat64(3, {st_mode=S_IFCHR|0600, st_rdev=makedev(239, 0), ...}) = 0
    ioctl(3, TCGETS, 0xffe55e68)            = -1 ENOSYS (Function not implemented)
    _llseek(3, 0, 0xffe55d58, SEEK_CUR)     = -1 ESPIPE (Illegal seek)
    ioctl(3, TCGETS, 0xffe55e08)            = -1 ENOSYS (Function not implemented)
    _llseek(3, 0, 0xffe55c48, SEEK_CUR)     = -1 ESPIPE (Illegal seek)
    ioctl(3, _IOC(_IOC_READ, 0x48, 0x14, 0x8), 0xffe55c38) = 0
    

    Using os.open():

    fd = os.open('/dev/hdmi_0_0_0', os.O_RDWR | os.O_SYNC)
    fcntl.ioctl(fd, 0x40084814, reg_acc)
    os.close(fd)
    

    =>

    openat(AT_FDCWD, "/dev/hdmi_0_0_0", O_RDWR|O_SYNC|O_LARGEFILE|O_CLOEXEC) = 3
    ioctl(3, _IOC(_IOC_READ, 0x48, 0x14, 0x8), 0xffa076e8) = 0