grantpt() · unlockpt() · ptsname() — Preparing the PTY Slave

 

Chapter 64 – Pseudoterminals (Part 3)
grantpt() · unlockpt() · ptsname() — Preparing the PTY Slave
grantpt
Fix slave permissions
unlockpt
Unlock slave device
ptsname
Get slave device path

What These Functions Do

After posix_openpt() opens the master side, the slave device file (/dev/pts/N) exists but is not yet safe to open. Before any other process opens the slave, you must:

grantpt(mfd)
Fix slave ownership & mode
owner=caller, group=tty
unlockpt(mfd)
Remove internal kernel lock
slave can now be opened
ptsname(mfd)
Get slave path string
e.g. “/dev/pts/7”
open(slaveName, …)
Open the slave fd

Each of these three functions takes the master file descriptor (mfd) as its argument. They work on the slave that corresponds to that master — you never have to name the slave explicitly until ptsname().

64.2.2 Changing Slave Ownership and Permissions: grantpt()

When a PTY master is opened, the kernel creates the slave device file but its ownership and permissions may not be correct for the calling user. grantpt() fixes this.

Function Signature

#define _XOPEN_SOURCE 500
#include <stdlib.h>

int grantpt(int mfd);
/* Returns 0 on success, or -1 on error */

What grantpt() Changes on the Slave Device

Property Before grantpt() After grantpt()
Owner root (or unset) Effective UID of calling process
Group root (or unset) tty
Permissions Unset Owner: rw-, Group: -w-, Other: —
crw--w----

How grantpt() Works Internally (non-Linux)

On systems where grantpt() is actually required to do work (not Linux), it forks a child process that executes a special set-user-ID-root helper program called pt_chown. This helper has root privilege and can change the device file’s ownership.

Calling process (your program)
fork() → child process
child exec()s pt_chown (set-UID-root helper)
pt_chown: chown slave → caller’s eUID, group → tty, chmod → crw–w—-
Slave device permissions are now correct

Why group=tty and group write permission?

The wall(1) and write(1) programs (which broadcast messages to all terminals) are set-group-ID programs owned by the tty group. To write a message to any terminal or PTY slave, they need write permission — the crw--w---- mode (group write allowed) gives them exactly that.

Important SUSv3 rule: Because grantpt() may fork a child process, SUSv3 states its behavior is unspecified if the calling program has installed a signal handler for SIGCHLD. The reason: the parent might reap the pt_chown child in the SIGCHLD handler before grantpt() can do so itself. If your program catches SIGCHLD, temporarily block or reset it around the grantpt() call on non-Linux systems.
On Linux specifically: The slave device is automatically configured with the correct ownership and permissions by the kernel when it is created. grantpt() is a no-op on Linux but must still be called for portable code that works on other UNIX systems.

64.2.3 Unlocking the Slave: unlockpt()

When a PTY master is first opened, the corresponding slave device has an internal kernel lock on it. unlockpt() removes that lock so that the slave can be opened by another process.

Function Signature

#define _XOPEN_SOURCE 500
#include <stdlib.h>

int unlockpt(int mfd);
/* Returns 0 on success, or -1 on error */

Why Is the Slave Locked in the First Place?

The locking mechanism ensures that the calling process can complete all initialization of the slave (such as calling grantpt() to fix permissions) before any other process is allowed to open it. Without this lock, a race condition could allow another process to open the slave before it is properly set up.

Time State of Slave Device
After posix_openpt() Slave exists but is LOCKED — open() would return EIO
After grantpt() Still locked — permissions are correct now, but slave still cannot be opened
After unlockpt() Slave is UNLOCKED — any process with permission can now open() it
What happens if you skip unlockpt()?
If you try to open() the slave device file before calling unlockpt(), the open will fail with errno == EIO. This is an easy bug to hit when porting old code that did not follow the standard four-step sequence.

64.2.4 Obtaining the Name of the Slave: ptsname()

ptsname() returns the filesystem path of the slave device corresponding to the open master. You need this path to open() the slave.

Function Signature

#define _XOPEN_SOURCE 500
#include <stdlib.h>

char *ptsname(int mfd);
/* Returns pointer to slave device name string on success,
   or NULL on error */

What Name Does It Return?

On Linux (and most UNIX systems), ptsname() returns a string of the form:

/dev/pts/N

where N is the PTY number. For example /dev/pts/0, /dev/pts/7, etc. Each number uniquely identifies one PTY slave on the system at a given time.

⚠ Static Buffer Warning:
The string returned by ptsname() points to a statically allocated internal buffer. This means:

  • Each call to ptsname() overwrites the previous result.
  • You must copy the result with strdup() or snprintf() before calling ptsname() again.
  • It is not thread-safe. In multi-threaded programs use ptsname_r() (Linux extension) instead.

