Tcl Source Code

View Ticket
Login
2026-03-13
08:11 • Ticket [f7495f63c0] valgrind reports uninitialized memory access running the tes... artifact: f5553a9eef user: jan.nijtmans
08:00
Merge 9.0 - Revert commit [f752c1271a]. See ticket [f7495f63c0] check-in: 24012d5118 user: apnadkarni tags: trunk, main
03:20
Revert commit [f752c1271a]. See ticket [f7495f63c0] check-in: dfd8f12ec5 user: apnadkarni tags: core-9-0-branch
03:17 • Ticket [f7495f63c0] valgrind reports uninitialized memory access running the tes... artifact: be27c75d09 user: apnadkarni
2026-03-12
13:13 • Ticket [f7495f63c0]: 5 changes artifact: f70b70c594 user: sebres
10:42 • Ticket [f7495f63c0]: 5 changes artifact: c630e81d36 user: jan.nijtmans
10:28 • Ticket [f7495f63c0]: 5 changes artifact: 9b435e683e user: sebres
07:57 • Ticket [f7495f63c0]: 5 changes artifact: 5128228f8e user: jan.nijtmans
2026-03-11
20:26 • Ticket [f7495f63c0]: 5 changes artifact: 2503991997 user: sebres
16:37 • Ticket [f7495f63c0]: 5 changes artifact: 039c0b5142 user: jan.nijtmans
16:33 • Ticket [f7495f63c0]: 5 changes artifact: 1f4f953377 user: sebres
15:58 • Ticket [f7495f63c0]: 5 changes artifact: 1edc08a736 user: jan.nijtmans
15:40 • Ticket [f7495f63c0]: 5 changes artifact: d6a0f70292 user: sebres
10:45 • Ticket [f7495f63c0]: 5 changes artifact: 61ced01f29 user: jan.nijtmans
02:53 • Ticket [f7495f63c0]: 5 changes artifact: 049f5e5503 user: apnadkarni
2026-03-10
18:35 • Ticket [f7495f63c0]: 5 changes artifact: ba7ef13a54 user: jan.nijtmans
17:41 • Ticket [f7495f63c0]: 5 changes artifact: 18d0f8c023 user: apnadkarni
14:48
Revert 'type' field back to an int. In stead fix the Tcl_InitHashTable() call. See [f7495f63c0] check-in: f752c1271a user: jan.nijtmans tags: core-9-0-branch
2026-03-09
07:11 • Ticket [f7495f63c0] valgrind reports uninitialized memory access running the tes... artifact: dc539e183e user: jan.nijtmans
07:09
Fix [f7495f63c0]. Valgrind reported error in SetScriptLimitCallback check-in: 1b6b19bd55 user: jan.nijtmans tags: core-8-6-branch
2026-03-08
04:07 • Closed ticket [f7495f63c0]: valgrind reports uninitialized memory access running... artifact: 1f7802c6b6 user: apnadkarni
04:04
Merge 9.0. Fix [f7495f63c0]. Valgrind reported error in SetScriptLimitCallback check-in: 9f41398a11 user: apnadkarni tags: trunk, main
03:57
Fix [f7495f63c0]. Valgrind reported error in SetScriptLimitCallback check-in: 58acf5656f user: apnadkarni tags: core-9-0-branch
2026-03-06
11:22 • Ticket [f7495f63c0] valgrind reports uninitialized memory access running the tes... artifact: 8c4bd64611 user: apnadkarni
11:07
Fix [f7495f63c0]. Valgrind reported error in SetScriptLimitCallback closed check-in: edef37bbf6 user: apnadkarni tags: bug-f7495f63c0
10:47 • New ticket [f7495f63c0] valgrind reports uninitialized memory access running the... artifact: 5c7a4d5478 user: apnadkarni

