View Ticket
Not logged in
Ticket UUID: b0f84119c8f040ed2cb4910e2aea33fc2ddc2d94
Title: TEBCresume(): undefined behavior for INST_LSHIFT
Type: Patch Version: 8.6.12
Submitter: chrstphrchvz Created on: 2021-12-16 17:22:45
Subsystem: 16. Commands A-H Assigned To: jan.nijtmans
Priority: 5 Medium Severity: Minor
Status: Closed Last Modified: 2021-12-18 14:56:19
Resolution: Fixed Closed By: jan.nijtmans
    Closed on: 2021-12-18 14:56:19
Description:

Yet another case where I do not observe Tcl behaving unexpectedly despite complaints from UBSan.

Example to trigger complaint from -fsanitize=shift-base (left shift by CHAR_BIT*sizeof(long)-1 causes signed integer overflow) and -fsanitize=signed-integer-overflow (2’s complement negation of LONG_MIN causes signed integer overflow) for sizeof(long)==4:

% expr {1<<0}
tcl/generic/tclExecute.c:6384:9: runtime error: left shift of 1 by 31 places cannot be represented in type 'long int'
tcl/generic/tclExecute.c:6384:5: runtime error: negation of -2147483648 cannot be represented in type 'long int'; cast to an unsigned type to negate this value to itself
1

Likewise for sizeof(long)==8:

% expr {1<<0}
tcl/generic/tclExecute.c:6384:9: runtime error: left shift of 1 by 63 places cannot be represented in type 'long'
tcl/generic/tclExecute.c:6384:5: runtime error: negation of -9223372036854775808 cannot be represented in type 'long'; cast to an unsigned type to negate this value to itself
1

Another example to trigger complaint from -fsanitize=shift-base (left-shifting negative signed integer is undefined behavior):

% expr {-1<<1}
tcl/generic/tclExecute.c:6385:17: runtime error: left shift of negative value -1
-2

Suggested fix for both of these is to use unsigned long literals and casts in a few places (this should avoid undefined behavior without changing actual behavior):

diff --git generic/tclExecute.c generic/tclExecute.c
index 8963472e5..19b0565 100644
--- generic/tclExecute.c
+++ generic/tclExecute.c
@@ -6381,8 +6381,8 @@ TEBCresume(
 
 		    if ((size_t) shift < CHAR_BIT*sizeof(long) && (l1 != 0)
 			    && !((l1>0 ? l1 : ~l1) &
-				-(1L<<(CHAR_BIT*sizeof(long) - 1 - shift)))) {
-			lResult = l1 << shift;
+				-(1UL<<(CHAR_BIT*sizeof(long) - 1 - shift)))) {
+			lResult = (unsigned long) l1 << shift;
 			goto longResultOfArithmetic;
 		    }
 		}

(I might also point out that l1 != 0 in this condition is always true because of the l1 == 0 check on line 6353, though I’m not sure whether removing it makes more sense than keeping it.)

User Comments: jan.nijtmans added on 2021-12-18 14:56:19:

Fix now merged to 8.6 and up. Thanks very much for the report and for the fix!


jan.nijtmans added on 2021-12-16 21:03:18:

Suggested fix committed [c8fd4fa71448bcf6|here]