Safe Usage Pattern

#define _XOPEN_SOURCE 500
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>

int open_pty_slave(int mfd)
{
    char *name;
    char slaveName[64];  /* our own buffer */

    name = ptsname(mfd);
    if (name == NULL) {
        perror("ptsname");
        return -1;
    }

    /* Copy immediately — the static buffer may be overwritten
       if ptsname() is called again elsewhere */
    strncpy(slaveName, name, sizeof(slaveName) - 1);
    slaveName[sizeof(slaveName) - 1] = '\0';

    printf("Slave device: %s\n", slaveName);

    return open(slaveName, O_RDWR | O_NOCTTY);
}

Thread-Safe Version: ptsname_r()

On Linux you can use the GNU extension ptsname_r() to write into a caller-supplied buffer, avoiding the static-buffer problem entirely:

#define _GNU_SOURCE
#include <stdlib.h>

/* ptsname_r() writes into buf instead of a static internal buffer */
int ptsname_r(int mfd, char *buf, size_t buflen);
/* Returns 0 on success, or a positive error number on error */

/* Usage: */
char slaveName[64];
if (ptsname_r(mfd, slaveName, sizeof(slaveName)) != 0) {
    perror("ptsname_r");
}

Complete Working Example: Open a PTY Pair and Pass Data

This program opens a full PTY pair using the standard four-step sequence, then writes a message from the master and reads it on the slave to prove the pair works.

#define _XOPEN_SOURCE 600
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

static int open_master(void)
{
    int mfd = posix_openpt(O_RDWR | O_NOCTTY);
    if (mfd == -1) { perror("posix_openpt"); exit(1); }
    return mfd;
}

static int prepare_slave(int mfd, char *slaveBuf, size_t bufLen)
{
    /* Step 2: fix slave permissions */
    if (grantpt(mfd) == -1)  { perror("grantpt");  return -1; }

    /* Step 3: unlock the slave */
    if (unlockpt(mfd) == -1) { perror("unlockpt"); return -1; }

    /* Step 4a: get slave name — copy immediately out of static buffer */
    if (ptsname_r(mfd, slaveBuf, bufLen) != 0) {
        perror("ptsname_r"); return -1;
    }
    return 0;
}

int main(void)
{
    char slaveName[64];
    int  mfd, sfd;
    char writeBuf[] = "Hello from master!\n";
    char readBuf[64];
    ssize_t n;

    /* Step 1 */
    mfd = open_master();
    printf("Master fd: %d\n", mfd);

    /* Steps 2, 3, 4a */
    if (prepare_slave(mfd, slaveName, sizeof(slaveName)) == -1)
        exit(1);
    printf("Slave device: %s\n", slaveName);

    /* Step 4b: open the slave */
    sfd = open(slaveName, O_RDWR | O_NOCTTY);
    if (sfd == -1) { perror("open slave"); close(mfd); exit(1); }
    printf("Slave fd: %d\n", sfd);

    /* Write from master -> read on slave */
    if (write(mfd, writeBuf, strlen(writeBuf)) == -1) {
        perror("write master"); goto cleanup;
    }

    memset(readBuf, 0, sizeof(readBuf));
    n = read(sfd, readBuf, sizeof(readBuf) - 1);
    if (n == -1) { perror("read slave"); goto cleanup; }
    printf("Slave read: %s", readBuf);

    /* Write from slave -> read on master */
    if (write(sfd, "Hello from slave!\n", 18) == -1) {
        perror("write slave"); goto cleanup;
    }
    memset(readBuf, 0, sizeof(readBuf));
    n = read(mfd, readBuf, sizeof(readBuf) - 1);
    if (n == -1) { perror("read master"); goto cleanup; }
    printf("Master read: %s", readBuf);

cleanup:
    close(sfd);
    close(mfd);
    return 0;
}
Compile and run:

gcc -o pty_full pty_full.c
./pty_full
# Master fd: 3
# Slave device: /dev/pts/8
# Slave fd: 4
# Slave read: Hello from master!
# Master read: Hello from slave!

Note: Due to the terminal line discipline on the PTY, writing from the slave back to the master will echo back. In practice you would use tcsetattr() to configure the PTY (e.g., disable echo with ECHO flag) before using it.

Summary: grantpt / unlockpt / ptsname Side by Side
Function Header Return Needed on Linux? Purpose
grantpt(mfd) <stdlib.h> 0 / -1 No (call anyway) Fix slave ownership & permissions
unlockpt(mfd) <stdlib.h> 0 / -1 Yes — always Unlock slave so it can be opened
ptsname(mfd) <stdlib.h> char * / NULL Yes — always Return path like /dev/pts/N

