diff --git a/CHANGELOG.md b/CHANGELOG.md index ff6f2b85..3d264dbb 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/GOALS.md b/GOALS.md index 2b820189..8fedbbaa 100644 --- a/GOALS.md +++ b/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 diff --git a/goals/G-120-scriptmode-std-channel-pin.md b/goals/G-120-scriptmode-std-channel-pin.md index 05c05c9b..3bbc148e 100644 --- a/goals/G-120-scriptmode-std-channel-pin.md +++ b/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 diff --git a/punkproject.toml b/punkproject.toml index faa4c7d2..e2d08578 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,4 +1,4 @@ [project] name = "punkshell" -version = "0.22.0" +version = "0.22.1" license = "BSD-2-Clause" diff --git a/src/modules/punk/libunknown-0.2.1.tm b/src/modules/punk/libunknown-0.2.2.tm similarity index 97% rename from src/modules/punk/libunknown-0.2.1.tm rename to src/modules/punk/libunknown-0.2.2.tm index 84d84eb8..471c5e0e 100644 --- a/src/modules/punk/libunknown-0.2.1.tm +++ b/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