Bash file descriptors vs Linux file descriptors - linux

I'm just trying to reconcile these two seemingly similar concepts.
In Bash, one is allowed to make arbitrary redirections, and importantly, using one's chosen file descriptor number. However in Linux, the value returned by an open call (AFAIK) cannot be chosen by the calling process.
Thus, are Bash fd numbers the same as the fd numbers returned by system calls? If not, what's the difference?

Here's a little experiment that might shed some light on what's going on when you open a file descriptor in bash with a number of your choosing:
> cat test.txt
foobar!
> cat test.sh
#!/bin/bash
exec 17<test.txt
read -u 17 line
echo "$line"
exec 17>&-
> strace ./test.sh
//// A bunch of stuff omitted so we can skip to the interesting part...
open("test.txt", O_RDONLY) = 3
fcntl(17, F_GETFD) = -1 EBADF (Bad file descriptor)
dup2(3, 17) = 17
close(3) = 0
fcntl(17, F_GETFD) = 0
ioctl(17, TCGETS, 0x7ffc56f093f0) = -1 ENOTTY (Inappropriate ioctl for device)
lseek(17, 0, SEEK_CUR) = 0
read(17, "foobar!\n", 128) = 8
write(1, "foobar!\n", 8foobar!) = 8
fcntl(17, F_GETFD) = 0
fcntl(17, F_DUPFD, 10) = 10
fcntl(17, F_GETFD) = 0
fcntl(10, F_SETFD, FD_CLOEXEC) = 0
close(17) = 0
The part that answers your question is where it calls open() on test.txt, which returns a value of 3. This is what you would most likely get in a C program if you did the same, because file descriptors 0, 1, and 2 (i.e., stdin, stdout, and stderr) are all you have open initially. The number 3 is just the next available file descriptor.
And we see that in the strace output of the bash script as well. What bash does differently is that it then calls fcntl(17, F_GETFD) to check if file descriptor 17 is already open (because it wants to use that fd for test.txt). Then, when fcntl returns EBADF indicating no such fd is open, bash knows it is free to use it. So then it calls dup2(3, 17) to make fd 17 a copy of fd 3. Finally, it calls close() on fd 3 to free it up again, leaving fd 17 (and only fd 17) as an open file descriptor for test.txt.
So the answer to your question is that bash file descriptors are not special creatures set apart from the "normal" file descriptors that everyone else uses. They are in fact just the same thing. You could easily use the same trick in your C program to open files with file descriptor numbers of your choosing.
Also, it's worth pointing out that bash doesn't really get to choose its own file descriptor when it calls open(). It has to make do with whatever open() returns, like everyone else. All that's really going on in your bash script is some smoke and mirrors (via dup2()) to make it seem as if you get to choose your own file descriptor.

Related

crontab with ed by commands on stream, results in "no modification made"

