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().
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.
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.grantpt() is a no-op on Linux but must still be called for portable code that works on other UNIX systems.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 |
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.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.
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()orsnprintf()before callingptsname()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");
}
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;
}
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.
| 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
| 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() 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.
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).
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.
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.
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.
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.
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().
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
