Skip to content

Commit d2a0f1d

Browse files
committed
Fix various problems in close(2) usage and pwdfd handling
Changelog for this commit: include/ast.h: - Added ast_close, which endeavors to close fds while handling EINTR correctly, depending on platform specific behavior. The following three methods are implemented: 1) Guarantee a close with posix_close(3p), which is the best available method. This function is still *very* new because it's a recent addition to POSIX. 2) Add a version that only calls close once, then copes with EINTR by treating it as a non-error. Only Linux, FreeBSD and Cygwin use this right now. (Other OSes probably provide functionally equivalent leak-proof forms of close, but the documentation available for those is too vague on this matter. Perhaps I'll have to dive into the code for the open source ones.) 3) Use the tried and true while loop to repeatedly invoke close, which is currently the generic fallback. On Linux, avoiding this method is effectively a bugfix, in large part because Linux can reuse a file descriptor after it's first closed before a second close call occurs, creating the potential for dangerous race conditions. For more information, please see the relevant LWN article et seq: https://lwn.net/Articles/576478/ - Delete many <errno.h> include directives in favor of a single one included in ast.h (ast_close uses errno and is implemented as a macro in the ast.h header). src/subshell.c: - Close all of the previous subshell pwdfds after a fork to fix a file descriptor leak (first introduced in 93u+ 2012-07-27). Reproducer: $ cat /tmp/reproducer (cd /bin; (cd /usr; (cd /tmp; (cd / echo 'ls /proc/${.sh.pid}/fd; :' >/tmp/a echo '#/bin/sh' >/tmp/b cat /tmp/a >>/tmp/b chmod +x /tmp/{a,b} /tmp/a # SHOPT_SPAWN=0 /tmp/b $ SHOPT_SPAWN=0 (ulimit -t unlimited; source /tmp/a) )))) exit 0 # Avoid optimizing away subshells - Initialize sp->pwdfd to -1 in all cases (this avoids the potential for incorrectly closing stdin). - While we're at it, switch to using C99 style struct initialization. bltins/cd_pwd.c, sh/io.c: - sh_close(): Set errno to zero to avoid passing on an inherited errno. The close(2) call might fail and subsequently set errno, which makes the previous behavior bogus. - b_cd(): If the sh_close() calls in cd fail with anything other than EINTR, the errno from fchdir(2) is thrown away and replaced with close's errno. This can cause two regression test failures in builtins.sh. Prevent this by saving the errno and restoring it at the relevant sh_close calls. Error output (snipped to fit it into the commit log): builtins.sh[996]: FAIL: can cd into a directory without x permission bit (absolute path arg) (expected status 1 and match of ': cd: /tmp/ksh93.shtests 377539.24978/builtins.C/no_x_dir: \[?Permission denied]?$', got status 1 and '<SNIP>/builtins.sh[994]: cd: /tmp/ksh93.shtests.377539.24978/builtins. /no_x_dir: Success') builtins.sh[1001]: FAIL: can cd into a directory without x permission bit (relative path arg) (expected status 1 and match of ': cd: no_x_dir: \[?Permission denied]?$', got status 1 and '<SNIP>/tests/builtins.sh[999]: cd: no_x_dir: No such file or directory') This bug was noticed after sh_close was changed to reset errno before calling close. - sh_diropenat(): If openat() managed to obtain a file descriptor >10 on its first try and O_CLOEXEC is available, the fd wouldn't be registered with the shell because no sh_fcntl() call would be reached. Fix that by setting the file attributes in sh_diropenat() rather than sh_fcntl(). Consequently, we don't need to rely on sh_fcntl and thus we can use fcntl(2) directly. (cf. sh_open() in io.c) bltins/test.c: - test_stat(): If we need to stat e_dot (the current working directory) and sh.pwdfd is available, use fstat(2) because it will avoid unnecessarily reopening '.'. This change results in a small performance increase for cd(1) usage in subshells: $ cat bench/v-cdloop.ksh for ((i=0; i!=10000; i++)) do (cd /home; (cd /tmp; (cd /bin; (cd /usr; (cd /var))))) done; : $ shbench -b bench/v-cdloop.ksh -l 4 /tmp/ksh-{before,after} ------------------------------------------------------ name /tmp/ksh-before /tmp/ksh-after ------------------------------------------------------ v-cdloop.ksh 0.414 [0.412-0.416] 0.381 [0.341-0.398] ------------------------------------------------------ sh/fault.c: - sh_done(): Delete an unnecessary close call backported from ksh93v-. The operating system closes all fds for us via exit(3), so a manual close(2) call only serves to make cleanup slower. comp/arc4random.c: - _ast_getentropy(): Fix a file descriptor leak when read(2) fails. tm/tvtouch.c: - tvtouch(): Add an else branch to avoid closing the same file descriptor twice. tests/subshell.sh: - Add a regression test capable of detecting the subshell pwdfd leak going back as far as ksh93u+ 2012-07-27, where the leak is incipient. (cherry-picked from fix-fd-closing)
1 parent 368f3c3 commit d2a0f1d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+243
-154
lines changed

