Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ksh93: Changes to functions within subshells are ignored #73

Open
McDutchie opened this issue Oct 6, 2017 · 11 comments
Open

ksh93: Changes to functions within subshells are ignored #73

McDutchie opened this issue Oct 6, 2017 · 11 comments
Labels

Comments

@McDutchie
Copy link
Contributor

Function definitions within non-forked subshells (including command substitutions) are silently ignored if a function by the same name exists in the main shell, so the wrong code is executed. unset -f is also silently ignored. Test script:

foo() { echo WRONG; }
(foo() { echo ok; } && foo && unset -f foo && foo)

Output:

WRONG
WRONG

Forcing the subshell to be forked (by making it a background job, or by including a redirection within a command substitution) is an effective workaround.

Silently executing the wrong code under any circumstances is a bad bug, but it seems to have been in ksh93 since 1993...

@McDutchie McDutchie changed the title ksh93: Changes to functions within subshells are ignored [BUG_FNSUBSH] ksh93: Changes to functions within subshells are ignored Oct 6, 2017
@krader1961
Copy link
Contributor

This is definitely a bug. But the subject line is misleading. The problem is that unless you force the (...) list of commands to be executed in a subshell you get the wrong behavior. The (...) notation does not force the creation of a subshell. It only creates a "separate environment". Whatever that means as the term appears to be unused anywhere else in the documentation or source code. I too used to think it created a subshell but it doesn't. Try this: echo $$; ( echo $$ ); ( echo $$ ) & to see what I mean.

Some things, most notably variable assignment, are handled as expected by parenthesized blocks:

$ x=1; echo $x $$; (x=2; echo $x $$); echo $x $$
1 51108
2 51108
1 51108

@krader1961 krader1961 changed the title ksh93: Changes to functions within subshells are ignored ksh93: Changes to functions within parenthesized expressions are ignored Nov 2, 2017
@McDutchie
Copy link
Contributor Author

No, your understanding is incorrect. The (...) notation always creates a subshell, but ksh93 by default implements subshells without forking a new process.

Your example echo $$; ( echo $$ ); ( echo $$ ) & simply prints the PID of the main shell three times. That's because $$ doesn't change (on any shell) when a subshell is created (whether by forking or not) because by definition it represents the PID of the main shell.

Instead, try this:

$ echo ${.sh.subshell}; (echo ${.sh.subshell}; (echo ${.sh.subshell}); echo ${.sh.subshell})
0
1
2
1

The subshell counter goes up as the subshell level increases.

@McDutchie McDutchie changed the title ksh93: Changes to functions within parenthesized expressions are ignored ksh93: Changes to functions within subshells are ignored Nov 2, 2017
@stephane-chazelas
Copy link

Some more fun:

$ cat a
foo() { echo WRONG; }
(foo() { echo ok; } && foo && unset -f foo && foo)
$ ksh ./a
WRONG
WRONG
$ ksh -c "$(cat a)"
ok
ksh: line 2: foo: not found
$ ksh -c 'eval "$(cat a)"'
ok
ksh: eval: line 2: foo: not found
$ ksh -c '. ./a'
ok
WRONG

@McDutchie
Copy link
Contributor Author

McDutchie commented Apr 26, 2018

@stephane-chazelas gave an essential hint in a comment on #480: running the ulimit builtin in a subshell forces it to fork off into a separate process right then and there.

So I went to look in the source code for how the ulimit builtin does that. And it's surprisingly simple. It's this little line in bltins/ulimit.c:

if (shp->subshell && !shp->subshare) sh_subfork();

With a lot of grepping, I figured out that shp->subshell refers to subshells of the non-forking variety (recall that ${.sh.subshell} gets reset to zero when a subshell is forked!) and shp->subshare refers to command substitutions of the ${ ...; } variety, which are actually considered to be subshells that share their state with the parent shell (see the comments on the definitions of SH_SUBSHARE and char subshare in include/shell.h).

So the line above means: if we're in a subshell of the non-forking variety, and it's not the ${ ...; } variant, then fork off and make the forked process continue where we left off (that is what sh_subfork() does).

And forking avoids #73. Thus, I am fairly confident that the following patch fixes it, without messing with complicated code. It seems to fix the oddities observed by Stéphane in the comment above as well.