I am trying to append a line to my crontab file. I know there are other ways to work around this problem, but still want to know what caused it. The command is run on raspberry pi 3 B+, raspbian lite is installed, with GNU ed 1.15, cron 3.0pl1-134+deb10u1.
The command that I'm stuck on is this:
$ echo -e 'a\n#asdf\n.\nwQ' | EDITOR=ed crontab -e
902
909
No modification made
I'm expecting it to add line #asdf at the end of my crontab file, but it doesn't.
Setting EDITOR='tee -a' as suggested on https://stackoverflow.com/a/30123606/8842387 does not solve the problem. So I guess it is the problem with cron.
Strangely enough, when I give ed commands from the keyboard directly, rather than streaming it, it just works. Maybe subshell creation caused the problem?
Here I'm attaching a few of the last lines from strace result.
$ echo -e 'a\n#asdf\n.\nwQ' | EDITOR=ed strace crontab -e
execve("/usr/bin/crontab", ["crontab", "-e"], 0x7ee54c14 /* 29 vars */) = 0
access("/etc/suid-debug", F_OK) = -1 ENOENT (No such file or directory)
...
read(3, "TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\7\0\0\0\7\0\0\0\0"..., 4096) = 659
_llseek(3, -393, [266], SEEK_CUR) = 0
read(3, "TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\7\0\0\0\7\0\0\0\0"..., 4096) = 393
close(3) = 0
getpid() = 18579
socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 3
connect(3, {sa_family=AF_UNIX, sun_path="/dev/log"}, 110) = 0
send(3, "<78>Nov 20 15:31:25 crontab[1857"..., 56, MSG_NOSIGNAL) = 56
openat(AT_FDCWD, "crontabs/pi", O_RDONLY) = -1 EACCES (Permission denied)
openat(AT_FDCWD, "/usr/share/locale/locale.alias", O_RDONLY|O_CLOEXEC) = 4
fstat64(4, {st_mode=S_IFREG|0644, st_size=2995, ...}) = 0
read(4, "# Locale name alias data base.\n#"..., 4096) = 2995
read(4, "", 4096) = 0
close(4) = 0
openat(AT_FDCWD, "/usr/share/locale/en_GB.UTF-8/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/share/locale/en_GB.utf8/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/share/locale/en_GB/LC_MESSAGES/libc.mo", O_RDONLY) = 4
fstat64(4, {st_mode=S_IFREG|0644, st_size=1433, ...}) = 0
mmap2(NULL, 1433, PROT_READ, MAP_PRIVATE, 4, 0) = 0x76f50000
close(4) = 0
openat(AT_FDCWD, "/usr/share/locale/en.UTF-8/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/share/locale/en.utf8/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/share/locale/en/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
write(2, "crontabs/pi/: fdopen: Permission"..., 39crontabs/pi/: fdopen: Permission denied) = 39
exit_group(1) = ?
+++ exited with 1 +++
openat(AT_FDCWD, "crontabs/pi", O_RDONLY) = -1 EACCES (Permission denied) looks a bit suspicious, but not sure why it opens the file read-only.
EDIT:
As suggested by #tink, I ran EDITOR=ed strace crontab -e to see what strace gives on an interactive session. The result was almost same (only varying on pid and fd numbers).
I noticed that running echo "..." | EDITOR=ed crontab -e exited with message No modification made but with strace the process halts without any messages. (EDITOR=ed strace crontab -e 2>&1 | grep "No mod" prints nothing). Guess the strace triggers different errors.
Following up on my VISUAL comment, these worked for me:
( unset VISUAL; printf '%s\n' a '#abcd' . wq | EDITOR=ed crontab -e )
printf '%s\n' a '#abcd' . wq | VISUAL=ed crontab -e
In my environment, both VISUAL and EDITOR are set to "vim"
Or, more roundabout, but don't need to monkey with env vars. This one also allows you to do it silently:
crontab <(printf '%s\n' a '#asdf' . '%p' | ed -s <(crontab -l))
I was doing the above on a Mac. On Linux, I can reproduce your observations, but can't explain them.
A small tweak to the last command works:
printf '%s\n' a '#asdf' . '%p' Q | ed -s <(crontab -l) | crontab -
TLDR; (sleep 1; echo -e 'a\n#asdf\n.\nwQ') | EDITOR=ed crontab -e works!
The problem was on crontab.
When I invoke crontab -e it creates a temporary copy of the user cron table in /tmp directory.
Then opens the temporary file with an editor specified by $EDITOR.
After the editing is done, crontab check if the file modification date have changed since its creation.
This is implemented in the patch that enables editing cron table via temporary file.
In my case, ed getting its command from stdin finished the editing too fast so that even a single digit of the modification timestamp of the temporary file had not been changed.
As crontab considered no human can make edition that fast, it assumes no modification made and discards it.
To bypass this behavior, I added sleep 1 before the release of the command.
This will hold ed to wait for its command from stdin after crontab created tempfile, which effectively lets the modification timestamp different.

How to add new file descriptors under /dev/fd?

I am using a remote linux server via ssh, so I don't have the super user authority. However, the mounted file descriptors in /dev/fd is not enough:
user >ls /dev/fd/
0 1 2
or:
user >ls /proc/self/fd
0 1 2
And what I want to is add new file descriptors, so that I can redirect the output stream in this way:
user >./main.exe 1>1.txt 2>2.txt 3>3.txt ...
Since the file descriptor is not enough, I can't create a file descriptor such as /dev/fd/3, an error triggered:
IOError: [Errno 2] No such file or directory: '/dev/fd/3'
/dev/fd is not a real directory. You don't add files to it, it just shows which fds the process (ls in your case) has open.
To open new FDs from the shell, you can just run
./yourprogram 3>myfile
If the program writes to FD 3, the output will end up in myfile.
Here's an example:
$ cat foo.c
#include <unistd.h>
void main() {
write(3, "hello world\n", 12);
}
$ gcc foo.c -o foo
$ ./foo 3> myfile
$ cat myfile
hello world

Pipeline management in linux shell

i'm currently looking how pipelining is managed into shells.
for example, in my shell, if i enter "ls | wc | less". The result of this operation will be the creation of three process, ls wc and less.
Ouput of ls will be piped to the enter input of wc, and the ouput of wc will be piped to the enter intput of less.
For me, it means that during the execution of "ls | wc | less". The standard input of less will not be the keyboard, but the ouput of wc. But, less will still be responsive to my keyboard. Why ? I don't understand, because for me, less should not be sensitive to the keyboard since it have been piped.
Do somebody have an idea ?
Thanks
The code from less
#if HAVE_DUP
/*
* Force standard input to be the user's terminal
* (the normal standard input), even if less's standard input
* is coming from a pipe.
*/
inp = dup(0);
close(0);
#if OS2
/* The __open() system call translates "/dev/tty" to "con". */
if (__open("/dev/tty", OPEN_READ) < 0)
#else
if (open("/dev/tty", OPEN_READ) < 0)
#endif
dup(inp);
#endif
It opens a direct stream from /dev/tty as well as whatever your stdin is.
Just a guess - less is opening /dev/console for the interactive session, I used that trick once. I was wrong - strace is your friend :-):
echo | strace less
) = 16
read(0, "\n", 8192) = 1
write(1, "\n", 1
) = 1
read(0, "", 8191) = 0
write(1, "\33[7m(END)\33[27m\33[K", 17(END)) = 17
read(3,
As you can see, less is reading from FD 3.
/* Standard file descriptors. */
#define STDIN_FILENO 0 /* Standard input. */
#define STDOUT_FILENO 1 /* Standard output. */
#define STDERR_FILENO 2 /* Standard error output. */
And a closer look (after 'q') shows:
open("/dev/tty", O_RDONLY) = 3
Which confirms #123's source code inspection - it opens /dev/tty.

What does shell do when we redirect using "<"?

Say I have a program called fstatcheck. It takes one argument from the command line and treat it as file descriptor. It checks the stat information of the file pointed by the file descriptor.
For example:
$./fstatcheck 1
l = 1
type: other, read: yes
Another example:
$./fstatcheck 3 < foobar.txt
l = 3
Fstat error: Bad file descriptor
Questions:
What is the shell doing in the second example?
I can guess that it takes 3 as a file descriptor and starts to analyze the stat, but descriptor 3 is not open. But how does shell treat the redirection?
I assume the shell performs the following algorithm:
if (fork() == 0) {
// What does the shell do here?
execve("fstatcheck", argv, envp);
}
Is there any way I can create a file descriptor 3 and let it connect to an open file table which points to foobar.txt file stat by just using the shell command (instead of using C code)?
Let's find out with strace:
$ strace sh -c 'cat < /dev/null'
[...]
open("/dev/null", O_RDONLY) = 3
fcntl(0, F_DUPFD, 10) = 10
close(0) = 0
fcntl(10, F_SETFD, FD_CLOEXEC) = 0
dup2(3, 0) = 0
close(3) = 0
[...]
execve("/bin/cat", ["cat"], [/* 28 vars */]) = 0
[...]
So in your code, the relevant parts would be:
if (fork() == 0) {
int fd = open(filename, O_RDONLY); // Open the file
close(0); // Close old stdin
dup2(fd, 0); // Copy fd as new stdin
close(fd); // Close the original fd
execve("fstatcheck", argv, envp); // Execute
}
As for opening another fd, absolutely:
myprogram 3< file
This will open file for reading on fd 3 for the program. < alone is a synonym for 0<.

Linux proc/pid/fd for stdout is 11?

Executing a script with stdout redirected to a file. So /proc/$$/fd/1 should point to that file (since stdout fileno is 1). However, actual fd of the file is 11. Please, explain, why.
Here is session:
$ cat hello.sh
#!/bin/sh -e
ls -l /proc/$$/fd >&2
$ ./hello.sh > /tmp/1
total 0
lrwx------ 1 nga users 64 May 28 22:05 0 -> /dev/pts/0
lrwx------ 1 nga users 64 May 28 22:05 1 -> /dev/pts/0
lr-x------ 1 nga users 64 May 28 22:05 10 -> /home/me/hello.sh
l-wx------ 1 nga users 64 May 28 22:05 11 -> /tmp/1
lrwx------ 1 nga users 64 May 28 22:05 2 -> /dev/pts/0
I have a suspicion, but this is highly dependent on how your shell behaves. The file descriptors you see are:
0: standard input
1: standard output
2: standard error
10: the running script
11: a backup copy of the script's normal standard out
Descriptors 10 and 11 are close on exec, so won't be present in the ls process. 0-2 are, however, prepared for ls before forking. I see this behaviour in dash (Debian Almquist shell), but not in bash (Bourne again shell). Bash instead does the file descriptor manipulations after forking, and incidentally uses 255 rather than 10 for the script. Doing the change after forking means it won't have to restore the descriptors in the parent, so it doesn't have the spare copy to dup2 from.
The output of strace can be helpful here.
The relevant section is
fcntl64(1, F_DUPFD, 10) = 11
close(1) = 0
fcntl64(11, F_SETFD, FD_CLOEXEC) = 0
dup2(2, 1) = 1
stat64("/home/random/bin/ls", 0xbf94d5e0) = -1 ENOENT (No such file or
+++++++>directory)
stat64("/usr/local/bin/ls", 0xbf94d5e0) = -1 ENOENT (No such file or directory)
stat64("/usr/bin/ls", 0xbf94d5e0) = -1 ENOENT (No such file or directory)
stat64("/bin/ls", {st_mode=S_IFREG|0755, st_size=96400, ...}) = 0
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD,
+++++++>child_tidptr=0xb75a8938) = 22748
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 22748
--- SIGCHLD (Child exited) # 0 (0) ---
dup2(11, 1) = 1
So, the shell moves the existing stdout to an available file descriptor above 10 (namely, 11), then moves the existing stderr onto its own stdout (due to the >&2 redirect), then restores 11 to its own stdout when the ls command is finished.

Resources