Skip to content

Commit b82f723

Browse files
committed
Fix IFS field splitting bugs in read -d ''
@dicktyr reports: > for the following script > > IFS=$'\n' read -d '' v1 v2 <<< "$(printf 'a\nb')" ; \ > printf '[%s]\n' "$v1" "$v2" > > bash produces the expected output > > [a] > [b] > > but ksh (93u+m/1.0.10) produces > > [a > b > ] > [] The bug is that 'read' does not correctly do IFS splitting by newline when given a null delimeter (-d '') or any delimiter other than a newline. In the course of debugging this, it was also found that there is incorrect behavour when IFS is unset. src/cmd/ksh93/bltins/read.c: sh_readline(): - If IFS is unset (ifs==NULL), handle field splitting using the default value, $' \t\n' (e_sptbnl). This is specified by POSIX. The type of ifs is changed to const char* to avoid a warning; this is a good idea anyway since sh_readline should never change the value of IFS. - Backport a fix from ksh 93v- 2013-04-02: if the delimiter is not newline but IFS contains a newline, turn on S_DELIM handling for the newline in sh.ifstable. This gets us most of the way towards a full fix, but leaves trailing newline delimiters intact, which is incorrect. - To fix that remaining problem, when stripping off trailing space delimiters, don't only look for the S_SPACE state but also for the S_DELIM state for isspace(3) characters. This *should* only ever be relevant for newline characters but I'm preferring a more generalised whitespace fix here, just in case. src/cmd/ksh93/tests/builtins.sh: - Remove an incorrect text that was enforcing the current buggy behaviour. It was backported from ksh 93v- 2013-03-18, but we didn't notice at the time that this test had been removed again in ksh 93v- 2013-04-02. (re: cfc8744) - Add tests for 10 reproducers related to this bug. Resolves: #926
1 parent 0f6866b commit b82f723

File tree

5 files changed

+57
-20
lines changed

5 files changed

+57
-20
lines changed

COPYRIGHT

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ ksh 93u+m general copyright notice
33
########################################################################
44
# #
55
# The KornShell 93u+m distribution #
6-
# Copyright (c) 2020-2025 Contributors to ksh 93u+m #
6+
# Copyright (c) 2020-2026 Contributors to ksh 93u+m #
77
# <https://github.com/ksh93/ksh> #
88
# Derived from AT&T's ast package (see below) #
99
# Licensed under the Eclipse Public License, Version 2.0 #

NEWS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ 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+
2026-01-21:
6+
7+
- Fixed several IFS field splitting bugs in read(1) that occurred
8+
when it is given a null delimiter (read -d '').
9+
510
2025-09-30:
611

712
- The rm path-bound built-in command from libcmd no longer suppresses error

