| Ticket UUID: | 8dd2807066d7cc4d8604164f39b7f0d65c0dc321 | ||
| Title: | bug in single-argument 'max' with bignums | ||
| Type: | Bug | Created on: | 2025-11-10 00:35:02 |
| Submitter: | emiliano | Assigned to: | jan.nijtmans |
| Subsystem: | 48. Number Handling | Severity: | Critical |
| Priority: | 5 Medium | Last modified: | 2025-11-14 10:04:37 |
| Status: | Closed | Closed by: | jan.nijtmans |
| Resolution: | Fixed | Closed on: | 2025-11-14 10:04:37 |
| Version: | 9 | ||
| Description: | ||||
The following code crash on 9.X
$ tclsh9.1
% expr {max(abs(-668336881543038127783364011867))}
alloc: invalid block: 0x5fcac4002780: 0 0
The same code works with 8.6
$ tclsh8.6
% expr {max(abs(-668336881543038127783364011867))}
668336881543038127783364011867
| ||||
| User Comments: | ||||
emiliano added on 2025-11-10 00:45:41:
gdb backtrace (linux x86_64)
Program received signal SIGILL, Illegal instruction.
Tcl_Panic (format=0x7ffff7e35c68 "alloc: invalid block: %p: %x %x %x")
at /home/emiliano/src/tcl/generic/tclPanic.c:108
108 __builtin_trap();
(gdb) bt
#0 Tcl_Panic (format=0x7ffff7e35c68 "alloc: invalid block: %p: %x %x %x")
at /home/emiliano/src/tcl/generic/tclPanic.c:108
#1 0x00007ffff7da4026 in Ptr2Block (ptr=0x5555556540f0)
at /home/emiliano/src/tcl/generic/tclThreadAlloc.c:811
#2 0x00007ffff7da33e7 in TclpFree (ptr=0x5555556540f0)
at /home/emiliano/src/tcl/generic/tclThreadAlloc.c:385
#3 0x00007ffff7c69c5b in Tcl_Free (ptr=0x5555556540f0)
at /home/emiliano/src/tcl/generic/tclCkalloc.c:1178
#4 0x00007ffff7e0ec59 in TclBN_mp_clear (a=0x7fffffffdac0)
at /home/emiliano/src/tcl/libtommath/bn_mp_clear.c:12
#5 0x00007ffff7d136a6 in TclCompareTwoNumbers (valuePtr=0x5555556012d0,
value2Ptr=0x5555556012d0) at /home/emiliano/src/tcl/generic/tclExecute.c:9265
#6 0x00007ffff7c5e9b9 in ExprMaxMinFunc (dummy7587=0x0,
interp=0x5555555a96e0, objc=2, objv=0x5555555ad790, op=1)
at /home/emiliano/src/tcl/generic/tclBasic.c:7617
#7 0x00007ffff7c5ea50 in ExprMaxFunc (dummy7628=0x0, interp=0x5555555a96e0,
objc=2, objv=0x5555555ad790) at /home/emiliano/src/tcl/generic/tclBasic.c:7634
#8 0x00007ffff7c58d43 in Dispatch (data=0x555555601368,
interp=0x5555555a96e0, dummy4602=0)
at /home/emiliano/src/tcl/generic/tclBasic.c:4641
#9 0x00007ffff7c58dae in TclNRRunCallbacks (interp=0x5555555a96e0, result=0,
rootPtr=0x0) at /home/emiliano/src/tcl/generic/tclBasic.c:4657
#10 0x00007ffff7c5bebc in TclEvalObjEx (interp=0x5555555a96e0,
objPtr=0x55555556ab10, flags=131072, invoker=0x0, word=0) at /home/emiliano/src/tcl/generic/tclBasic.c:6116
#11 0x00007ffff7c5be4c in Tcl_EvalObjEx (interp=0x5555555a96e0, objPtr=0x55555556ab10, flags=131072) at /home/emiliano/src/tcl/generic/tclBasic.c:6097
#12 0x00007ffff7d1f7b7 in Tcl_RecordAndEvalObj (interp=0x5555555a96e0, cmdPtr=0x55555556ab10, flags=131072) at /home/emiliano/src/tcl/generic/tclHistory.c:182
#13 0x00007ffff7d636b7 in Tcl_MainEx (argc=-1, argv=0x7fffffffdfc8, appInitProc=0x55555555523c <Tcl_AppInit>, interp=0x5555555a96e0)
at /home/emiliano/src/tcl/generic/tclMain.c:523
#14 0x000055555555523c in main (argc=1, argv=0x7fffffffdfc8) at /home/emiliano/src/tcl/unix/tclAppInit.c:98
apnadkarni added on 2025-11-10 06:32:55:
Proposed fix in branch bug-8dd28070 apnadkarni added on 2025-11-10 07:34:05:
While I have a proposed fix that checks for the specific failure mode when Review by someone more familiar with bignums (Jan?) would be beneficial. jan.nijtmans added on 2025-11-10 09:41:24:
I think the solution is more complicated than that. Tcl_GetNumberFromObj() documentation: * Side effects: * Can allocate thread-specific data for handling the copy-out space for * bignums; this space is shared within a thread. So you cannot call Tcl_GetNumberFromObj() twice in a row without copying the result in between ..... That looks to be the problem here. apnadkarni added on 2025-11-10 11:14:09:
Jan, Huh! How about this then? Still WIP because I have two questions. Would it not be safer to use Second, when comparing integers/doubles against bigints, the code only checks whether the sebres added on 2025-11-10 11:29:57:
I'm also with Jan here - it looks more complicated to me... Rather I cannot explain right now why this one doesn't generate BOOM (without the fix):
apnadkarni added on 2025-11-10 11:35:55:
> it looks more complicated to me. Sergey, did you mean my latest commit or the original proposed fix? apnadkarni added on 2025-11-10 11:38:32:
Sergey, as to why your example does not go boom, I would guess it is because the additional reference count from the variable reference x means Tcl_TakeBignumFromObj does not change the bignum internal rep because it is now shared. sebres added on 2025-11-10 11:41:47:
I meant it generally. I don't understand why a comparison of bignum (compared with itself) handles different... not to mention why `max` (with single argument) is compiled in this way. jan.nijtmans added on 2025-11-10 11:53:17:
> I think the solution is more complicated than that Some confusion here. I didn't say that Ashok's solution was too complicated. I intended to say that the correct solution should be more complicated that a simple if-statement. :-) emiliano added on 2025-11-10 13:21:44:
About Sergey's example not going kaboom "...because the additional reference count...", note this also crashes
$ tclsh9.1
% set a -668336881543038127783364011867
-668336881543038127783364011867
% expr {max(abs($a))}
alloc: invalid block: 0x5b63c9713780: 0 0
apnadkarni added on 2025-11-10 16:00:23:
I'm confused by what commit the comments here are referring to.
Does anyone see problems with the tip of the [bug-8dd28070] branch? It passes both the original report as well @emiliano's example.
Regarding Sergey's and Emiliano's query - in Sergey's case, the passed in valuePtr (== value2Ptr) have reference counts of 3. Does the Tcl_Obj is not emptied by Tcl_TakeBignumFromObj. In the original report and Emiliano's expr {max(abs($a))} example, the reference count is 1, so the object gets emptied on value1 resulting in a crash on value2 manipulation.
jan.nijtmans added on 2025-11-10 16:15:49:
> Does anyone see problems with the tip of the [bug-8dd28070] branch? My comment was directed at your first commit, adding the if() statement only. I didn't have a full look at your later commits, but it matches the direction I would it expect it to go. So, just go ahead! apnadkarni added on 2025-11-10 17:24:37:
I decided to go with the fix in [bug-8dd28070-alt] which simply replaces the Tcl_Take* calls with Tcl_Get* calls. It's less code changes and also more accurate a fix for the actual problem. Will merge once GH CI passes. sebres added on 2025-11-10 19:48:49:
Well, it is basically a nonsense that there is a public TAKE function which modify the object in this way, and still worse all that depending on objects refCount (only). I'm not against the release of the internal representation of object - normally a change of internal representation may indeed happen (even for shared objects, for instance if object shimmers), but then the string representation still contain original value, so if internal representation gets needed again, it'd be reconstructed again. Again, freeing of internal representation on some object without string representation is bad idea at all (regardless it is shared or not) and shall be always avoided. The emphasis is on always. Just because it modifies the object drastically and because it violates EIAS concept. Therefore this issue is just an aftereffect and may happen in other cases by usage of Tcl_TakeBignumFromObj() function (also on some extensions). I have an alternative solution for this:
jan.nijtmans added on 2025-11-10 20:09:27:
@sebres, I agree with your analysis. I think it's possible to construct a testcase demonstrating this cornercase, but even without such testcase I agree. What a teamwork! :-) I also agree with Ashok's proposal. The reason two GetNumberFromObj() calls after each-other do no harm in this case is because the returned value is not used any more. The actual value is later obtained by another Tcl_GetBignumFromObj() calls. So, +1 on all suggestions. jan.nijtmans added on 2025-11-10 21:00:10:
Hm .... wait a bit. Thinking more about it, Tcl_TakeBignumFromObj() should only be used when the Tcl_Obj is Tcl_DecrRefCount'ed immediately. In that case, the current implementation makes sense. So, if - ever - the string-representation is lost, it means Tcl_GetBignumFromObj() should have been used in stead. The bug in this ticket is an example of the wrong Tcl_TakeBignumFromObj(). The more I think about it, the more it makes sense. jan.nijtmans added on 2025-11-11 08:07:16:
Fixed [ce87baf3c7a1ecf1|here] Let's discuss the changes in Tcl_TakeBignumFromObj() further. apnadkarni added on 2025-11-11 09:57:03:
Jan,
That is not an appropriate fix. If you insist on keeping the use of FWIW, I still prefer to simply replace Take with Get instead and not require any changes at the call site in jan.nijtmans added on 2025-11-11 10:21:18:
> That is not an appropriate fix Well, it fixes the reported problem. But I don't intend to stop here, don't worry. Just give me some time ..... jan.nijtmans added on 2025-11-11 12:00:56:
In Tcl 8.6, it's intentional that TclCompareTwoNumbers is destructive: It is only used in the bytecode engine, when two numbers are popped of the stack, and the compare result is pushed back on the stack. As long as that's the only place it's used, that should be OK. On 9.0, the function is re-used for the max/min functions which were re-implemented in C. That's where the problem started. sebres added on 2025-11-11 12:10:32:
> Let's discuss the changes in Tcl_TakeBignumFromObj() further. Agree. > I still prefer to simply replace Take with Get instead and not require any changes at the call site in `ExprMaxMinFunc`. It'd be unneeded if Jan had merged the whole fix of me, but... > A new bignum has to be created in either case. Well, I think this is exactly that, what Jan trying to achieve or rather to assume with "the current implementation makes sense" - the hope is that bignum can be moved from object to bignumValue pointer if it is unshared (so it'd be not created)... But, IMHO it is not correct - even with refCounts 1 (or 0), one can't guarantee that the object will be unused hereafter (or as Jan wrote "Tcl_TakeBignumFromObj() should only be used when the Tcl_Obj is Tcl_DecrRefCount'ed immediately"). So, IMHO, it is not correct to rely on refCounts in public function (however I'm OK if it'd happen always and panic if shared). Just it is often the case that the objects supplied to In any case, I don't understand why we can not use the value of jan.nijtmans added on 2025-11-11 14:09:13:
> But, IMHO it is not correct - even with refCounts 1 (or 0), one can't guarantee that the object will be unused hereafter I'm trying to create a test-case, showing the remaining problem (if there is one), but I'm not getting very far. Something like:
$ tclsh9.0
% tcl::unsupported::representation [expr {max(9999999999999999999999999999+1,0)}]
value is a bignum with a refcount of 2, object pointer at 0xa0009c0c0, internal representation 0xa000dfc30:0x100002, no string representation
% puts [expr {max(9999999999999999999999999999+1,0)}]
10000000000000000000000000000
This works as expected. Any idea? sebres added on 2025-11-11 14:46:19:
Huh? Your assumption is not correct, you shall check the refCount for literal, not for expr:
and from tclsh it'd be always larger or equal 3 (literal in code, reference in history, reference in objv in command). So calling max it'd be always shared.
In original issue it was a result of jan.nijtmans added on 2025-11-11 15:38:11:
$ tclsh9.0
% tcl::unspupported::representation [expr {max(abs(-668336881543038127783364011867),0)}]
invalid command name "tcl::unspupported::representation"
% tcl::unsupported::representation [expr {max(abs(-668336881543038127783364011867),0)}]
value is a pure string with a refcount of 1, object pointer at 0xa000a02e0, string representation ""
Got it! Thanks! sebres added on 2025-11-12 22:08:55:
[780d2f3375e35f2c] illustrates what I meant with unneeded CoR (copy on read)...
This improves Additionally it is also 4x faster than with a copy:
I'd like to merge it later if no objections follow. apnadkarni added on 2025-11-13 03:56:41:
Sergey, your fix seems fine for the current implementation but I am uncomfortable with it for the following reason. The When callers of these routines read numeric values through the reported storage pointer, they are accessing memory that belongs to the Tcl library. The Tcl library has the power to overwrite or free this memory. The storage pointer reported by a call to Tcl_GetNumber or Tcl_GetNumberFromObj should not be used after the same thread has possibly returned control to the Tcl library. Thus, as documented, there is no guarantee that ptr1 is even valid anymore after the second call to But you and Jan are more competent to make that call; I'm just voicing my concern. sebres added on 2025-11-13 11:48:46:
> I can imagine a possibility that if an existing allocation from a previous call is too big for a smaller bignum, it is reallocated Yes, but it'd be the pointer stored in mp_int::dp, so quasi pointer in pointer (of ptr1/ptr2), whereas ptr1/ptr2 themselves always point to the same address in TSD (per thread). > In which case ptr1 may be != ptr2 but no longer even points to valid storage. No, it cannot. TSD is allocated once per thread and remains unchanged for every static anchor. If GetNumberFromObj returned different pointers one of them is not a bignum, if they are equal - both are bignum and the last win. This variant (well, a bit more complex) works on my side already dozen years without any issue. However I think it'd be better to backport more from there, e. g. yet another GetNumberFromObj to avoid getting bignum in TSD (just a performance thing) and then one could always use TclUnpackBignum for local variable for any constant accesses to bignum type. This way one could eliminate almost every Tcl_GetBignumFromObj/Tcl_TakeBignumFromObj invocation and improve the performance drastically. apnadkarni added on 2025-11-14 08:03:52:
Perhaps memory reallocation is not an issue but my general point was about risks related to changes in internal implementation of bignums. But as you probably know, I tend to be very conservative, in this case with regards to depending on module internals even within the Tcl core. So if you are comfortable with it, go ahead, I do not see any faults in the implementation. jan.nijtmans added on 2025-11-14 10:04:37:
I don't have a basic objection against @sebres' suggestion. A factor 4x seems worth it. Maybe an interal TclGetNumberFromObjEx() with points to a user-defined space (a union of int, double and mp_int) in stead of a Tcl-provided space would be a minor improvement. But that can be done later. It would be nice to have this improvement in Tcl 9.1a1, so +1 from me. | ||||
