Browse Source

G-120 active: libunknown 0.2.2 fixes leaked static-probe interp pinning std channels

Activation: proposed->active flip (user-confirmed), goals_xref score re-run
recorded in detail Notes (nothing beyond the drafting survey).

Holder named: punk::libunknown zipfs_tm_UnknownHandler's builtin-static
shortcircuit leaked its version-probe child interp 'ptest' whenever an
UNVERSIONED package require of a true static reached the handler -
'package vsatisfies $v' with zero requirement args throws wrong#args,
skipping both interp-delete lines. The leaked non-safe child holds
registrations on the process std channels, so 'chan close stderr' left the
OS handle alive and the slot occupied; the next open got a fileN name and
puts stderr failed (punk91/tclsfe-9.1b0 only: Thread is a true static there,
punk905 loads it from a dll so the block never fires).

Fix (0.2.2): auto-named probe interp deleted in try/finally; zero-requirement
requires accept any provided static version; probe errors fall through to the
normal tm search. Disposition recorded in the goal detail: leak, fixed - not
console-ownership machinery.

Verified: bin/punk91 src script recipe yields channel auto-named stderr with
working puts stderr; libunknown+console+repl suites 105/105 pass (tclsh90).
punkproject 0.22.1 + CHANGELOG.

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 3 days ago
parent
commit
5d207dbfb6
  1. 13
      CHANGELOG.md
  2. 2
      GOALS.md
  3. 38
      goals/G-120-scriptmode-std-channel-pin.md
  4. 2
      punkproject.toml
  5. 61
      src/modules/punk/libunknown-0.2.2.tm

13
CHANGELOG.md

