Index: TODO =================================================================== diff -u -rf3407d8d1674a3018603edb8c367d717441b72a2 -r5bdff1d1c1174aad3ee37081292a6ad8d8baa619 --- TODO (.../TODO) (revision f3407d8d1674a3018603edb8c367d717441b72a2) +++ TODO (.../TODO) (revision 5bdff1d1c1174aad3ee37081292a6ad8d8baa619) @@ -2277,6 +2277,13 @@ - nx.tcl: needsForwarder is true, when method "get" is specified on a slot - nx.tcl: Don't generate per-slot add/assign handlers, when not needed +- nsf.c: fixed a nasty bug within namespace deletion, when a deletion + of one tcl cmd caused implicit deletions of other cmds in the same + namespace. The classical idiom for looking over hash tables with + Tcl_GetHashValue() and Tcl_NextHashEntry(hSrch) can lead to crashes + (and has different behavior depending on the number of buckets). +- added regression test + TODO: - object parameter type forward: Index: generic/nsf.c =================================================================== diff -u -rbd1cce484140aaf66113cf647f060ae48d32b24f -r5bdff1d1c1174aad3ee37081292a6ad8d8baa619 --- generic/nsf.c (.../nsf.c) (revision bd1cce484140aaf66113cf647f060ae48d32b24f) +++ generic/nsf.c (.../nsf.c) (revision 5bdff1d1c1174aad3ee37081292a6ad8d8baa619) @@ -914,7 +914,6 @@ assert(object->flags & NSF_DELETED); MEM_COUNT_FREE("NsfObject/NsfClass", object); - #if defined(NSFOBJ_TRACE) fprintf(stderr, "CKFREE Object %p refcount=%d\n", object, object->refCount); #endif @@ -3269,10 +3268,16 @@ static void NSDeleteChild(Tcl_Interp *interp, Tcl_Command cmd, int deleteObjectsOnly) { - /*fprintf(stderr, "NSDeleteChildren child %p (%s) epoch %d\n", - cmd, Tcl_GetCommandName(interp, cmd), Tcl_Command_cmdEpoch(cmd));*/ + /*fprintf(stderr, "NSDeleteChildren child flags %.6x\n", Tcl_Command_flags(cmd)); + fprintf(stderr, "NSDeleteChildren child %p (%s) epoch %d\n", + cmd, Tcl_GetCommandName(interp, cmd), Tcl_Command_cmdEpoch(cmd));*/ - assert(Tcl_Command_cmdEpoch(cmd) == 0); + /* + * In some situations (e.g. small buckets, less than 12 entries), we + * get from the cmd-table already deleted cmds; we had previously an + * assert(Tcl_Command_cmdEpoch(cmd) == 0); + * which will fail in such cases. + */ if (!Tcl_Command_cmdEpoch(cmd)) { NsfObject *object = NsfGetObjectFromCmdPtr(cmd); @@ -3348,30 +3353,70 @@ Tcl_HashSearch hSrch; Tcl_HashEntry *hPtr; - #ifdef OBJDELETION_TRACE - fprintf(stderr, "NSDeleteChildren %p %s\n", nsPtr, nsPtr->fullName); + fprintf(stderr, "NSDeleteChildren %p %s activationCount %d\n", + nsPtr, nsPtr->fullName, Tcl_Namespace_activationCount(nsPtr)); #endif /* * First, get rid of namespace imported objects; don't delete the * object, but the reference. */ Tcl_ForgetImport(interp, nsPtr, "*"); /* don't destroy namespace imported objects */ - + + +#if OBJDELETION_TRACE /* + * Deletion is always tricky. Show, what elements should be deleted + * in this loop. The actually deleted elements might be acutally + * less, if a deletion of one item triggers the destroy of another + * item. + */ + for (hPtr = Tcl_FirstHashEntry(cmdTablePtr, &hSrch); hPtr; + hPtr = Tcl_NextHashEntry(&hSrch)) { + Tcl_Command cmd = (Tcl_Command)Tcl_GetHashValue(hPtr); + fprintf(stderr, "will destroy %p %s\n", cmd, Tcl_GetCommandName(interp, cmd)); + } +#endif + /* * Second, delete the objects. */ for (hPtr = Tcl_FirstHashEntry(cmdTablePtr, &hSrch); hPtr; hPtr = Tcl_NextHashEntry(&hSrch)) { - NSDeleteChild(interp, (Tcl_Command)Tcl_GetHashValue(hPtr), 1); + /* Tcl_Command cmd = (Tcl_Command)Tcl_GetHashValue(hPtr); + fprintf(stderr, "NSDeleteChild %p table %p\n", cmd, hPtr->tablePtr);*/ + /* + * If a destroy of one element of the hash table triggers the + * destroy of another item, Tcl_NextHashEntry() can lead to a + * valid looking hPtr, when the next entry was already + * deleted. This seem to occur only, when there are more than 12 + * hash entries in the table (multiple buckets). However, the + * valid looking hPtr might return garbage (looks like + * uninitialized memory). Most probably Tcl_NextHashEntry() should + * return 0; + */ + if (hPtr->tablePtr) { + NSDeleteChild(interp, (Tcl_Command)Tcl_GetHashValue(hPtr), 1); + } else { + /* + * In this situation even Tcl_NextHashEntry() produces an + * invalid read, so assume, everything is deleted + */ + break; + } + /*fprintf(stderr, "after: hSrch->tablePtr %p hSrch->nextEntryPtr %p hSrch->nextIndex %d\n", + hSrch.tablePtr, hSrch.nextEntryPtr, hSrch.nextIndex);*/ } /* * Finally, delete the classes. */ for (hPtr = Tcl_FirstHashEntry(cmdTablePtr, &hSrch); hPtr; hPtr = Tcl_NextHashEntry(&hSrch)) { - NSDeleteChild(interp, (Tcl_Command)Tcl_GetHashValue(hPtr), 0); + if (hPtr->tablePtr) { + NSDeleteChild(interp, (Tcl_Command)Tcl_GetHashValue(hPtr), 0); + } else { + break; + } } } @@ -10813,8 +10858,10 @@ if (Tcl_InterpDeleted(interp)) return; #ifdef OBJDELETION_TRACE - fprintf(stderr, " physical delete of %p id=%p destroyCalled=%d '%s'\n", - object, object->id, (object->flags & NSF_DESTROY_CALLED), ObjectName(object)); + {Command *cmdPtr = object->id; + fprintf(stderr, " physical delete of %p id=%p (cmd->refCount %d) destroyCalled=%d '%s'\n", + object, object->id, cmdPtr->refCount, (object->flags & NSF_DESTROY_CALLED), ObjectName(object)); + } #endif CleanupDestroyObject(interp, object, 0); Index: generic/nsf.h =================================================================== diff -u -r28fd214e129bc6c2384a2ef587a2be8b480c7248 -r5bdff1d1c1174aad3ee37081292a6ad8d8baa619 --- generic/nsf.h (.../nsf.h) (revision 28fd214e129bc6c2384a2ef587a2be8b480c7248) +++ generic/nsf.h (.../nsf.h) (revision 5bdff1d1c1174aad3ee37081292a6ad8d8baa619) @@ -140,16 +140,16 @@ # define CHECK_ACTIVATION_COUNTS 1 # define NsfCleanupObject(object,string) \ /*fprintf(stderr, "NsfCleanupObject %p %s\n",object,string);*/ \ - NsfCleanupObject_(object); + NsfCleanupObject_(object) # define CscFinish(interp,cscPtr,string) \ /*fprintf(stderr, "CscFinish %p %s\n",cscPtr,string);*/ \ - CscFinish_(interp, cscPtr); + CscFinish_(interp, cscPtr) #else # define NDEBUG 1 # define NsfCleanupObject(object,string) \ - NsfCleanupObject_(object); + NsfCleanupObject_(object) # define CscFinish(interp,cscPtr,string) \ - CscFinish_(interp, cscPtr); + CscFinish_(interp, cscPtr) #endif #if !defined(CHECK_ACTIVATION_COUNTS) Index: tests/destroy.test =================================================================== diff -u -ref1f9efa0bc697404c0aa5322bbd5cc2d7796c2c -r5bdff1d1c1174aad3ee37081292a6ad8d8baa619 --- tests/destroy.test (.../destroy.test) (revision ef1f9efa0bc697404c0aa5322bbd5cc2d7796c2c) +++ tests/destroy.test (.../destroy.test) (revision 5bdff1d1c1174aad3ee37081292a6ad8d8baa619) @@ -677,4 +677,68 @@ ::nx::Object filter "" } +Test case nested-ordered-composite { + # The following test case an explicit deletion/redefinition of an + # toplevel object (o1) will cause the implicit deletion of a nested + # object o1::o2. The object o2 has as well several included objects, + # containing an "ordered composite". The deletion of the ordered + # compostite causes the (explicit) deletion of its siblings (all + # children of o1::o2). This is actually a stress test for the deletion + # of o2's namespace, since the loop over its children will be + # confronted with the deletion of indirectly deleted items (deleted by + # the deletion of the ordered composite). + + Class create C { + :attribute os + :public method destroy {} { + #puts stderr "[self] destroy ${:os}" + foreach o [${:os}] { + if {[::nsf::isobject $o]} { + #puts stderr "--D $o destroy" + $o destroy + } + next + } + } + } + # + # 10 siblings of oc1: + # deletion order in bucket: 8 4 10 9 5 1 6 2 oc1 7 3 + # oc1 deletes 7 and 3, fine + # ... loop might run into an epoched cmd -> might crash + # + + set c 0 + for {set i 0} {$i < 10} {incr i} { + set os [list] + for {set j 0} {$j < 10} {incr j} {lappend os ::o1::o2::[incr c]} + Object create ::o1 + Object create ::o1::o2 + foreach o $os {Object create $o} + C create ::o1::o2::oc1 -os $os + ? {llength [o1 info children]} 1 + ? {llength [o1::o2 info children]} 11 + } + + ### 20 siblings of oc1 (has to be >12): + # deletion order in bucket: 17 18 1 20 19 2 3 4 5 6 7 8 9 19 11 oc1 12 13 14 15 16 + # oc1 deletes 12 13 14 15 16 + # after destroy of oc1 + # a) NextHashEntry(hSearch) returns valid looking hPtr + # b) Tcl_GetHashValue(hPtr) returns garbage (uninitialized memory?) instead of cmd + # --> might crash + # + set c 0 + for {set i 0} {$i < 10} {incr i} { + set os [list] + for {set j 0} {$j < 20} {incr j} {lappend os ::o1::o2::[incr c]} + Object create ::o1 + Object create ::o1::o2 + foreach o $os {Object create $o} + C create ::o1::o2::oc1 -os $os + ? {llength [o1 info children]} 1 + ? {llength [o1::o2 info children]} 21 + } +} + #puts stderr "==== EXIT ===="