NEWS

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@ This documents significant changes in the dev branch of ksh 93u+m.
22
For full details, see the git log at: https://github.com/ksh93/ksh
33
Uppercase BUG_* IDs are shell bug IDs as used by the Modernish shell library.
44

5+
2025-07-10:
6+
7+
- Fixed a file descriptor leak introduced in 93u+ 2012-07-27 that occurred
8+
when forking nested subshells.
9+
10+
- Fixed a bug that could cause cd(1) to fail with the wrong error message
11+
if the underlying close(2) calls failed after cd failed to change the
12+
directory with fchdir(2).
13+
14+
- Fixed a file descriptor leak that could occur on systems lacking a
15+
useable implementation of arc4random(3).
16+
17+
2025-07-09:
18+
19+
- [v1.1] Backported 'builtin -p' and 'umask -p' from ksh93v-, which
20+
can be used to print reinputtable commands for recreating the builtin
21+
table state and umask.
22+
- [v1.1] Backported 'trap -l' from ksh93v- for listing signals.
23+
524
2025-07-03:
625

726
- Fixed a regression introduced on 02-02-2022 that could cause ksh to crash
@@ -21,13 +40,6 @@ Uppercase BUG_* IDs are shell bug IDs as used by the Modernish shell library.
2140

2241
- Ksh is now capable of allocating memory within a 64-bit address space.
2342

24-
2025-07-09:
25-
26-
- [v1.1] Backported 'builtin -p' and 'umask -p' from ksh93v-, which
27-
can be used to print reinputtable commands for recreating the builtin
28-
table state and umask.
29-
- [v1.1] Backported 'trap -l' from ksh93v- for listing signals.
30-
3143
2025-06-14:
3244

3345
- Fixed a bug occurring on systems with posix_spawn(3), spawnve(2), and

src/cmd/builtin/pty.c

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ mkpty(int* master, int* minion)
313313
return -1;
314314
if (grantpt(*master) || unlockpt(*master) || !(sname = ptsname(*master)) || (*minion = open(sname, O_RDWR|O_cloexec)) < 0)
315315
{
316-
close(*master);
316+
ast_close(*master);
317317
return -1;
318318
}
319319
#else
@@ -325,8 +325,8 @@ mkpty(int* master, int* minion)
325325
struct termios tst;
326326
if (tcgetattr(*minion, &tst) < 0 && (ioctl(*minion, I_PUSH, "ptem") < 0 || ioctl(*minion, I_PUSH, "ldterm") < 0))
327327
{
328-
close(*minion);
329-
close(*master);
328+
ast_close(*minion);
329+
ast_close(*master);
330330
return -1;
331331
}
332332
}
@@ -1122,7 +1122,7 @@ b_pty(int argc, char** argv, Shbltin_t* context)
11221122
error(ERROR_system(1), "unable run %s", argv[0]);
11231123
UNREACHABLE();
11241124
}
1125-
close(minion);
1125+
ast_close(minion);
11261126
if (messages)
11271127
{
11281128
drop = 1;
@@ -1137,10 +1137,10 @@ b_pty(int argc, char** argv, Shbltin_t* context)
11371137
error(ERROR_system(1), "%s: cannot redirect messages", messages);
11381138
UNREACHABLE();
11391139
}
1140-
close(2);
1140+
ast_close(2);
11411141
dup(fd);
11421142
if (drop)
1143-
close(fd);
1143+
ast_close(fd);
11441144
}
11451145
minion = (*fun)(mp, lp, delay, timeout);
11461146
master = procclose(proc);