@ -5,6 +5,19 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to
Entries are newest-first; one bullet per notable change. See the root `AGENTS.md`
"Project Versioning" section for the bump policy.
## [0.22.1] - 2026-07-25
- punk::libunknown 0.2.2 (G-120): the package-unknown static-builtin shortcircuit no longer
leaks its version-probe child interp. On kits whose runtime provides Thread (or any
required package) as a true static (e.g the tclsfe 9.1b0 runtime behind punk91), an
unversioned `package require` made the probe's `package vsatisfies` throw (it requires at
least one requirement argument), skipping the interp delete; the leaked child's std-channel
registrations then disabled Tcl's std-slot recycling process-wide - after
`chan close stderr` the slot stayed occupied and the next opened channel got a fileN name,
so `puts stderr` failed. The probe interp is now auto-named and deleted in a try/finally,
zero-requirement requires accept any provided static version, and probe errors fall
through to the normal tm search instead of aborting the require.
## [0.22.0] - 2026-07-25
- punk-runtime `list -remote` identity columns (G-119 follow-on; both payloads + rewrapped

2
GOALS.md

@ -402,6 +402,6 @@ Detail: goals/G-115-declarative-vfs-composition.md
Scope: src/buildsuites/suite_tcl90/ (build_tcltls module + crypto-backend build, sources.config + build.zig.zon source records, kit-family bi payload + metadata extension, test_gate record step); src/vfs kit payloads carrying vendored tcltls binaries (current-state reference + recorded disposition only - removal stays G-004-era work); punkbin (as eventual artifact destination via the G-067 library class); consumers punk::imap4 / punk::netbox (package require tls) as verification context
Detail: goals/G-116-suite-built-tcltls.md
### G-120 [proposed] 9.1-kit script-mode boot pins process std channels: find holder, restore std-slot recycling
### G-120 [active] 9.1-kit script-mode boot pins process std channels: find holder, restore std-slot recycling
Scope: src/modules/punk/console-999999.0a1.0.tm; src/modules/punk/repl-999999.0a1.0.tm; src/lib/app-punkscript/; src/vfs/_config/punk_main.tcl
Detail: goals/G-120-scriptmode-std-channel-pin.md

38
goals/G-120-scriptmode-std-channel-pin.md

@ -1,6 +1,6 @@
# G-120 9.1-kit script-mode boot pins process std channels: find holder, restore std-slot recycling
Status: proposed
Status: active
Scope: src/modules/punk/console-999999.0a1.0.tm; src/modules/punk/repl-999999.0a1.0.tm; src/lib/app-punkscript/; src/vfs/_config/punk_main.tcl
Goal: identify what in the 9.1-kit script-mode boot path retains a registration on the process std channels (keeping their std slots occupied and their OS handles alive across an explicit chan close), determine whether that retention is deliberate console-ownership machinery or a leak, and land the recorded disposition so that closing a std channel frees its slot - restoring Tcl's std-channel recycling - without breaking console abstraction behaviour.
Acceptance: the registration holder is named (module, proc, mechanism) with reproduction evidence; the disposition is recorded here (deliberate-and-documented, or fixed); a script-mode run of the recipe `chan close stderr; open NUL w` on the current 9.1 kit yields a channel auto-named stderr with working puts stderr, and the console + repl module suites (src/tests/modules/punk/console/, src/tests/modules/punk/repl/) pass.
@ -46,6 +46,38 @@ by absence: strip the suspected stage and re-run the recipe.
## Notes
- HOLDER (named 2026-07-25): punk::libunknown 0.2.1, proc
`zipfs_tm_UnknownHandler`, builtin-static shortcircuit block - the static
version-probe child interp (fixed name `ptest`) leaked when
`package vsatisfies $static_version {*}$args` threw `wrong # args` on an
UNVERSIONED require (zero requirement args) between `interp create` and
`interp delete`. A non-safe child interp holds registrations on the process
std channels (Tcl shares stdin/stdout/stderr into it at creation), so the
leaked child kept the closed stderr's OS handle alive and its std slot
occupied - Tcl_CreateChannel's std-slot install never fires.
- Reproduction evidence (2026-07-25, bin/punk91.exe 9.1b0 script mode):
`interp children` -> `ptest` with `chan names` = stderr stdin stdout and
`info loaded {}` = {{} Thread}; recipe yielded `file216195028c0` and
"can not find channel named stderr". Same-process `interp delete ptest`
then recipe -> channel auto-named stderr, puts stderr OK (causality proven).
Post-boot re-trigger: `package forget thread; package require thread`
recreated the leak on demand (`package require dde` likewise, while
SUCCEEDING via the fallback path); step-through isolated
`package vsatisfies 3.0.5` (zero requirements) as the throwing call -
identical arity error on 9.0.5 and 9.1b0, so the punk91-only symptom is
runtime-payload-driven: tclsfe 9.1b0 provides Thread as a true static
(`{}` prefix in `info loaded`, hit by boot's unversioned require), while
punk905 loads Thread from a dll path (`//zipfs:/app/bin/tcl9thread301.dll`)
so the shortcircuit never fires there.
- DISPOSITION: leak (bug), FIXED - not deliberate console-ownership machinery
(the G-007 ownership registry was not involved; the Context ruled-out list
stands). Fix = punk::libunknown 0.2.2: auto-named probe interp deleted in a
try/finally, zero-requirement requires accept any provided static version,
probe errors fall through to the normal tm search instead of aborting the
require. Verified under `bin/punk91 src script`: no child interp after boot,
recipe yields a channel auto-named stderr with working puts stderr. Suites:
libunknown + console + repl (12 files, 105 tests) all pass under native
tclsh90 (Tcl 9.0.3).
- Related: G-118 (achieved 2026-07-25 - see goals/archive/G-118-tclsh-subcommand-review.md;
console-path machinery + piperepl residue territory; its Notes
record punk91's runtime as unpatched, ruling out the patch as the holder); G-011
@ -54,6 +86,10 @@ by absence: strip the suspected stage and re-run the recipe.
channel-lifetime territory); archived G-007
(goals/archive/G-007-console-location-transparency.md - the console ownership
registry lineage the holder most likely belongs to).
- Activation freshness 2026-07-25: goals_xref.tcl score G-120 re-run at the
proposed->active flip - no relationships beyond the drafting survey below (top
unlinked hits G-013/G-044/G-008/G-043 are the already-dismissed repl-feature
module-path sharers; no goals drafted in the interval).
- Overlap survey 2026-07-25 (goals_xref.tcl paths over the scope): surfaced
G-002/003/008/009/010/011/013/014/034/038/042/043/044 (punk/repl + punk/console
module-path sharing - repl-feature goals, no substantive pin relationship), G-077

2
punkproject.toml

@ -1,4 +1,4 @@
[project]
name = "punkshell"
version = "0.22.0"
version = "0.22.1"
license = "BSD-2-Clause"

61
src/modules/punk/libunknown-0.2.1.tm → src/modules/punk/libunknown-0.2.2.tm

@ -7,7 +7,7 @@
# (C) 2025
#
# @@ Meta Begin
# Application punk::libunknown 0.2.1
# Application punk::libunknown 0.2.2
# Meta platform tcl
# Meta license MIT
# @@ Meta End
@ -18,6 +18,19 @@
# from src/modules/AGENTS.md "Versioning And Releases" - bumping means
# renaming the file AND updating the Meta line above, the manpage_begin line
# below and the provide-block version, then appending a line here):
#0.2.2 - zipfs_tm_UnknownHandler: the builtin-static shortcircuit probe interp no
# longer leaks (G-120). The probe child interp was created under the fixed
# name ptest and deleted only on the non-error paths; an unversioned
# 'package require foo' of a static package made the version check
# 'package vsatisfies $v' throw (wrong # args - vsatisfies needs >=1
# requirement), skipping the delete. The leaked child held registrations
# on the process std channels (Tcl shares them into non-safe child
# interps), so an explicit 'chan close stderr' left the OS handle alive
# and the std slot occupied - the next opened channel got a fileN name
# instead of recycling into stderr. Now: auto-named probe interp, deleted
# in a try/finally; zero-requirement requires treat any provided version
# as satisfying; probe errors fall through to the normal tm search
# instead of aborting the require.
#0.2.1 - zipfs_tm_UnknownHandler: the zipfs static-path can_skip_update state is
# written keyed by the SPECIFIC searchpath actually scanned (tm path +
# package namespace subdir) to match its reader and libunknown::init's
@ -42,7 +55,7 @@
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin punkshell_module_punk::libunknown 0 0.2.1]
#[manpage_begin punkshell_module_punk::libunknown 0 0.2.2]
#[copyright "2025"]
#[titledesc {Module API}] [comment {-- Name section and table of contents description --}]
#[moddesc {-}] [comment {-- Description at end of page heading --}]
@ -190,22 +203,36 @@ tcl::namespace::eval ::punk::libunknown {
set loaded [lsearch -inline -index 1 -nocase [info loaded] $name]
if {[llength $loaded] == 2 && [lindex $loaded 0] eq ""} {
lassign $loaded _ cased_name
interp create ptest
ptest eval [list load {} $cased_name]
set static_version [ptest eval [list package provide [string tolower $cased_name]]]
set pname [string tolower $cased_name]
if {$static_version eq ""} {
set static_version [ptest eval [list package provide $cased_name]]
set pname $cased_name
}
if {$static_version ne ""} {
if {[package vsatisfies $static_version {*}$args]} {
package ifneeded $pname $static_version [list load {} $cased_name]
interp delete ptest
return
#Version-probe the static package in a throwaway child interp.
#The child must never outlive this block: a (non-safe) child interp
#holds registrations on the process std channels, so a leaked probe
#interp disables Tcl's std-slot recycling process-wide (after
#'chan close stderr' the OS handle stays alive and the slot stays
#occupied, so the next opened channel cannot become stderr) - G-120.
set probeinterp [interp create]
try {
interp eval $probeinterp [list load {} $cased_name]
set static_version [interp eval $probeinterp [list package provide [string tolower $cased_name]]]
set pname [string tolower $cased_name]
if {$static_version eq ""} {
set static_version [interp eval $probeinterp [list package provide $cased_name]]
set pname $cased_name
}
if {$static_version ne ""} {
#zero requirements (plain 'package require foo') accepts any
#provided version; package vsatisfies errors without at least
#one requirement argument.
if {![llength $args] || [package vsatisfies $static_version {*}$args]} {
package ifneeded $pname $static_version [list load {} $cased_name]
return
}
}
} on error {} {
#probe failure (e.g a static whose init needs more than a bare
#child interp) - not fatal: fall through to the normal tm search.
} finally {
interp delete $probeinterp
}
interp delete ptest
}
#------------------------------
@ -2130,7 +2157,7 @@ namespace eval ::punk::args::register {
package provide punk::libunknown [tcl::namespace::eval ::punk::libunknown {
variable pkg punk::libunknown
variable version
set version 0.2.1
set version 0.2.2
}]
return
Loading…
Cancel
Save