diff --git a/src/cmd/ksh93/bltins/typeset.c b/src/cmd/ksh93/bltins/typeset.c
index 64f38b7..7643de4 100644
--- a/src/cmd/ksh93/bltins/typeset.c
+++ b/src/cmd/ksh93/bltins/typeset.c
@@ -1127,6 +1127,10 @@ static int unall(int argc, char **argv, Dt_t *troot, Shell_t *shp) {
     while (r = optget(argv, name)) {
	 switch (r) {
	     case 'f': {
+                if (shp->subshell && !shp->subshare) {
+                    // When unsetting a function in a subshell, fork to avoid https://github.com/att/ast/issues/73
+                    sh_subfork();
+                }
		 troot = sh_subfuntree(shp, 1);
		 break;
	     }
diff --git a/src/cmd/ksh93/sh/xec.c b/src/cmd/ksh93/sh/xec.c
index 9d674bc..b39cfd5 100644
--- a/src/cmd/ksh93/sh/xec.c
+++ b/src/cmd/ksh93/sh/xec.c
@@ -2350,6 +2350,10 @@ int sh_exec(Shell_t *shp, const Shnode_t *t, int flags) {
		 char *fname = ((struct functnod *)t)->functnam;
		 Namval_t *npv = 0, *mp;
		 cp = strrchr(fname, '.');
+                if (shp->subshell && !shp->subshare) {
+                    // When (re)defining a function in a subshell, fork to avoid https://github.com/att/ast/issues/73
+                    sh_subfork();
+                }
 #if SHOPT_COSHELL
		 if (shp->inpool) {
		     sh_exec(shp, t->funct.functtre, 0);

@kernigh
Copy link
Contributor

kernigh commented Apr 26, 2018

This looks broken to me:

foo() { echo WRONG; }
(foo() { echo ok; } && foo)

outputs WRONG, but

foo() { echo WRONG; }; (foo() { echo ok; } && foo)

outputs ok. The only change is from a newline to a semicolon. There is a similar problem with aliases, as I just mentioned in #471.

@kernigh
Copy link
Contributor

kernigh commented Apr 27, 2018

I applied the patch by @McDutchie (using patch -l -p1 because of a whitespace problem). On my machine running OpenBSD/amd64 6.3, this patch causes failures in at least meson test coprocess and meson test namespace.

The patch uses sh_subfork() to fork the subshell so that sh_subfuntree() doesn't create the subshell's function tree. The patch doesn't fix autoload:

$ cat fee
fee() { echo loaded; }
$ cat try.sh
foo() { echo WRONG; }
FPATH=.
(autoload fee && foo() { echo ok; } && foo)
$ nksh try.sh
WRONG

This is because autoload fee called sh_subfuntree() and created the tree. The work around is to call sh_subfork() from sh_subfuntree(). Undo that patch and apply this one (but coprocess and namespace still fail):

diff --git a/src/cmd/ksh93/sh/subshell.c b/src/cmd/ksh93/sh/subshell.c
index 90196bf2..97b8e3eb 100644
--- a/src/cmd/ksh93/sh/subshell.c
+++ b/src/cmd/ksh93/sh/subshell.c
@@ -373,10 +373,15 @@ Dt_t *sh_subfuntree(Shell_t *shp, int create) {
     struct subshell *sp = subshell_data;
     if (!sp || shp->curenv == 0) return (shp->fun_tree);
     if (!sp->sfun && create) {
+#if 1
+        // Fork to avoid https://github.com/att/ast/issues/73
+        sh_subfork();
+#else
         sp->sfun = dtopen(&_Nvdisc, Dtoset);
         dtuserdata(sp->sfun, shp, 1);
         dtview(sp->sfun, shp->fun_tree);
         shp->fun_tree = sp->sfun;
+#endif
     }
     return sp->shp->fun_tree;
 }

I also have a different patch that fixes function replacement, but doesn't fix unset -f. Undo the other patches and apply this one:

diff --git a/src/cmd/ksh93/sh/parse.c b/src/cmd/ksh93/sh/parse.c
index d8020382..9a6f642c 100644
--- a/src/cmd/ksh93/sh/parse.c
+++ b/src/cmd/ksh93/sh/parse.c
@@ -1370,8 +1370,8 @@ static Shnode_t *simple(Lex_t *lexp, int flag, struct ionod *io) {
                     np = nv_bfsearch(argp->argval, lexp->sh->fun_tree, (Namval_t **)&t->comnamq,
                                      NULL);
                 }
-                if (cmdarg == 0) t->comnamp = (void *)np;
                 if (np && is_abuiltin(np)) {
+                    if (cmdarg == 0) t->comnamp = (void *)np;
                     if (nv_isattr(np, BLT_DCL)) {
                         assignment = 1 + !strcmp(argp->argval, "alias");
                         if (np == SYSTYPESET) {

The parser can set t->comnamp to the wrong function. Suppose that the shell has executed foo() { echo WRONG; } and is now parsing (foo() { echo ok; } && foo). The parser was setting t->comnamp to the wrong foo. The patch doesn't set t->comnamp unless it was a builtin. Now the subshell can't call t->comnamp, so it looks for foo and finds the ok foo in the subshell's function tree.

With this patch, unset -f is still wrong:

$ cat fee    
fee() { echo loaded; }
$ cat try.sh
foo() { echo WRONG; }
FPATH=.
(autoload fee && foo() { echo ok; } && foo)
(unset -f foo && foo)
$ nksh try.sh
ok
WRONG

@saper
Copy link
Contributor

saper commented May 11, 2018

I don't think it is a bug, although it feels strange: ( introduces not only a subshell (@krader1961 a subshell is a "separate environment" according to the Korn Shell book) but also is a compound command.

If the first token of the command is one of the following operators:
( ksh reads it as a compound command, until the matching closing ).

Also semicolon separated commands are not equivalent to newline separated commands.

I am still wrapping my head around this but it makes sense when reading the book and is consistent.

 a; b

is a list command, which is a compound command - which is read all at once.

It is not equivalent to

a
b

which are two simple commands.

McDutchie added a commit to ksh93/ksh that referenced this issue Jun 11, 2020
Functions can now be correctly redefined and unset in subshell
environments (such as ( ... ), $(command substitutions), etc).
Before this fix, attempts to do this were silently ignored (!!!),
causing the wrong code (i.e.: the function by the same name from
the parent shell environment) to be executed.

Redefining and unsetting functions within "shared" command
substitutions of the form '${ ...; }' is also fixed.

Prior discussion: att#73

src/cmd/ksh93/sh/parse.c:
- A fix from George Koelher (URL above). He writes:
  | The parser can set t->comnamp to the wrong function.
  | Suppose that the shell has executed
  |     foo() { echo WRONG; }
  | and is now parsing
  |     (foo() { echo ok; } && foo)
  | The parser was setting t->comnamp to the wrong foo. [This
  | fix] doesn't set t->comnamp unless it was a builtin. Now the
  | subshell can't call t->comnamp, so it looks for foo and finds
  | the ok foo in the subshell's function tree.

src/cmd/ksh93/bltins/typeset.c:
- Unsetting functions in a virtual/non-forked subshell still
  doesn't work: nv_open() fails to find the function. To work
  around this problem, make 'unset -f' fork the subshell into its
  own process with sh_subfork().
- The workaround exposed another bug: if we unset a function in a
  subshell tree that overrode a function by the same name in the
  main shell, then nv_delete() exposes the function from the main
  shell scope. Since 'unset -f' now always forks a subshell, the
  fix is to simply walk though troot's parent views and delete any
  such zombie functions as well. (Without this, the 4 'more fun'
  tests in tests/subshell.sh fail.)

src/cmd/ksh93/sh/subshell.c: sh_subfuntree():
- Fix function (re)definitions and unsetting in "shared" command
  substitutions of the form '${ commandlist; }' (i.e.: if
  sp->shp->subshare is true). Though internally this is a weird
  form of virtual subshell, the manual page says it does not
  execute in a subshell (meaning, all changes must survive it), so
  a subshell function tree must not be created for these.

src/cmd/ksh93/tests/subshell.sh:
- Add regression tests related to these bugfixes. Test unsetting
  and redefining a function in all three forms of virtual subshell.

(cherry picked from commit dde387825ab1bbd9f2eafc5dc38d5fd0bf9c3652)
McDutchie added a commit to ksh93/ksh that referenced this issue Jun 11, 2020
Functions can now be correctly redefined and unset in subshell
environments (such as ( ... ), $(command substitutions), etc).
Before this fix, attempts to do this were silently ignored (!!!),
causing the wrong code (i.e.: the function by the same name from
the parent shell environment) to be executed.

Redefining and unsetting functions within "shared" command
substitutions of the form '${ ...; }' is also fixed.

Prior discussion: att#73

src/cmd/ksh93/sh/parse.c:
- A fix from George Koelher (URL above). He writes:
  | The parser can set t->comnamp to the wrong function.
  | Suppose that the shell has executed
  |     foo() { echo WRONG; }
  | and is now parsing
  |     (foo() { echo ok; } && foo)
  | The parser was setting t->comnamp to the wrong foo. [This
  | fix] doesn't set t->comnamp unless it was a builtin. Now the
  | subshell can't call t->comnamp, so it looks for foo and finds
  | the ok foo in the subshell's function tree.

src/cmd/ksh93/bltins/typeset.c:
- Unsetting functions in a virtual/non-forked subshell still
  doesn't work: nv_open() fails to find the function. To work
  around this problem, make 'unset -f' fork the subshell into its
  own process with sh_subfork().
- The workaround exposed another bug: if we unset a function in a
  subshell tree that overrode a function by the same name in the
  main shell, then nv_delete() exposes the function from the main
  shell scope. Since 'unset -f' now always forks a subshell, the
  fix is to simply walk though troot's parent views and delete any
  such zombie functions as well. (Without this, the 4 'more fun'
  tests in tests/subshell.sh fail.)

src/cmd/ksh93/sh/subshell.c: sh_subfuntree():
- Fix function (re)definitions and unsetting in "shared" command
  substitutions of the form '${ commandlist; }' (i.e.: if
  sp->shp->subshare is true). Though internally this is a weird
  form of virtual subshell, the manual page says it does not
  execute in a subshell (meaning, all changes must survive it), so
  a subshell function tree must not be created for these.

src/cmd/ksh93/tests/subshell.sh:
- Add regression tests related to these bugfixes. Test unsetting
  and redefining a function in all three forms of virtual subshell.

(cherry picked from commit dde387825ab1bbd9f2eafc5dc38d5fd0bf9c3652)
@cassioneri
Copy link

The following seems another manifestation of this bug which, through an extra level of indirection, makes assignments in subshells to affect unset variables in the parent shell:

$ cat bug.sh
#!/bin/ksh93

unset a b
c=0

function set_ac { a=1; c=1; }
function set_abc { ( set_ac ; b=1 ) }

set_abc
echo "a=$a b=$b c=$c"
$ ./bug.sh
a=1 b= c=0

As mentioned by @McDutchie in #73 (comment) and #478 (comment) the following are known workarounds. Replace the definition of set_abc with either

function set_abc { ( set_ac ; b=1 ) | echo -n; }

or

function set_abc { ( ulimit -t unlimited ; set_ac ; b=1 ) }

Here is a third workaround that shows how bad subshells step on each other's toes. By setting a in a second subshell, it prevents the first subshell to set a in the parent shell.

function set_abc { ( set_ac ; b=1 ) }
( a=1 )

With either of the three above we get

$ ./bug.sh
a= b= c=0

@McDutchie
Copy link
Contributor Author

Hi @cassioneri , thank you for that report. This AT&T repo is no longer active, but the (to the best of my knowledge) only actively and openly developed fork of ksh93 is at https://github.com/ksh93/ksh -- with much of this bug already fixed, but sure enough, your test script exposes another bug that's not fixed yet. Would you mind opening an issue there, please?

@cassioneri
Copy link

@McDutchie. Will do.

@McDutchie
Copy link
Contributor Author

Now that I'm here, I'd like to give belated thanks to @kernigh for the parser fix and to @stephane-chazelas for the reproducers, both of which I've incorporated in what is, to the best of my knowledge, currently the only actively and openly developed fork of ksh93. It's based off the "stable" 93u+ 2012-08-01 version (which might be the least buggy version ever released but is still nothing like actually stable). See my new repo, particularly 047cb33 and 759157b. I'd like to invite you to follow the development and provide any input you feel called to give. The idea is to get a rock-solid ksh93 release out there, so the more eyes on this code, the better.

citrus-it pushed a commit to citrus-it/ast that referenced this issue Apr 15, 2021
This bug was reported on the old mailing list:
https://www.mail-archive.com/ast-developers@lists.research.att.com/msg00207.html

A fork bomb can occur when SIGTSTP is sent to the vi editor. Vi
must be launched from a script run with exec (tested with
BusyBox vi, nvi and vim):
$ cat /tmp/foo
vi /tmp/bar
echo end
$ ksh
$ chmod +x /tmp/foo
$ exec /tmp/foo
While in vi, send SIGTSTP using Ctrl-Z

src/cmd/ksh93/sh/fault.c:
- Only fork after Ctrl-Z if job control is available. The patch
  used checks 'job.jobcontrol' instead of 'SH_MONITOR':
  https://git.centos.org/rpms/ksh/blob/c8/f/SOURCES/ksh-20120801-forkbomb.patch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

7 participants