src/cmd/ksh93/bltins/cd_pwd.c

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,9 @@ int sh_diropenat(int dir, const char *path)
6969
}
7070
if(fd < 10)
7171
{
72-
/* Duplicate the fd and register it with the shell */
73-
int shfd = sh_fcntl(fd, F_dupfd_cloexec, 10);
74-
close(fd);
72+
/* Duplicate the fd */
73+
int shfd = fcntl(fd, F_dupfd_cloexec, 10);
74+
ast_close(fd);
7575
if(shfd < 0)
7676
return shfd;
7777
if(F_dupfd_cloexec == F_DUPFD)
@@ -81,7 +81,8 @@ int sh_diropenat(int dir, const char *path)
8181
else if(O_cloexec == 0)
8282
needs_cloexec = 1;
8383
if(needs_cloexec)
84-
sh_fcntl(fd,F_SETFD,FD_CLOEXEC);
84+
fcntl(fd,F_SETFD,FD_CLOEXEC);
85+
sh.fdstatus[fd] = (IOREAD|IOCLEX);
8586
return fd;
8687
}
8788
#endif /* _lib_openat */
@@ -92,7 +93,7 @@ int b_cd(int argc, char *argv[],Shbltin_t *context)
9293
Pathcomp_t *cdpath = 0;
9394
const char *dp;
9495
int saverrno=0;
95-
int rval,pflag=0,eflag=0,ret=1;
96+
int rval,pflag=0,eflag=0,ret=1,saverr;
9697
char *oldpwd, *cp;
9798
Namval_t *opwdnod, *pwdnod;
9899
#if _lib_openat
@@ -240,7 +241,9 @@ int b_cd(int argc, char *argv[],Shbltin_t *context)
240241
sh_pwdupdate(newdirfd);
241242
goto success;
242243
}
244+
saverr = errno;
243245
sh_close(newdirfd);
246+
errno = saverr;
244247
}
245248
#if !O_SEARCH
246249
else if((rval=chdir(cp)) >= 0)
@@ -268,7 +271,9 @@ int b_cd(int argc, char *argv[],Shbltin_t *context)
268271
sh_pwdupdate(newdirfd);
269272
goto success;
270273
}
274+
saverr = errno;
271275
sh_close(newdirfd);
276+
errno = saverr;
272277
}
273278
#if !O_SEARCH
274279
else if((rval=chdir(dir)) >= 0)

src/cmd/ksh93/bltins/mkservice.c

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ static void process_stream(Sfio_t* iop)
204204
r = (*sp->actionf)(sp, fd, 0);
205205
service_list[fd] = sp;
206206
if(r<0)
207-
close(fd);
207+
ast_close(fd);
208208
}
209209
}
210210

@@ -284,7 +284,7 @@ static int Accept(Service_t *sp, int accept_fd)
284284
fd = fcntl(accept_fd, F_DUPFD, 10);
285285
if (fd >= 0)
286286
{
287-
close(accept_fd);
287+
ast_close(accept_fd);
288288
if (nq)
289289
{
290290
char* av[3];
@@ -295,7 +295,7 @@ static int Accept(Service_t *sp, int accept_fd)
295295
sfsprintf(buff, sizeof(buff), "%d", fd);
296296
if (sh_fun(nq, sp->node, av))
297297
{
298-
close(fd);
298+
ast_close(fd);
299299
return -1;
300300
}
301301
}
@@ -382,7 +382,7 @@ static void putval(Namval_t* np, const char* val, nvflag_t flag, Namfun_t* fp)
382382
{
383383
if(service_list[i]==sp)
384384
{
385-
close(i);
385+
ast_close(i);
386386
if(--sp->refcount<=0)
387387
break;
388388
}
@@ -447,7 +447,7 @@ int b_mkservice(int argc, char** argv, Shbltin_t *context)
447447
UNREACHABLE();
448448
}
449449
if((sp->fd = fcntl(fd, F_DUPFD, 10))>=10)
450-
close(fd);
450+
ast_close(fd);
451451
else
452452
sp->fd = fd;
453453
np = nv_open(var,sh.var_tree,NV_ARRAY|NV_VARNAME);

src/cmd/ksh93/bltins/test.c

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,7 @@ static int test_mode(const char *file)
708708
}
709709

710710
/*
711-
* do an fstat() for /dev/fd/n, otherwise stat()
711+
* do an fstat() for /dev/fd/n or PWD's fd for better performance, otherwise stat()
712712
*/
713713
static int test_stat(const char *name,struct stat *buff)
714714
{
@@ -717,8 +717,11 @@ static int test_stat(const char *name,struct stat *buff)
717717
errno = ENOENT;
718718
return -1;
719719
}
720+
#if _lib_openat
721+
if(name==e_dot && sh.pwdfd > -1)
722+
return fstat(sh.pwdfd,buff);
723+
#endif
720724
if(sh_isdevfd(name))
721725
return fstat((int)strtol(name+8, NULL, 10),buff);
722-
else
723-
return stat(name,buff);
726+
return stat(name,buff);
724727
}