Ticket UUID: f7495f63c01ea80069198ac183e1e0cc8873dabe
Title: valgrind reports uninitialized memory access running the test suite
Type: Bug Created on: 2026-03-06 10:47:53
Submitter: apnadkarni Assigned to: apnadkarni
Subsystem: - New Builtin Commands Severity: Severe
Priority: 5 Medium Last modified: 2026-03-13 08:11:02
Status: Closed Closed by: jan.nijtmans
Resolution: Fixed Closed on: 2026-03-13 08:11:02
Version: trunk
Description:
==269512== Use of uninitialised value of size 8
==269512==    at 0x497CFC8: Tcl_DeleteHashEntry (tclHash.c:414)
==269512==    by 0x49891F0: DeleteScriptLimitCallback (tclInterp.c:4185)
==269512==    by 0x4988D95: TclLimitRemoveAllHandlers (tclInterp.c:3790)
==269512==    by 0x48802A9: DeleteInterpProc (tclBasic.c:2029)
==269512==    by 0x49DC268: Tcl_EventuallyFree (tclPreserve.c:296)
==269512==    by 0x48800B7: Tcl_DeleteInterp (tclBasic.c:1929)
==269512==    by 0x498768E: ChildObjCmdDeleteProc (tclInterp.c:2743)
==269512==    by 0x4882C80: Tcl_DeleteCommandFromToken (tclBasic.c:3858)
==269512==    by 0x4983D35: NRInterpCmd (tclInterp.c:891)
==269512==    by 0x4883E8D: Dispatch (tclBasic.c:4732)
==269512==    by 0x4883EF1: TclNRRunCallbacks (tclBasic.c:4748)
==269512==    by 0x4883799: Tcl_EvalObjv (tclBasic.c:4486)
==269512==    by 0x4885F5D: TclEvalEx (tclBasic.c:5587)
==269512==    by 0x49AA7F7: Tcl_FSEvalFileEx (tclIOUtil.c:1793)
==269512==    by 0x49BC5AA: Tcl_MainEx (tclMain.c:408)
==269512==    by 0x400701: main (tclAppInit.c:98)
User Comments:
apnadkarni added on 2026-03-06 11:22:03:
Also present in core-9-0-branch since [24d8933cbd]. Proposed fix at [edef37bbf6].

The change of the ScriptLimitCallbackKey.type field from long to int introduced trailing pad bytes in a structure used as a key in hash tables. Uninitialized pad bytes are a no-no for structs used as hash keys.

Now changed to be intptr_t which should not result in pad bytes as it will always be same size as a pointer on all known platforms.

apnadkarni added on 2026-03-08 04:07:15:
Fixed in [58acf5656f].

jan.nijtmans added on 2026-03-09 07:11:51:

Good catch! Solution works.

However, if I may .....(Sorry!). Two remarks:

1) In Tcl 8.6, the "type" field still is a long, which is 32-bits on Windows 64. So, the same problem is there too.

2) I don't think the presence of the pad bytes is the problem. The hashtable initialization is! If you calculate the size as "sizeof(ScriptLimitCallbackKey)/sizeof(int))", the pad-bytes are counted as well, that's the real problem.

Fixed that [1b6b19bd5561efc0|here], for 8.6 too. Are you OK with forwarding this solution to 9.0 and 9.1 too?


apnadkarni added on 2026-03-10 17:41:04:

Sorry, I did not see your response earlier. It appears I am not getting mail notifications from bugs any more and I'm currently only sporadically tracking the tickets.

Yep, I know Windows also had the problem. But it was visible on Unix thanks to valgrind. I think if there were tests that forced collisions in the hash table, it might have shown up on both platforms.

I am uncomfortable with your solution for two reasons.

First, the C standard does not guarantee the 4 pad bytes appear as trailing bytes. They could appear before the type field (and no trailing pad) while still maintaining the alignment and field ordering guarantees made by the standard. And in that case your code would break.

Having said that, perhaps I'm being pedantic and no compiler does that in the real world.

Second, and more important, consider that with your code, the AllocArrayEntry in the hash module allocates 12 bytes for the key. However, the line

keyPtr = (ScriptLimitCallbackKey *)
		Tcl_GetHashKey(&iPtr->limit.callbacks, hashPtr);

casts the 12 byte allocation to a pointer to a 16 byte struct! I really do not like this kind of violation of compiler contracts no matter whether there is any practical effect.

So I would still prefer the fix I had proposed particularly as I don't see any particular advantage of your solution.


jan.nijtmans added on 2026-03-10 18:35:10:
> I don't see any particular advantage of your solution

The advantage of my solution is that the type field can be accessed with a 32-bit access, not with a 64-bit access, and all hash-key compares only have to compare 3 integers not 4! That's surely better. Hoping you see it now.

> First, the C standard does not guarantee the 4 pad bytes appear as trailing bytes.
Hmmm. You are wrong here. Padding always concerns trailing bytes!

> I really do not like this kind of violation of compiler contracts no matter whether there is any practical effect.
Sorry, wrong again. On a 64-bit platform, which requires 64-bit values to be properly aligned, all mallocs are aligned on 8-byte boundaries.

So, please don't invent problems which don't exist.

apnadkarni added on 2026-03-11 02:53:59:

Hmmm. You are wrong here. Padding always concerns trailing bytes!

Read the standard. There is no such requirement anywhere. The only thing the standard mandates is maintaining the order of fields in a struct and alignment of individual fields. The location and length of pad bytes is completely up to the compiler except that the padding cannot be at the beginning of the struct. But, as I said I'm being pedantic and may not matter in practice.