src/cmd/ksh93/bltins/read.c

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* *
33
* This software is part of the ast package *
44
* Copyright (c) 1982-2012 AT&T Intellectual Property *
5-
* Copyright (c) 2020-2025 Contributors to ksh 93u+m *
5+
* Copyright (c) 2020-2026 Contributors to ksh 93u+m *
66
* and is licensed under the *
77
* Eclipse Public License, Version 2.0 *
88
* *
@@ -221,7 +221,7 @@ int sh_readline(char **names, volatile int fd, int flags, ssize_t size, Sflong_t
221221
char *name, *val;
222222
Sfio_t *iop;
223223
Namfun_t *nfp;
224-
char *ifs;
224+
const char *ifs;
225225
unsigned char *cpmax;
226226
unsigned char *del;
227227
char was_escape = 0;
@@ -314,6 +314,8 @@ int sh_readline(char **names, volatile int fd, int flags, ssize_t size, Sflong_t
314314
Namval_t *mp;
315315
/* set up state table based on IFS */
316316
ifs = nv_getval(mp=sh_scoped(IFSNOD));
317+
if(!ifs)
318+
ifs = e_sptbnl; /* unset == default */
317319
if((flags&R_FLAG) && sh.ifstable['\\']==S_ESC)
318320
sh.ifstable['\\'] = 0;
319321
else if(!(flags&R_FLAG) && sh.ifstable['\\']==0)
@@ -322,7 +324,10 @@ int sh_readline(char **names, volatile int fd, int flags, ssize_t size, Sflong_t
322324
sh.ifstable[delim] = S_NL;
323325
if(delim!='\n')
324326
{
325-
sh.ifstable['\n'] = 0;
327+
if(strchr(ifs,'\n'))
328+
sh.ifstable['\n'] = S_DELIM;
329+
else
330+
sh.ifstable['\n'] = 0;
326331
nv_putval(mp, ifs, NV_RDONLY);
327332
}
328333
sh.ifstable[0] = S_EOF;
@@ -782,13 +787,13 @@ int sh_readline(char **names, volatile int fd, int flags, ssize_t size, Sflong_t
782787
{
783788
/* strip off trailing space delimiters */
784789
unsigned char *vp = (unsigned char*)val + strlen(val);
785-
while(sh.ifstable[*--vp]==S_SPACE);
790+
while(sh.ifstable[*--vp]==S_SPACE || (isspace(*vp) && sh.ifstable[*vp]==S_DELIM));
786791
if(vp==del)
787792
{
788793
if(vp==(unsigned char*)val)
789794
vp--;
790795
else
791-
while(sh.ifstable[*--vp]==S_SPACE);
796+
while(sh.ifstable[*--vp]==S_SPACE || (isspace(*vp) && sh.ifstable[*vp]==S_DELIM));
792797
}
793798
vp[1] = 0;
794799
}

src/cmd/ksh93/include/version.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* *
33
* This software is part of the ast package *
44
* Copyright (c) 1982-2012 AT&T Intellectual Property *
5-
* Copyright (c) 2020-2025 Contributors to ksh 93u+m *
5+
* Copyright (c) 2020-2026 Contributors to ksh 93u+m *
66
* and is licensed under the *
77
* Eclipse Public License, Version 2.0 *
88
* *
@@ -18,14 +18,14 @@
1818
#include <ast_release.h>
1919
#include "git.h"
2020

21-
#define SH_RELEASE_DATE "2025-06-22" /* must be in this format for $((.sh.version)) */
21+
#define SH_RELEASE_DATE "2026-01-21" /* must be in this format for $((.sh.version)) */
2222
/*
2323
* This comment keeps SH_RELEASE_DATE a few lines away from SH_RELEASE_SVER to avoid
2424
* merge conflicts when cherry-picking dev branch commits onto a release branch.
2525
*/
2626
#define SH_RELEASE_FORK "93u+m" /* only change if you develop a new ksh93 fork */
2727
#define SH_RELEASE_SVER "1.1.0-alpha" /* semantic version number: https://semver.org */
28-
#define SH_RELEASE_CPYR "(c) 2020-2025 Contributors to ksh " SH_RELEASE_FORK
28+
#define SH_RELEASE_CPYR "(c) 2020-2026 Contributors to ksh " SH_RELEASE_FORK
2929

3030
/* Scripts sometimes field-split ${.sh.version}, so don't change amount of whitespace. */
3131
/* Arithmetic $((.sh.version)) uses the last 10 chars, so the date must be at the end. */

src/cmd/ksh93/tests/builtins.sh

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# #
33
# This software is part of the ast package #
44
# Copyright (c) 1982-2012 AT&T Intellectual Property #
5-
# Copyright (c) 2020-2025 Contributors to ksh 93u+m #
5+
# Copyright (c) 2020-2026 Contributors to ksh 93u+m #
66
# and is licensed under the #
77
# Eclipse Public License, Version 2.0 #
88
# #
@@ -1576,16 +1576,6 @@ print ". $tmp/evalbug" > "$tmp/envfile"
15761576
[[ $(ENV=$tmp/envfile "$SHELL" -i -c : 2> /dev/null) == ok ]] || err_exit 'eval inside dot script called from profile file not working'
15771577
fi # !SHOPT_SCRIPTONLY
15781578
1579-
# Backported ksh93v- 2013-03-18 test for 'read -A', where
1580-
# IFS sets the delimiter to a newline while -d specifies
1581-
# no delimiter (-d takes priority over IFS).
1582-
if ((SHOPT_BRACEPAT)); then
1583-
got=$(printf %s\\n {a..f} | IFS=$'\n' read -rd '' -A a; typeset -p a)
1584-
exp=$'typeset -a a=($\'a\\nb\\nc\\nd\\ne\\nf\\n\')'
1585-
[[ $got == "$exp" ]] || err_exit "IFS overrides the delimiter specified by the read command's -d option" \
1586-
"(expected $(printf %q "$exp"), got $(printf %q "$got"))"
1587-
fi
1588-
15891579
# The read builtin's -a and -A flags should function identically
15901580
read_a_test=$tmp/read_a_test.sh
15911581
cat > "$read_a_test" << 'EOF'
@@ -1756,6 +1746,43 @@ exp='%'
17561746
"(expected $(printf %q "$exp"), got $(printf %q "$got"))"
17571747
fi # SHOPT_MULTIBYTE
17581748
1749+
# ======
1750+
# read(1): IFS field splitting bugs when given a null delimiter (-d '')
1751+
# https://github.com/ksh93/ksh/issues/926
1752+
unset v1 v2 L
1753+
IFS=$'\n' read -d '' v1 v2 <<< $'a\nb'
1754+
got="<$v1> <$v2>"
1755+
exp="<a> <b>"
1756+
[[ $got == "$exp" ]] || err_exit "issue 926 r1 (expected $(printf %q "$exp"), got $(printf %q "$got"))"
1757+
IFS='' read -d '' v1 v2 <<< $'a\nb'
1758+
got="<$v1> <$v2>"
1759+
exp=$'<a\nb\n> <>'
1760+
[[ $got == "$exp" ]] || err_exit "issue 926 r2 (expected $(printf %q "$exp"), got $(printf %q "$got"))"
1761+
got=$(printf 'one\0two\0three\0' | while read -r -d "" L; do printf "<%s>\\n" "$L"; done; printf "end: <%s>\\n" "$L")
1762+
exp=$'<one>\n<two>\n<three>\nend: <>'
1763+
[[ $got == "$exp" ]] || err_exit "issue 926 r3 (expected $(printf %q "$exp"), got $(printf %q "$got"))"
1764+
got=$(printf 'one\0two\0three\0' | while IFS= read -r -d "" L; do printf "<%s>\\n" "$L"; done; printf "end: <%s>\\n" "$L")
1765+
exp=$'<one>\n<two>\n<three>\nend: <>'
1766+
[[ $got == "$exp" ]] || err_exit "issue 926 r4 (expected $(printf %q "$exp"), got $(printf %q "$got"))"
1767+
got=$(printf 'one\ntwo\nthree\n' | while read -r -d "" L; do printf "<%s>\\n" "$L"; done; printf "end: <%s>\\n" "$L")
1768+
exp=$'end: <one\ntwo\nthree>'
1769+
[[ $got == "$exp" ]] || err_exit "issue 926 r5 (expected $(printf %q "$exp"), got $(printf %q "$got"))"
1770+
got=$(printf 'one\ntwo\nthree\n' | while IFS= read -r -d "" L; do printf "<%s>\\n" "$L"; done; printf "end: <%s>\\n" "$L")
1771+
exp=$'end: <one\ntwo\nthree\n>'
1772+
[[ $got == "$exp" ]] || err_exit "issue 926 r6 (expected $(printf %q "$exp"), got $(printf %q "$got"))"
1773+
got=$(printf "one\ntwo\nthXree\nX" | while IFS=X read -r -d "" L; do printf "<%s>\n" "$L"; done; printf "end: <%s>\n" "$L")
1774+
exp=$'end: <one\ntwo\nthXree\nX>'
1775+
[[ $got == "$exp" ]] || err_exit "issue 926 r7 (expected $(printf %q "$exp"), got $(printf %q "$got"))"
1776+
got=$(printf "one\ntwo\nth ree\n " | while IFS=' ' read -r -d "" L; do printf "<%s>\n" "$L"; done; printf "end: <%s>\n" "$L")
1777+
exp=$'end: <one\ntwo\nth ree\n>'
1778+
[[ $got == "$exp" ]] || err_exit "issue 926 r8 (expected $(printf %q "$exp"), got $(printf %q "$got"))"
1779+
got=$(printf "one\ntwo\nthree\n" | while read -r -d "" L; do printf "<%s>\n" "$L"; done; printf "end: <%s>\n" "$L")
1780+
exp=$'end: <one\ntwo\nthree>'
1781+
[[ $got == "$exp" ]] || err_exit "issue 926 r9 (expected $(printf %q "$exp"), got $(printf %q "$got"))"
1782+
got=$(unset IFS; printf "one\ntwo\nthree\n" | while read -r -d "" L; do printf "<%s>\n" "$L"; done; printf "end: <%s>\n" "$L")
1783+
exp=$'end: <one\ntwo\nthree>'
1784+
[[ $got == "$exp" ]] || err_exit "issue 926 r10 (expected $(printf %q "$exp"), got $(printf %q "$got"))"
1785+
17591786
# ====== MUST BE AT END ======
17601787
# checks for tests run in parallel (see top)
17611788
wait "$parallel_1"

0 commit comments

Comments
 (0)