src/cmd/ksh93/data/msg.c

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626

2727
#include "shopt.h"
2828
#include <ast.h>
29-
#include <errno.h>
3029
#include "defs.h"
3130
#include "path.h"
3231
#include "io.h"

src/cmd/ksh93/edit/edit.c

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929

3030
#include "shopt.h"
3131
#include <ast.h>
32-
#include <errno.h>
3332
#include <fault.h>
3433
#include "FEATURE/time"
3534
#if _hdr_utime

src/cmd/ksh93/features/fchdir

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ tst fchdir_osearch_compat note{ can fchdir(2) use file descriptors obtained usin
4040
tst openat_enotdir note{ does openat set ENOTDIR when it fails because the file isn't a directory }end output{
4141
#include <ast.h>
4242
#include <stdio.h>
43-
#include <errno.h>
4443
#if !_lib_openat
4544
#error openat(2) doesn't exist, so this test is pointless
4645
#endif

src/cmd/ksh93/features/poll

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ tst pipe_socketpair note{ use socketpair() for peekable pipe() }end execute{
2929
char buf[256];
3030
pid_t pid;
3131
static char msg[] = "hello world\n";
32-
close(0);
32+
ast_close(0);
3333
if (pipe(pfd) < 0 ||
3434
socketpair(AF_UNIX, SOCK_STREAM, 0, sfd) < 0 ||
3535
shutdown(sfd[1], SHUT_RD) < 0 ||
@@ -39,29 +39,29 @@ tst pipe_socketpair note{ use socketpair() for peekable pipe() }end execute{
3939
return 1;
4040
if (pid)
4141
{
42-
close(pfd[1]);
43-
close(sfd[1]);
42+
ast_close(pfd[1]);
43+
ast_close(sfd[1]);
4444
wait(&n);
4545
if (sfpkrd(pfd[0], buf, sizeof(buf), '\n', -1, 1) >= 0 ||
4646
sfpkrd(sfd[0], buf, sizeof(buf), '\n', -1, 1) < 0)
4747
return 1;
4848
}
4949
else
5050
{
51-
close(pfd[0]);
52-
close(sfd[0]);
51+
ast_close(pfd[0]);
52+
ast_close(sfd[0]);
5353
write(pfd[1], msg, sizeof(msg) - 1);
5454
write(sfd[1], msg, sizeof(msg) - 1);
5555
return 0;
5656
}
57-
close(pfd[0]);
58-
close(sfd[0]);
57+
ast_close(pfd[0]);
58+
ast_close(sfd[0]);
5959
signal(SIGPIPE, handler);
6060
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sfd) < 0 ||
6161
shutdown(sfd[1], SHUT_RD) < 0 ||
6262
shutdown(sfd[0], SHUT_WR) < 0)
6363
return 1;
64-
close(sfd[0]);
64+
ast_close(sfd[0]);
6565
write(sfd[1], msg, sizeof(msg) - 1);
6666
return 1;
6767
}
@@ -75,18 +75,18 @@ tst socketpair_devfd note{ /dev/fd/N handles socketpair() }end execute{
7575
int devfd;
7676
int n;
7777
int sfd[2];
78-
close(0);
78+
ast_close(0);
7979
open("/dev/null", O_RDONLY);
8080
if ((n = open("/dev/fd/0", O_RDONLY)) < 0)
8181
return 1;
82-
close(n);
82+
ast_close(n);
8383
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sfd) < 0 ||
8484
shutdown(sfd[0], 1) < 0 ||
8585
shutdown(sfd[1], 0) < 0)
8686
return 1;
87-
close(0);
87+
ast_close(0);
8888
dup(sfd[0]);
89-
close(sfd[0]);
89+
ast_close(sfd[0]);
9090
if ((n = open("/dev/fd/0", O_RDONLY)) < 0)
9191
return 1;
9292
return 0;

src/cmd/ksh93/include/defs.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ extern int sh_trace(char*[],int);
141141
extern void sh_trim(char*);
142142
extern int sh_type(const char*);
143143
extern void sh_unscope(void);
144+
extern void sh_clear_subshell_pwdfd(void);
144145
#if _lib_openat
145146
extern int sh_diropenat(int,const char *);
146147
extern void sh_pwdupdate(int);

0 commit comments

Comments
 (0)