Sorry, wrong again. On a 64-bit platform, which requires 64-bit values to be properly aligned, all mallocs are aligned on 8-byte boundaries.

Your reference to malloc indicates you simply did not understand the issue. It has nothing to do with allocation strategy. It's a compiler expectation that (ScriptLimitCallbackKey *) will point to a location with sizeof(ScriptLimitCallbackKey) valid bytes, no matter how they were allocated, dynamically via malloc, on the stack, or static storage. What your code is doing is the equivalent of this:

char buffer[sizeof(ScriptLimitCallbackKey)-sizeof(int)];
ScriptLimitCallbackKey *key = (ScriptLimitCallbackKey *)buf;

And with regards to accessing 32-bits versus 64-bits, cost of memory and registry operations is still the same on 64-bit platforms. And if you think adding one less integer is an "optimization" for Tcl performance, I don't know what to say!

In any case, do as you wish. I'm not going to comment further.


jan.nijtmans added on 2026-03-11 10:45:12:

Noted! Thanks!


sebres added on 2026-03-11 15:40:45:

> It has nothing to do with allocation strategy.

I'm agree. However it is indirectly about allocation, see below...

> It's a compiler expectation that (ScriptLimitCallbackKey *) will point to a location with sizeof(ScriptLimitCallbackKey) valid bytes

Nope. The compiler expectation here is to allocate, compare, hash, whatever action with an array of int's, and although it allows so to handle structures in a hash-table, it doesn't do that using structures internally, because the hash-table API considers blocks of int's, nothing else.

So if you want to estimate the impact of the change of Jan, you shall rather consider it as array of ints everywhere. But!..

The fix of Jan may indeed introduce a problem indirectly, since AllocArrayEntry could allocate a block for hash-entry + 3 integers (12 bytes), it can indeed cause many issues at corner case (e. g. ckalloc returns last block matching the range of buffer). And in best case an UB, since casting a struct that requires 16 bytes (due to 4 bytes of trailing padding) to a memory block of only 12 bytes may lead to UB by an attempt to access the struct as a whole or to use it in certain operations.
Of course it depends, but in worse case it can be even out-of-bounds memory access (with SF or data corruption), BO on write (in assumed as padding memory), broken array pointer arithmetic etc.

So I am with Ashok here and it shall be rewritten (at least for 64-bit target).


jan.nijtmans added on 2026-03-11 15:58:45:

Wasting too much time in this kind of discussion, but ....

> ... may lead to UB by an attempt to access the struct as a whole or to use it in certain operations

The struct is _never_ accessed as a whole. Inside the Tcl_HashTable it is always accessed as 3 integers. Outside the Tcl_HashTables, there is no way to access the padding storage in the struct.

If there's something wrong, valgrind should be able to show that.

Sorry!


sebres added on 2026-03-11 16:33:13:

> Outside the Tcl_HashTables, there is no way to access the padding storage in the struct.

But there is it, right now, see for instance TclRemoveScriptLimitCallbacks.

Don't forget that optimization is compiler related thing and... Just imagine it'd write a byte code using 64-bit instruction to load keyPtr->type to 64-bit register (in order to compare its lower 32-bits later)... What would happen if this would be on range of allocated memory?

Also it may do that later by some changes if we'd get more of such casts. Much worse if it'd be the write case.

I know that it is broken by design, but lets not make it more faulty as it is.


jan.nijtmans added on 2026-03-11 16:37:29:
> But there is it, right now, see for instance TclRemoveScriptLimitCallbacks.

I see keyPtr->interp and keyPtr->type being accessed, that's not the structure 'as a whole'.

sebres added on 2026-03-11 20:26:26:

Are you in mode to cherry-pick matching words, Jan?..

Also you seemed to miss "or to use it in certain operations" after "access the struct as a whole".

Anyway, because it is valid to optimize the code in the way I described above (if the compiler knows that after keyPtr->type there are still 4 bytes for padding), I can not exclude that by code like this (e. g. using ARM instructions like LDR, LDM or LDP) it would not cause SF by out-of-bounds memory access:

ldp x0, x1, [x2];  // load pair keyPtr->interp and keyPtr->type with single instruction
                ;  // thereby x0 contains pointer keyPtr->interp, 
                ;  //         w1 contains keyPtr->type as int (lower 32-bits word of x1)
...
bl Tcl_LimitRemoveHandler

Can you really swear that it won't happen?


jan.nijtmans added on 2026-03-12 07:57:42:

Well, just for the fun, I checked the assembler generated (both with gcc and clang on ARM64, maximum optimization -O3). It looked like:

        mov     x1, x0
        ldr     x0, [x0]
        ldr     w1, [x1, 8]
        b       Tcl_LimitRemoveHandler(void*, int)
