[LTP] [PATCH] syscalls/ioctl02: Move TTY discovery from shell to C

Abdulla abdulla1@linux.ibm.com
Mon Apr 6 12:03:11 CEST 2026


The test previously required a shell wrapper to discover and validate
TTY devices. This modernization moves all TTY discovery logic into C,
eliminating the shell dependency.

Key changes:
- Added is_valid_tty_name() for pattern matching tty[0-9]*
- Added is_valid_tty_device() for ioctl-based validation
- Added find_valid_ttys() for /dev directory scanning
- Made device parameter optional with auto-discovery mode
- Extracted test_single_device() for code reusability

The test now supports two modes:
1. Single device: ./ioctl02 -d /dev/tty1 (backward compatible)
2. Auto-discovery: ./ioctl02 (discovers all valid TTY devices)

Signed-off-by: Shaik Abdulla <abdulla1@linux.ibm.com>
---
 .../syscalls/ioctl/ioctl02_modernized.c       | 431 ++++++++++++++++++
 1 file changed, 431 insertions(+)
 create mode 100644 testcases/kernel/syscalls/ioctl/ioctl02_modernized.c

diff --git a/testcases/kernel/syscalls/ioctl/ioctl02_modernized.c b/testcases/kernel/syscalls/ioctl/ioctl02_modernized.c
new file mode 100644
index 000000000..44b43db19
--- /dev/null
+++ b/testcases/kernel/syscalls/ioctl/ioctl02_modernized.c
@@ -0,0 +1,431 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) International Business Machines  Corp., 2001
+ * Copyright (c) 2020 Petr Vorel <pvorel@suse.cz>
+ * Copyright (c) 2023 Marius Kittler <mkittler@suse.de>
+ * Copyright (c) 2026 Modernization - TTY discovery moved from shell to C
+ */
+
+/*\
+ * Test TCGETA/TCGETS and TCSETA/TCSETS ioctl implementations for tty driver.
+ *
+ * This test can operate in two modes:
+ * 1. Single device mode: Use -d option to test a specific TTY device
+ * 2. Auto-discovery mode: Automatically discovers and tests all valid /dev/tty[0-9]* devices
+ *
+ * In this test, the parent and child open the parentty and the childtty
+ * respectively.  After opening the childtty the child flushes the stream
+ * and wakes the parent (thereby asking it to continue its testing). The
+ * parent, then starts the testing. It issues a TCGETA/TCGETS ioctl to
+ * get all the tty parameters. It then changes them to known values by
+ * issuing a TCSETA/TCSETS ioctl. Then the parent issues a TCSETA/TCGETS
+ * ioctl again and compares the received values with what it had set
+ * earlier. The test fails if TCGETA/TCGETS or TCSETA/TCSETS fails, or if
+ * the received values don't match those that were set. The parent does
+ * all the testing, the requirement of the child process is to moniter
+ * the testing done by the parent, and hence the child just waits for the
+ * parent.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <dirent.h>
+#include <sys/stat.h>
+#include <asm/termbits.h>
+
+#include "lapi/ioctl.h"
+#include "tst_test.h"
+
+#define MAX_TTYS 100
+#define TTY_PATH_MAX 256
+
+static struct termio termio, termio_exp;
+static struct termios termios, termios_exp, termios_bak;
+
+static char *parenttty, *childtty;
+static int parentfd = -1;
+static int parentpid, childpid;
+
+/* TTY device list for auto-discovery mode */
+static char *tty_list[MAX_TTYS];
+static int tty_count = 0;
+
+static void do_child(void);
+static void prepare_termio(void);
+static void run_ptest(void);
+static void chk_tty_parms_termio(void);
+static void chk_tty_parms_termios(void);
+static void setup(void);
+static void cleanup(void);
+static void test_single_device(const char *dev);
+static void find_valid_ttys(void);
+static void free_tty_list(void);
+static int is_valid_tty_name(const char *name);
+static int is_valid_tty_device(const char *path);
+
+static char *device;
+
+static struct variant {
+	const char *name;
+	void *termio, *termio_exp, *termio_bak;
+	size_t termio_size;
+	void (*check)(void);
+	int tcget, tcset;
+} variants[] = {
+	{
+		.name = "termio",
+		.termio = &termio,
+		.termio_exp = &termio_exp,
+		.termio_size = sizeof(termio),
+		.check = &chk_tty_parms_termio,
+		.tcget = TCGETA,
+		.tcset = TCSETA,
+	},
+	{
+		.name = "termios",
+		.termio = &termios,
+		.termio_exp = &termios_exp,
+		.termio_size = sizeof(termios),
+		.check = &chk_tty_parms_termios,
+		.tcget = TCGETS,
+		.tcset = TCSETS,
+	},
+};
+
+/*
+ * is_valid_tty_name() - Check if device name matches tty[0-9] or tty[0-9][0-9]
+ */
+static int is_valid_tty_name(const char *name)
+{
+	const char *device_no;
+
+	/* Must start with "tty" */
+	if (strncmp(name, "tty", 3) != 0)
+		return 0;
+
+	device_no = name + 3;
+
+	/* Must have at least one digit */
+	if (!isdigit(device_no[0]))
+		return 0;
+
+	/* If there's a second character, it must be a digit */
+	if (device_no[1] != '\0' && !isdigit(device_no[1]))
+		return 0;
+
+	/* Must not have more than 2 digits */
+	if (device_no[1] != '\0' && device_no[2] != '\0')
+		return 0;
+
+	return 1;
+}
+
+/*
+ * is_valid_tty_device() - Validate TTY device by attempting ioctl
+ * This replaces the shell's "stty -F" validation
+ */
+static int is_valid_tty_device(const char *path)
+{
+	int fd;
+	struct termios t;
+	int ret;
+
+	/* Try to open device with non-blocking flag */
+	fd = open(path, O_RDWR | O_NONBLOCK);
+	if (fd < 0)
+		return 0;
+
+	/* Try to get terminal attributes - this validates it's a real TTY */
+	ret = ioctl(fd, TCGETS, &t);
+	close(fd);
+
+	return (ret == 0);
+}
+
+/*
+ * find_valid_ttys() - Discover all valid TTY devices in /dev
+ * This replaces the shell wrapper's "for tttype in `ls /dev/tty*`" logic
+ */
+static void find_valid_ttys(void)
+{
+	DIR *dir;
+	struct dirent *entry;
+	char tty_path[TTY_PATH_MAX];
+
+	dir = opendir("/dev");
+	if (!dir)
+		tst_brk(TBROK | TERRNO, "Cannot open /dev directory");
+
+	tst_res(TINFO, "Scanning /dev for valid TTY devices...");
+
+	while ((entry = readdir(dir)) != NULL) {
+		/* Check if name matches tty[0-9] or tty[0-9][0-9] pattern */
+		if (!is_valid_tty_name(entry->d_name))
+			continue;
+
+		/* Build full path */
+		snprintf(tty_path, sizeof(tty_path), "/dev/%s", entry->d_name);
+
+		/* Validate it's a working TTY device */
+		if (!is_valid_tty_device(tty_path)) {
+			tst_res(TINFO, "Skipping %s (not a valid TTY)", tty_path);
+			continue;
+		}
+
+		/* Add to list if we have space */
+		if (tty_count >= MAX_TTYS) {
+			tst_res(TWARN, "Too many TTY devices found, skipping %s", tty_path);
+			continue;
+		}
+
+		tty_list[tty_count] = strdup(tty_path);
+		if (!tty_list[tty_count])
+			tst_brk(TBROK | TERRNO, "Failed to allocate memory for TTY path");
+
+		tst_res(TINFO, "Found valid TTY device: %s", tty_path);
+		tty_count++;
+	}
+
+	closedir(dir);
+
+	if (tty_count == 0)
+		tst_brk(TBROK, "No valid TTY devices found in /dev");
+
+	tst_res(TINFO, "Total valid TTY devices found: %d", tty_count);
+}
+
+/*
+ * free_tty_list() - Free allocated TTY device paths
+ */
+static void free_tty_list(void)
+{
+	int i;
+
+	for (i = 0; i < tty_count; i++) {
+		if (tty_list[i]) {
+			free(tty_list[i]);
+			tty_list[i] = NULL;
+		}
+	}
+	tty_count = 0;
+}
+
+/*
+ * test_single_device() - Test a single TTY device
+ * Extracted from verify_ioctl() to support both single and multi-device modes
+ */
+static void test_single_device(const char *dev)
+{
+	parenttty = childtty = (char *)dev;
+
+	parentpid = getpid();
+	childpid = SAFE_FORK();
+	if (!childpid) {
+		do_child();
+		exit(EXIT_SUCCESS);
+	}
+
+	TST_CHECKPOINT_WAIT(0);
+
+	parentfd = SAFE_OPEN(parenttty, O_RDWR, 0777);
+	SAFE_IOCTL(parentfd, TCFLSH, TCIOFLUSH);
+
+	run_ptest();
+
+	TST_CHECKPOINT_WAKE(0);
+	SAFE_CLOSE(parentfd);
+	parentfd = -1;
+}
+
+static void verify_ioctl(void)
+{
+	int i;
+
+	tst_res(TINFO, "Testing %s variant", variants[tst_variant].name);
+
+	if (device) {
+		/* Single device mode - test specified device only */
+		tst_res(TINFO, "Single device mode: testing %s", device);
+		test_single_device(device);
+	} else {
+		/* Auto-discovery mode - find and test all valid TTYs */
+		tst_res(TINFO, "Auto-discovery mode: scanning for TTY devices");
+		find_valid_ttys();
+
+		for (i = 0; i < tty_count; i++) {
+			tst_res(TINFO, "Testing device %d/%d: %s",
+				i + 1, tty_count, tty_list[i]);
+			test_single_device(tty_list[i]);
+		}
+
+		free_tty_list();
+	}
+}
+
+static void prepare_termio(void)
+{
+	/* Use "old" line discipline */
+	termios_exp.c_line = termio_exp.c_line = 0;
+
+	/* Set control modes */
+	termios_exp.c_cflag = termio_exp.c_cflag = B50 | CS7 | CREAD | PARENB | PARODD | CLOCAL;
+
+	/* Set control chars. */
+	for (int i = 0; i < NCC; i++)
+		termio_exp.c_cc[i] = CSTART;
+	for (int i = 0; i < VEOL2; i++)
+		termios_exp.c_cc[i] = CSTART;
+
+	/* Set local modes. */
+	termios_exp.c_lflag = termio_exp.c_lflag =
+	    ((unsigned short)(ISIG | ICANON | XCASE | ECHO | ECHOE | NOFLSH));
+
+	/* Set input modes. */
+	termios_exp.c_iflag = termio_exp.c_iflag =
+	    BRKINT | IGNPAR | INPCK | ISTRIP | ICRNL | IUCLC | IXON | IXANY |
+	    IXOFF;
+
+	/* Set output modes. */
+	termios_exp.c_oflag = termio_exp.c_oflag = OPOST | OLCUC | ONLCR | ONOCR;
+}
+
+/*
+ * run_ptest() - setup the various termio/termios structure values and issue
+ * the TCSETA/TCSETS ioctl call with the TEST macro.
+ */
+static void run_ptest(void)
+{
+	struct variant *v = &variants[tst_variant];
+
+	/* Init termio/termios structures used to check if all params got set */
+	memset(v->termio, 0, v->termio_size);
+
+	SAFE_IOCTL(parentfd, v->tcset, v->termio_exp);
+
+	/* Get termio and see if all parameters actually got set */
+	SAFE_IOCTL(parentfd, v->tcget, v->termio);
+	v->check();
+}
+
+static int cmp_attr(unsigned long long exp, unsigned long long act, const char *attr)
+{
+	if (act == exp)
+		return 0;
+	tst_res(TFAIL, "%s has incorrect value %llu, expected %llu",
+		attr, act, exp);
+	return 1;
+}
+
+static int cmp_c_cc(unsigned char *exp_c_cc, unsigned char *act_c_cc, int ncc)
+{
+	int i, fails = 0;
+	char what[32];
+
+	for (i = 0; i < ncc; ++i) {
+		sprintf(what, "control char %d", i);
+		fails += cmp_attr(exp_c_cc[i], act_c_cc[i], what);
+	}
+	return fails;
+}
+
+#define CMP_ATTR(term_exp, term, attr, flag)				\
+({															\
+	flag += cmp_attr((term_exp).attr, (term).attr, #attr);	\
+	flag;                                                   \
+})
+
+#define CMP_C_CC(term_exp, term, flag)								\
+({																	\
+	flag += cmp_c_cc(term_exp.c_cc, term.c_cc, sizeof(term.c_cc));	\
+	flag;															\
+})
+
+static void chk_tty_parms_termio(void)
+{
+	int flag = 0;
+
+	flag = CMP_ATTR(termio_exp, termio, c_line, flag);
+	flag = CMP_C_CC(termio_exp, termio, flag);
+	flag = CMP_ATTR(termio_exp, termio, c_lflag, flag);
+	flag = CMP_ATTR(termio_exp, termio, c_iflag, flag);
+	flag = CMP_ATTR(termio_exp, termio, c_oflag, flag);
+
+	if (!flag)
+		tst_res(TPASS, "TCGETA/TCSETA tests");
+}
+
+static void chk_tty_parms_termios(void)
+{
+	int flag = 0;
+
+	flag = CMP_ATTR(termios_exp, termios, c_line, flag);
+	flag = CMP_C_CC(termios_exp, termios, flag);
+	flag = CMP_ATTR(termios_exp, termios, c_lflag, flag);
+	flag = CMP_ATTR(termios_exp, termios, c_iflag, flag);
+	flag = CMP_ATTR(termios_exp, termios, c_oflag, flag);
+
+	if (!flag)
+		tst_res(TPASS, "TCGETS/TCSETS tests");
+}
+
+static void do_child(void)
+{
+	int cfd = SAFE_OPEN(childtty, O_RDWR, 0777);
+
+	SAFE_IOCTL(cfd, TCFLSH, TCIOFLUSH);
+
+	/* tell the parent that we're done */
+	TST_CHECKPOINT_WAKE(0);
+
+	TST_CHECKPOINT_WAIT(0);
+	tst_res(TINFO, "child: parent has finished testing");
+	SAFE_CLOSE(cfd);
+}
+
+static void setup(void)
+{
+	int fd;
+
+	if (device) {
+		/* Single device mode - validate the specified device */
+		tst_res(TINFO, "Using specified device: %s", device);
+
+		fd = SAFE_OPEN(device, O_RDWR, 0777);
+		SAFE_IOCTL(fd, TCGETS, &termios_bak);
+		SAFE_CLOSE(fd);
+	} else {
+		/* Auto-discovery mode - will find devices during test */
+		tst_res(TINFO, "Auto-discovery mode enabled");
+	}
+
+	prepare_termio();
+}
+
+static void cleanup(void)
+{
+	if (parentfd >= 0) {
+		SAFE_IOCTL(parentfd, TCSETS, &termios_bak);
+		SAFE_CLOSE(parentfd);
+	}
+
+	/* Free any allocated TTY list */
+	free_tty_list();
+}
+
+static struct tst_test test = {
+	.timeout = 20,
+	.needs_root = 1,
+	.needs_checkpoints = 1,
+	.forks_child = 1,
+	.setup = setup,
+	.cleanup = cleanup,
+	.test_all = verify_ioctl,
+	.test_variants = 2,
+	.options = (struct tst_option[]) {
+		{"d:", &device, "TTY device to test (optional). If not specified, auto-discovers all /dev/tty[0-9]* devices. Example: /dev/tty1"},
+		{}
+	}
+};
+
-- 
2.39.5 (Apple Git-154)



More information about the ltp mailing list