What the Slave Device Looks Like After grantpt()

$ ls -l /dev/pts/7
crw--w---- 1 ravi tty 136, 7 Jun 18 10:30 /dev/pts/7
#  ^           ^    ^
#  |           |    +-- Group: tty
#  |           +------- Owner: ravi (the user who called posix_openpt)
#  +------------------- Character device, owner=rw, group=w, other=none

Common Mistakes and How to Avoid Them
Mistake Result Fix
Skip unlockpt() before opening slave open(slave) fails with EIO Always call unlockpt() first
Use ptsname() result after another ptsname() call Pointer now points to wrong data (static buffer overwritten) strdup(ptsname(mfd)) or use ptsname_r()
ptsname() in multi-threaded code Race condition — static buffer shared between threads Use ptsname_r() with per-thread buffer
SIGCHLD handler installed when calling grantpt() Undefined behavior on non-Linux (handler may reap pt_chown) Block SIGCHLD around grantpt() on non-Linux systems
Forgetting to call grantpt() at all Works on Linux but breaks portability Always call it; the call is a no-op on Linux and costs nothing

Key Terms

grantpt() unlockpt() ptsname() ptsname_r() pt_chown set-user-ID-root tty group EIO error static buffer SIGCHLD crw–w—- slave lock /dev/pts/N

Interview Questions & Answers
Q1. What does grantpt() do? Is it necessary on Linux?

grantpt() changes the slave device’s owner to the caller’s effective UID, changes its group to tty, and sets permissions to crw--w----. On Linux this is done automatically by the kernel when the slave is created, so grantpt() is a no-op. However, it must still be called for portability — on other UNIX systems it is required and uses a set-UID-root helper called pt_chown to perform the changes.

Q2. What error do you get if you open() the PTY slave before calling unlockpt()?

The open() call fails with errno set to EIO. The slave device has an internal kernel lock that is removed only by unlockpt(). The purpose of this lock is to prevent a race condition where another process opens the slave before the master process has finished initializing it (e.g., before grantpt() completes).

Q3. Why is the return value of ptsname() dangerous to store for later use?

ptsname() returns a pointer to a statically allocated internal buffer. Any subsequent call to ptsname() (even from another thread) overwrites that buffer. If you store the pointer and use it later, you may read garbage. The fix is to immediately copy the result using strdup() or snprintf() into your own buffer, or to use the thread-safe ptsname_r() extension.

Q4. Why does grantpt() set the slave group to “tty” and enable group write permission?

The wall(1) and write(1) utilities (used to broadcast messages to terminals) are set-group-ID programs owned by the tty group. To write to any terminal or PTY slave, they need write access. Group write permission (crw--w----) gives them exactly that access without opening the device to all users.

Q5. Why is it dangerous to have a SIGCHLD handler installed when calling grantpt() on non-Linux systems?

On non-Linux systems, grantpt() forks a child process that executes pt_chown. If the calling program has a SIGCHLD handler, that handler may call waitpid() and accidentally reap the pt_chown child before grantpt() can collect its exit status. This makes the behavior of grantpt() undefined. SUSv3 explicitly warns about this. The solution is to block or temporarily reset SIGCHLD around the call.

Q6. What format does ptsname() return on Linux? Give an example.

It returns a path of the form /dev/pts/N where N is a non-negative integer uniquely identifying the slave on the system. For example: /dev/pts/0, /dev/pts/7, /dev/pts/23. These entries live in the devpts virtual filesystem and are dynamically created when the master is opened and removed when the master is closed.

Q7. Describe the complete standard sequence to set up a PTY pair in C, including headers and error checking.

See the complete code example above. The sequence is: (1) posix_openpt(O_RDWR | O_NOCTTY) — needs <stdlib.h> and <fcntl.h>; (2) grantpt(mfd) — needs <stdlib.h>; (3) unlockpt(mfd) — needs <stdlib.h>; (4) copy ptsname(mfd) result, then open(slaveName, O_RDWR | O_NOCTTY). Check the return value of every call and handle errors with perror() and exit().

Q8. What is pt_chown and when is it used?

pt_chown is a small set-user-ID-root helper program used by grantpt() on non-Linux UNIX systems. Because changing device file ownership requires root privilege, and grantpt() runs as a normal user, it forks a child and exec’s pt_chown which has the necessary elevated privileges. On Linux this is not needed because the kernel sets correct slave ownership automatically.

Chapter 64 Complete

You have covered all four UNIX 98 PTY setup functions and their internals.

← Part 1: PTY Intro & Applications ← Part 2: posix_openpt() & PTY Limits

Leave a Reply

Your email address will not be published. Required fields are marked *