Conclusion: the optimization you suggest, which I don't think is valid, isn't done by either gcc or clang. If - for the fun - the type of "type" is changed to ptrdiff, I get:
        ldp     x0, x1, [x0]
        b       Tcl_LimitRemoveHandler(void*, long)
so the compiler knows the ldp instruction.

I did another experiment, forcing 64-bit access in this exact place. The result was a crash in testcase interp-34.7. So whenever a compiler does this (in my view) invalid optimization, we have a testcase for it, which triggers consistently. I'm sure that, whenever a compiler introduces this optimization, Tcl will not be the only project broken. (It would not be the first time this happens)


sebres added on 2026-03-12 10:28:26:

OK, now I'm agree with Ashok - you don't understand what the issue is. If today the compiler doesn't optimize it using LDP instruction yet, it can do that later, because this will be not "broken" as you said, but fully legitimate thing - the compiler knows that the structure has a padding (basically it is 16 bytes large), so LDP instruction would be fully correct here. It is strange to push this on the fact that it is not currently in use. Maybe simply not yet? Or not in this optimization level, but within -O3 etc. Or simply by certain circumstances...

But what it surely unable to know, is that we had casted it to a 12-byte block of memory (since this allocation happens in another files). This is definitively an UB.

Even a known and still working UB remains UB, and it is weird to listen you decided to retain it... Not to mention you're trying to rely to some test coverage. Test coverage of the UB?! Really?

This is calling bomb-planting, Jan... And saying that I get dejavu feeling.


jan.nijtmans added on 2026-03-12 10:42:28:

So, let's agree we disagree then. You think this would be a valid (future) optimization. I think it won't be a valid optimization at all, since it would do read access of uninitialized padding bytes.

I wasted enough time on this discussion already.


sebres added on 2026-03-12 13:13:23:

You think this would be a valid (future) optimization. I think it won't be a valid optimization at all

It is not about "think", it is about "know". Because I know this.

The compiler is entitled to assume that an object of type struct occupies its full sizeof(struct) in memory. That's it.

it would do read access of uninitialized padding bytes

Jan, this sentence doesn't make sense at all. Why shall the padding be initialized? And what do you think the padding is for? The alignment (for faster CPU access) is only one purpose (and almost of little importance)...
The more purposes are:

  • to allow CPUs faster fetch data from memory in fixed-size chunks known as the "word size" (e.g. 8 bytes at a time).
  • hardware compatibility, since on some architectures accessing unaligned data is completely disallowed and can cause a hardware exception or crash. But in the same way there are architectures where the accessing of blocks smaller than "word size" is impossible.

Apropos alignment, who guarantees that the block retrieved by
(ScriptLimitCallbackKey *)Tcl_GetHashKey(&iPtr->limit.callbacks, hashPtr);
is properly aligned? If for instance the internal handling of allocation in hash table changes tomorrow and the buckets will be allocated as 10*12, e. g. as a chunk with 10 each 12 bytes blocks, so every 2nd block in such a chunk becomes misaligned.

Again, you disagree not with me, but with C-standard. Or rather with the fact that such cast can be considered as an UB. Particularly with this 3 paragraphs in the C-standard:

  1. Pointer Conversion and Alignment (6.3.2.3, p7)
  2. Accessing Beyond Object Boundaries (6.5.6, p8)
  3. Strict Aliasing (6.5, p7)

Some UBs may works years till some days they'd stop to work.


apnadkarni added on 2026-03-13 03:17:42:

Jan,

Sorry, I'm reverting your revert.

  • This is undefined behaviour as per the standard. Just the fact that the key pointer lands up pointing to a memory block that is smaller than sizeof(*key) should really suffice for this point but Sergey has articulated in detail.

  • Your experiment forcing a 64-bit crash actually further proves the point. And the excuse that interp-34.7 catches this case is completely meaningless. It catches one specific case using a specific compiler on a specific platform, there are other cases, code generators and platforms that it might not. Even if that were not so, what is the guarantee that we test with gcc 14 and at some user compiles with a newer gcc release? Claiming UB is ok because of test suite would catch failures is not logical.

  • As I said before, a single additional integer operation will make absolutely no difference to performance in any application. The fact that the code path is almost never ever exercised makes this "optimization" completely meaningless. So what makes you so adamant about this?

Finally, even if you don't change your opinion, please consider that in case of disagreements, it's reasonable that the majority opinion hold.

With apologies,

/Ashok


jan.nijtmans added on 2026-03-13 08:11:02:

apologies accepted :-)

The rule "better safe than sorry" applies here too. The performance difference between the two solutions is negligible anyway, it's not worth further discussion. I learned a lot (I never heard of the the ARM ldp instruction ...)