diff options
author | Denys Vlasenko <vda.linux@googlemail.com> | 2023-06-25 17:42:05 +0200 |
---|---|---|
committer | Denys Vlasenko <vda.linux@googlemail.com> | 2023-06-25 17:42:05 +0200 |
commit | c1c267fd36b0fcac8c8871232eecc1e360173990 (patch) | |
tree | 83a490c0098926b5e5ef66f474c2b8a6dcfd6620 | |
parent | 019dd31150526b7dd9dd0addfc38f08bcf7ec551 (diff) | |
download | busybox-w32-c1c267fd36b0fcac8c8871232eecc1e360173990.tar.gz busybox-w32-c1c267fd36b0fcac8c8871232eecc1e360173990.tar.bz2 busybox-w32-c1c267fd36b0fcac8c8871232eecc1e360173990.zip |
shell/math: bash-compatible handling of too large numbers
function old new delta
parse_with_base - 170 +170
evaluate_string 1477 1309 -168
------------------------------------------------------------------------------
(add/remove: 1/0 grow/shrink: 0/1 up/down: 170/-168) Total: 2 bytes
Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
-rw-r--r-- | shell/ash_test/ash-arith/arith-bignum1.right | 13 | ||||
-rwxr-xr-x | shell/ash_test/ash-arith/arith-bignum1.tests | 17 | ||||
-rw-r--r-- | shell/ash_test/ash-arith/arith.right | 3 | ||||
-rwxr-xr-x | shell/ash_test/ash-arith/arith.tests | 6 | ||||
-rw-r--r-- | shell/hush_test/hush-arith/arith-bignum1.right | 13 | ||||
-rwxr-xr-x | shell/hush_test/hush-arith/arith-bignum1.tests | 17 | ||||
-rw-r--r-- | shell/hush_test/hush-arith/arith.right | 3 | ||||
-rwxr-xr-x | shell/hush_test/hush-arith/arith.tests | 6 | ||||
-rw-r--r-- | shell/math.c | 84 |
9 files changed, 118 insertions, 44 deletions
diff --git a/shell/ash_test/ash-arith/arith-bignum1.right b/shell/ash_test/ash-arith/arith-bignum1.right new file mode 100644 index 000000000..42a8016ec --- /dev/null +++ b/shell/ash_test/ash-arith/arith-bignum1.right | |||
@@ -0,0 +1,13 @@ | |||
1 | 18 digits: 999999999999999999 | ||
2 | 19 digits: -8446744073709551617 | ||
3 | 20 digits: 7766279631452241919 | ||
4 | 18 digits- -999999999999999999 | ||
5 | 19 digits- 8446744073709551617 | ||
6 | 20 digits- -7766279631452241919 | ||
7 | Hex base#: | ||
8 | 16 digits: 9876543210abcedf | ||
9 | 17 digits: 876543210abcedfc | ||
10 | 18 digits: 76543210abcedfcc | ||
11 | 16 digits: 6789abcdef543121 | ||
12 | 17 digits: 789abcdef5431204 | ||
13 | 18 digits: 89abcdef54312034 | ||
diff --git a/shell/ash_test/ash-arith/arith-bignum1.tests b/shell/ash_test/ash-arith/arith-bignum1.tests new file mode 100755 index 000000000..ef8f928bc --- /dev/null +++ b/shell/ash_test/ash-arith/arith-bignum1.tests | |||
@@ -0,0 +1,17 @@ | |||
1 | exec 2>&1 | ||
2 | # If the number does not fit in 64 bits, bash uses truncated 64-bit value | ||
3 | # (essentially, it does not check for overflow in "n = n * base + digit" | ||
4 | # calculation). | ||
5 | echo 18 digits: $((999999999999999999)) | ||
6 | echo 19 digits: $((9999999999999999999)) | ||
7 | echo 20 digits: $((99999999999999999999)) | ||
8 | echo 18 digits- $((-999999999999999999)) | ||
9 | echo 19 digits- $((-9999999999999999999)) | ||
10 | echo 20 digits- $((-99999999999999999999)) | ||
11 | echo "Hex base#:" | ||
12 | printf '16 digits: %016x\n' $((16#9876543210abcedf)) | ||
13 | printf '17 digits: %016x\n' $((16#9876543210abcedfc)) | ||
14 | printf '18 digits: %016x\n' $((16#9876543210abcedfcc)) | ||
15 | printf '16 digits: %016x\n' $((-16#9876543210abcedf)) | ||
16 | printf '17 digits: %016x\n' $((-16#9876543210abcedfc)) | ||
17 | printf '18 digits: %016x\n' $((-16#9876543210abcedfcc)) | ||
diff --git a/shell/ash_test/ash-arith/arith.right b/shell/ash_test/ash-arith/arith.right index 8bc78b8d1..b2c3f56d8 100644 --- a/shell/ash_test/ash-arith/arith.right +++ b/shell/ash_test/ash-arith/arith.right | |||
@@ -80,8 +80,9 @@ other bases | |||
80 | 62 62 | 80 | 62 62 |
81 | 63 63 | 81 | 63 63 |
82 | missing number after base | 82 | missing number after base |
83 | 0 0 | 83 | ./arith.tests: line 161: arithmetic syntax error |
84 | ./arith.tests: line 162: arithmetic syntax error | 84 | ./arith.tests: line 162: arithmetic syntax error |
85 | ./arith.tests: line 163: arithmetic syntax error | ||
85 | ./arith.tests: line 164: divide by zero | 86 | ./arith.tests: line 164: divide by zero |
86 | ./arith.tests: let: line 165: arithmetic syntax error | 87 | ./arith.tests: let: line 165: arithmetic syntax error |
87 | ./arith.tests: line 166: arithmetic syntax error | 88 | ./arith.tests: line 166: arithmetic syntax error |
diff --git a/shell/ash_test/ash-arith/arith.tests b/shell/ash_test/ash-arith/arith.tests index b9cb8ba4c..42cd7fdbf 100755 --- a/shell/ash_test/ash-arith/arith.tests +++ b/shell/ash_test/ash-arith/arith.tests | |||
@@ -155,12 +155,12 @@ echo 63 $(( 64#_ )) | |||
155 | #ash# # weird bases (error) | 155 | #ash# # weird bases (error) |
156 | #ash# echo $(( 3425#56 )) | 156 | #ash# echo $(( 3425#56 )) |
157 | 157 | ||
158 | echo missing number after base | ||
159 | echo 0 $(( 2# )) | ||
160 | 158 | ||
161 | # these should generate errors | 159 | # these should generate errors |
160 | echo missing number after base | ||
161 | ( echo $(( 2# )) ) | ||
162 | ( echo $(( 7 = 43 )) ) | 162 | ( echo $(( 7 = 43 )) ) |
163 | #ash# echo $(( 2#44 )) | 163 | ( echo $(( 2#44 )) ) |
164 | ( echo $(( 44 / 0 )) ) | 164 | ( echo $(( 44 / 0 )) ) |
165 | ( let 'jv += $iv' ) | 165 | ( let 'jv += $iv' ) |
166 | ( echo $(( jv += \$iv )) ) | 166 | ( echo $(( jv += \$iv )) ) |
diff --git a/shell/hush_test/hush-arith/arith-bignum1.right b/shell/hush_test/hush-arith/arith-bignum1.right new file mode 100644 index 000000000..42a8016ec --- /dev/null +++ b/shell/hush_test/hush-arith/arith-bignum1.right | |||
@@ -0,0 +1,13 @@ | |||
1 | 18 digits: 999999999999999999 | ||
2 | 19 digits: -8446744073709551617 | ||
3 | 20 digits: 7766279631452241919 | ||
4 | 18 digits- -999999999999999999 | ||
5 | 19 digits- 8446744073709551617 | ||
6 | 20 digits- -7766279631452241919 | ||
7 | Hex base#: | ||
8 | 16 digits: 9876543210abcedf | ||
9 | 17 digits: 876543210abcedfc | ||
10 | 18 digits: 76543210abcedfcc | ||
11 | 16 digits: 6789abcdef543121 | ||
12 | 17 digits: 789abcdef5431204 | ||
13 | 18 digits: 89abcdef54312034 | ||
diff --git a/shell/hush_test/hush-arith/arith-bignum1.tests b/shell/hush_test/hush-arith/arith-bignum1.tests new file mode 100755 index 000000000..ef8f928bc --- /dev/null +++ b/shell/hush_test/hush-arith/arith-bignum1.tests | |||
@@ -0,0 +1,17 @@ | |||
1 | exec 2>&1 | ||
2 | # If the number does not fit in 64 bits, bash uses truncated 64-bit value | ||
3 | # (essentially, it does not check for overflow in "n = n * base + digit" | ||
4 | # calculation). | ||
5 | echo 18 digits: $((999999999999999999)) | ||
6 | echo 19 digits: $((9999999999999999999)) | ||
7 | echo 20 digits: $((99999999999999999999)) | ||
8 | echo 18 digits- $((-999999999999999999)) | ||
9 | echo 19 digits- $((-9999999999999999999)) | ||
10 | echo 20 digits- $((-99999999999999999999)) | ||
11 | echo "Hex base#:" | ||
12 | printf '16 digits: %016x\n' $((16#9876543210abcedf)) | ||
13 | printf '17 digits: %016x\n' $((16#9876543210abcedfc)) | ||
14 | printf '18 digits: %016x\n' $((16#9876543210abcedfcc)) | ||
15 | printf '16 digits: %016x\n' $((-16#9876543210abcedf)) | ||
16 | printf '17 digits: %016x\n' $((-16#9876543210abcedfc)) | ||
17 | printf '18 digits: %016x\n' $((-16#9876543210abcedfcc)) | ||
diff --git a/shell/hush_test/hush-arith/arith.right b/shell/hush_test/hush-arith/arith.right index df8154f97..d3a978611 100644 --- a/shell/hush_test/hush-arith/arith.right +++ b/shell/hush_test/hush-arith/arith.right | |||
@@ -82,7 +82,8 @@ other bases | |||
82 | 62 62 | 82 | 62 62 |
83 | 63 63 | 83 | 63 63 |
84 | missing number after base | 84 | missing number after base |
85 | 0 0 | 85 | hush: arithmetic syntax error |
86 | hush: arithmetic syntax error | ||
86 | hush: arithmetic syntax error | 87 | hush: arithmetic syntax error |
87 | hush: divide by zero | 88 | hush: divide by zero |
88 | hush: can't execute 'let': No such file or directory | 89 | hush: can't execute 'let': No such file or directory |
diff --git a/shell/hush_test/hush-arith/arith.tests b/shell/hush_test/hush-arith/arith.tests index 6b707486c..9f0399816 100755 --- a/shell/hush_test/hush-arith/arith.tests +++ b/shell/hush_test/hush-arith/arith.tests | |||
@@ -159,12 +159,12 @@ echo 63 $(( 64#_ )) | |||
159 | #ash# # weird bases (error) | 159 | #ash# # weird bases (error) |
160 | #ash# echo $(( 3425#56 )) | 160 | #ash# echo $(( 3425#56 )) |
161 | 161 | ||
162 | echo missing number after base | ||
163 | echo 0 $(( 2# )) | ||
164 | 162 | ||
165 | # these should generate errors | 163 | # these should generate errors |
164 | echo missing number after base | ||
165 | ( echo $(( 2# )) ) | ||
166 | ( echo $(( 7 = 43 )) ) | 166 | ( echo $(( 7 = 43 )) ) |
167 | #ash# echo $(( 2#44 )) | 167 | ( echo $(( 2#44 )) ) |
168 | ( echo $(( 44 / 0 )) ) | 168 | ( echo $(( 44 / 0 )) ) |
169 | ( let 'jv += $iv' ) | 169 | ( let 'jv += $iv' ) |
170 | ( echo $(( jv += \$iv )) ) | 170 | ( echo $(( jv += \$iv )) ) |
diff --git a/shell/math.c b/shell/math.c index ac758639f..fbf5c587e 100644 --- a/shell/math.c +++ b/shell/math.c | |||
@@ -531,29 +531,11 @@ static const char op_tokens[] ALIGN1 = { | |||
531 | #define END_POINTER (&op_tokens[sizeof(op_tokens)-1]) | 531 | #define END_POINTER (&op_tokens[sizeof(op_tokens)-1]) |
532 | 532 | ||
533 | #if ENABLE_FEATURE_SH_MATH_BASE | 533 | #if ENABLE_FEATURE_SH_MATH_BASE |
534 | static arith_t strto_arith_t(const char *nptr, char **endptr) | 534 | static arith_t parse_with_base(const char *nptr, char **endptr, unsigned base) |
535 | { | 535 | { |
536 | unsigned base; | 536 | arith_t n = 0; |
537 | arith_t n; | 537 | const char *start = nptr; |
538 | |||
539 | # if ENABLE_FEATURE_SH_MATH_64 | ||
540 | n = strtoull(nptr, endptr, 0); | ||
541 | # else | ||
542 | n = strtoul(nptr, endptr, 0); | ||
543 | # endif | ||
544 | if (**endptr != '#' | ||
545 | || (*nptr < '1' || *nptr > '9') | ||
546 | || (n < 2 || n > 64) | ||
547 | ) { | ||
548 | return n; | ||
549 | } | ||
550 | 538 | ||
551 | /* It's "N#nnnn" or "NN#nnnn" syntax, NN can't start with 0, | ||
552 | * NN is in 2..64 range. | ||
553 | */ | ||
554 | base = (unsigned)n; | ||
555 | n = 0; | ||
556 | nptr = *endptr + 1; | ||
557 | for (;;) { | 539 | for (;;) { |
558 | unsigned digit = (unsigned)*nptr - '0'; | 540 | unsigned digit = (unsigned)*nptr - '0'; |
559 | if (digit >= 10 /* not 0..9 */ | 541 | if (digit >= 10 /* not 0..9 */ |
@@ -582,15 +564,52 @@ static arith_t strto_arith_t(const char *nptr, char **endptr) | |||
582 | n = n * base + digit; | 564 | n = n * base + digit; |
583 | nptr++; | 565 | nptr++; |
584 | } | 566 | } |
585 | /* Note: we do not set errno on bad chars, we just set a pointer | ||
586 | * to the first invalid char. For example, this allows | ||
587 | * "N#" (empty "nnnn" part): 64#+1 is a valid expression, | ||
588 | * it means 64# + 1, whereas 64#~... is not, since ~ is not a valid | ||
589 | * operator. | ||
590 | */ | ||
591 | *endptr = (char*)nptr; | 567 | *endptr = (char*)nptr; |
568 | /* "64#" and "64#+1" used to be valid expressions, but bash 5.2.15 | ||
569 | * no longer allow such, detect this: | ||
570 | */ | ||
571 | // NB: bash allows $((0x)), this is probably a bug... | ||
572 | if (nptr == start) | ||
573 | *endptr = NULL; /* there weren't any digits, bad */ | ||
592 | return n; | 574 | return n; |
593 | } | 575 | } |
576 | |||
577 | static arith_t strto_arith_t(const char *nptr, char **endptr) | ||
578 | { | ||
579 | /* NB: we do not use strtoull here to be bash-compatible: | ||
580 | * $((99999999999999999999)) is 7766279631452241919 | ||
581 | * (the 64-bit truncated value). | ||
582 | */ | ||
583 | unsigned base; | ||
584 | |||
585 | /* nptr[0] is '0'..'9' here */ | ||
586 | |||
587 | base = nptr[0] - '0'; | ||
588 | if (base == 0) { /* nptr[0] is '0' */ | ||
589 | base = 8; | ||
590 | if ((nptr[1] | 0x20) == 'x') { | ||
591 | base = 16; | ||
592 | nptr += 2; | ||
593 | } | ||
594 | // NB: bash allows $((0x)), this is probably a bug... | ||
595 | return parse_with_base(nptr, endptr, base); | ||
596 | } | ||
597 | |||
598 | if (nptr[1] == '#') { | ||
599 | if (base > 1) | ||
600 | return parse_with_base(nptr + 2, endptr, base); | ||
601 | /* else: bash says "invalid arithmetic base" */ | ||
602 | } | ||
603 | |||
604 | if (isdigit(nptr[1]) && nptr[2] == '#') { | ||
605 | base = 10 * base + (nptr[1] - '0'); | ||
606 | if (base >= 2 && base <= 64) | ||
607 | return parse_with_base(nptr + 3, endptr, base); | ||
608 | /* else: bash says "invalid arithmetic base" */ | ||
609 | } | ||
610 | |||
611 | return parse_with_base(nptr, endptr, 10); | ||
612 | } | ||
594 | #else /* !ENABLE_FEATURE_SH_MATH_BASE */ | 613 | #else /* !ENABLE_FEATURE_SH_MATH_BASE */ |
595 | # if ENABLE_FEATURE_SH_MATH_64 | 614 | # if ENABLE_FEATURE_SH_MATH_64 |
596 | # define strto_arith_t(nptr, endptr) strtoull(nptr, endptr, 0) | 615 | # define strto_arith_t(nptr, endptr) strtoull(nptr, endptr, 0) |
@@ -702,7 +721,6 @@ evaluate_string(arith_state_t *math_state, const char *expr) | |||
702 | dbg("[%d] var:IGNORED", (int)(numstackptr - numstack)); | 721 | dbg("[%d] var:IGNORED", (int)(numstackptr - numstack)); |
703 | expr = p; | 722 | expr = p; |
704 | numstackptr->var_name = NULL; | 723 | numstackptr->var_name = NULL; |
705 | push_num0: | ||
706 | numstackptr->val = 0; | 724 | numstackptr->val = 0; |
707 | } | 725 | } |
708 | push_num: | 726 | push_num: |
@@ -715,11 +733,12 @@ evaluate_string(arith_state_t *math_state, const char *expr) | |||
715 | /* Number */ | 733 | /* Number */ |
716 | char *end; | 734 | char *end; |
717 | numstackptr->var_name = NULL; | 735 | numstackptr->var_name = NULL; |
718 | errno = 0; | ||
719 | /* code is smaller compared to using &expr here: */ | 736 | /* code is smaller compared to using &expr here: */ |
720 | numstackptr->val = strto_arith_t(expr, &end); | 737 | numstackptr->val = strto_arith_t(expr, &end); |
721 | expr = end; | 738 | expr = end; |
722 | dbg("[%d] val:%lld", (int)(numstackptr - numstack), numstackptr->val); | 739 | dbg("[%d] val:%lld", (int)(numstackptr - numstack), numstackptr->val); |
740 | if (!expr) /* example: $((10#)) */ | ||
741 | goto syntax_err; | ||
723 | /* A number can't be followed by another number, or a variable name. | 742 | /* A number can't be followed by another number, or a variable name. |
724 | * We'd catch this later anyway, but this would require numstack[] | 743 | * We'd catch this later anyway, but this would require numstack[] |
725 | * to be ~twice as deep to handle strings where _every_ char is | 744 | * to be ~twice as deep to handle strings where _every_ char is |
@@ -728,13 +747,6 @@ evaluate_string(arith_state_t *math_state, const char *expr) | |||
728 | */ | 747 | */ |
729 | if (isalnum(*expr) || *expr == '_') | 748 | if (isalnum(*expr) || *expr == '_') |
730 | goto syntax_err; | 749 | goto syntax_err; |
731 | if (errno) { | ||
732 | // TODO: bash 5.2.15 does not catch ERANGE (some older version did?). | ||
733 | // $((99999999999999999999)) is 7766279631452241919 (the 64-bit truncated value). | ||
734 | // Our BASE# code does this as well: try $((10#99999999999999999999)), | ||
735 | // but the "ordinary" code path with strtoull() does not do this. | ||
736 | goto push_num0; /* bash compat */ | ||
737 | } | ||
738 | goto push_num; | 750 | goto push_num; |
739 | } | 751 | } |
740 | 752 | ||