From 0018335e589dad38872588ffc7fa8ae5052585c0 Mon Sep 17 00:00:00 2001 From: Nakata Maho Date: Sat, 3 Oct 2026 08:45:34 +0900 Subject: [PATCH 1/9] kswap: register the segment and size environment backups at swap time (cherry picked from commit 2e76de3ddfb6fc7f794c3e466611ac621ef842ef) (cherry picked from commit bf4cd613683de8cf50ca769d5f15d92a9fc1d276) --- shell/kswap.c | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/shell/kswap.c b/shell/kswap.c index a8d559e3..576ace71 100644 --- a/shell/kswap.c +++ b/shell/kswap.c @@ -72,7 +72,7 @@ static void kswapSetISR(void) void kswapRegister(kswap_p ctxt) { IREGS r; - dprintf(("[KSWAP: Registering static context at: 0x%04x]\n", (word)ctxt)); + dprintf(("[KSWAP: Registering static context at: 0x%04x]\n", FP_SEG((void far *)ctxt))); assert(ctxt); /* our own PSP gets patched in order to load the values of the Criter and ^Break handlers of the context on termination @@ -83,7 +83,7 @@ void kswapRegister(kswap_p ctxt) kswapSetISR(); r.r_ax = 0x4bfd; /* Set kswap argument structure segm */ - r.r_bx = (word)ctxt; + r.r_bx = FP_SEG((void far *)ctxt); r.r_dx = FD_MAGIC; intrpt(0x21, &r); if(r.r_flags & 1) { /* failed */ @@ -91,22 +91,15 @@ void kswapRegister(kswap_p ctxt) dprintf(("[KSWAP: Registering failed, kernel swap deactivated]\n")); return; } - /* Initialize the constant values */ - /* environment block required only, if swapping is avail */ - ctxt->envSize = mcb_length(env_glbSeg); - ctxt->envSegm = allocSysBlk(ctxt->envSize, 0x82); - if(!ctxt->envSegm) { - error_kswap_allocmem(); - swapOnExec = ERROR; - } - else - dprintf(("[KSWAP: master environment allocated at 0x%04x]\n" - , ctxt->envSegm)); + /* Allocate the saved environment at swap time: initialization and SET + may still change its size after this registration. */ + ctxt->envSize = 0; + ctxt->envSegm = 0; } void kswapDeRegister(kswap_p ctxt) { if(swapOnExec != ERROR) { /* context belongs to this FreeCOM */ - dprintf(("[KSWAP: DeRegistering static context at: 0x%04x]\n", (word)ctxt)); + dprintf(("[KSWAP: DeRegistering static context at: 0x%04x]\n", FP_SEG((void far *)ctxt))); assert(ctxt); ctxt->shell = 0; /* causes the kernel swap support to exit */ } @@ -119,7 +112,7 @@ void kswapDeRegister(kswap_p ctxt) unsigned kswapMkStruc(const char * const prg, const char * const cmdline) { word shellname; - word segm; + word segm, envSize; char *q, *h; assert(prg); @@ -134,14 +127,26 @@ unsigned kswapMkStruc(const char * const prg, const char * const cmdline) swap after all */ /* preserve the environment */ - assert(kswapContext->envSegm); - assert(kswapContext->envSize); + envSize = env_glbSeg ? mcb_length(env_glbSeg) : 0; + if(!envSize) + return FALSE; segm = kswapContext->envSegm; + if(!segm || mcb_length(segm) < envSize) { + word replacement = allocSysBlk(envSize, forceLow ? 0x02 : 0x82); + if(!replacement) { + error_kswap_allocmem(); + return FALSE; /* Execute normally; never overrun the backup. */ + } + if(segm) + freeSysBlk(segm); + segm = kswapContext->envSegm = replacement; + } + kswapContext->envSize = envSize; dprintf(("[KSWAP: Updating master environment at 0x%04x]\n", segm)); assert(isMCB(SEG2MCB(segm))); assert(isMCB(SEG2MCB(env_glbSeg))); assert(mcb_length(env_glbSeg) <= mcb_length(segm)); - _fmemcpy(MK_FP(segm, 0), MK_FP(env_glbSeg, 0), mcb_length(env_glbSeg)); + _fmemcpy(MK_FP(segm, 0), MK_FP(env_glbSeg, 0), envSize); /* Update the shell name as maybe %COMSPEC% was changed */ /* COMSPEC is the central and traditionally the only place of the name of From 9aa366e2a6b651f7fb199bc41a40e7d859fdb737 Mon Sep 17 00:00:00 2001 From: Nakata Maho Date: Sat, 3 Oct 2026 08:51:26 +0900 Subject: [PATCH 2/9] kswap: preserve dynamic context ownership and counters across reload (cherry picked from commit f957976a53895dfb3005ab5d2faf1aa205f793f8) (cherry picked from commit 76f1aac62317107b4cf7e7d1acb79749b064a3ab) --- include/context.h | 1 + shell/kswap.c | 101 ++++++++++++++++++++++++++++++++++++++++++---- tools/kssf.asm | 7 ++++ 3 files changed, 102 insertions(+), 7 deletions(-) diff --git a/include/context.h b/include/context.h index c3531ddd..f006a6e2 100644 --- a/include/context.h +++ b/include/context.h @@ -36,6 +36,7 @@ enum { CTXT_SWAPINFO_PRGNAME = 1 , CTXT_SWAPINFO_CMDLINE , CTXT_SWAPINFO_SHELLNAME + , CTXT_SWAPINFO_STATUS }; #define ctxtAlias ctxt diff --git a/shell/kswap.c b/shell/kswap.c index 576ace71..9efd08c6 100644 --- a/shell/kswap.c +++ b/shell/kswap.c @@ -106,6 +106,64 @@ void kswapDeRegister(kswap_p ctxt) } +/* Context variables are strings. Encode the counters in hex, after reserving + the entry, so that the saved counters include the entry's own allocation. + This avoids reconstructing history/dirstack numbering from their contents. */ +#define KSWAP_INFO_BYTES (sizeof(ctxt_info_t) * (CTXT_TAG_ALIAS - CTXT_FIRST_TAG + 1)) +#define KSWAP_STATUS_BYTES (KSWAP_INFO_BYTES + 1) + +static int kswapSaveStatus(void) +{ + static const char hex[] = "0123456789abcdef"; + char reserve[2 * KSWAP_STATUS_BYTES + 1]; + unsigned i, value; + char far *p; + + memset(reserve, '0', sizeof(reserve) - 1); + reserve[sizeof(reserve) - 1] = 0; + if(ctxtSet(CTXT_TAG_SWAPINFO, CTXT_SWAPINFO_STATUS, reserve)) + return FALSE; + p = ctxtAddress(CTXT_TAG_SWAPINFO, CTXT_SWAPINFO_STATUS); + if(!p) + return FALSE; + for(i = 0; i < KSWAP_STATUS_BYTES; ++i) { + value = i < KSWAP_INFO_BYTES ? ((unsigned char *)ctxt_info)[i] + : (forceLow != 0); + *p++ = hex[value >> 4]; + *p++ = hex[value & 15]; + } + return TRUE; +} + +static int kswapRestoreStatus(void) +{ + unsigned char saved[KSWAP_STATUS_BYTES]; + unsigned i, value; + char c; + char far *p = ctxtAddress(CTXT_TAG_SWAPINFO, CTXT_SWAPINFO_STATUS); + + if(!p) + return FALSE; + for(i = 0; i < 2 * KSWAP_STATUS_BYTES; ++i) { + c = *p++; + if(c >= '0' && c <= '9') + value = c - '0'; + else if(c >= 'a' && c <= 'f') + value = c - 'a' + 10; + else + return FALSE; + if(!(i & 1)) + saved[i / 2] = value << 4; + else + saved[i / 2] |= value; + } + if(*p || saved[KSWAP_INFO_BYTES] > 1) + return FALSE; + memcpy(ctxt_info, saved, KSWAP_INFO_BYTES); + forceLow = saved[KSWAP_INFO_BYTES]; + return TRUE; +} + /* Update the kswap argument block Return: 0 on error <-> no swapping possible else: segment of structure */ @@ -113,6 +171,8 @@ unsigned kswapMkStruc(const char * const prg, const char * const cmdline) { word shellname; word segm, envSize; + int shellInContext; + struct MCB _seg *mcb; char *q, *h; assert(prg); @@ -151,8 +211,9 @@ unsigned kswapMkStruc(const char * const prg, const char * const cmdline) /* Update the shell name as maybe %COMSPEC% was changed */ /* COMSPEC is the central and traditionally the only place of the name of the shell */ - if(isSwapFile - || (shellname = env_findVar(segm, "COMSPEC") + 8) == (unsigned)-1 + 8) { + shellInContext = isSwapFile + || (shellname = env_findVar(segm, "COMSPEC") + 8) == (unsigned)-1 + 8; + if(shellInContext) { char *p = comResFile(); ctxtSet(CTXT_TAG_SWAPINFO, CTXT_SWAPINFO_SHELLNAME, p); free(p); @@ -188,10 +249,24 @@ unsigned kswapMkStruc(const char * const prg, const char * const cmdline) ctxtSet(CTXT_TAG_SWAPINFO, CTXT_SWAPINFO_CMDLINE, "\1 \r"); ctxtSet(CTXT_TAG_SWAPINFO, CTXT_SWAPINFO_PRGNAME, prg); - return (kswapContext->cmdline /* fetch first in case of failure */ - = ctxtAddress(CTXT_TAG_SWAPINFO, CTXT_SWAPINFO_CMDLINE)) != 0 - && (kswapContext->prg - = ctxtAddress(CTXT_TAG_SWAPINFO, CTXT_SWAPINFO_PRGNAME)) != 0; + if(!kswapSaveStatus()) + return FALSE; + /* Adding entries can relocate ctxt; take all pointers only afterwards. */ + if(shellInContext) + kswapContext->shell = ctxtAddress(CTXT_TAG_SWAPINFO, CTXT_SWAPINFO_SHELLNAME); + kswapContext->cmdline = ctxtAddress(CTXT_TAG_SWAPINFO, CTXT_SWAPINFO_CMDLINE); + if(!kswapContext->shell || !kswapContext->cmdline) + return FALSE; + kswapContext->prg = ctxtAddress(CTXT_TAG_SWAPINFO, CTXT_SWAPINFO_PRGNAME); + if(!kswapContext->prg) + return FALSE; + + /* The child and the reloaded shell need these strings and counters. + DOS must not release the context when this shell exits to KSSF. */ + mcb = MK_SEG_PTR(struct MCB, SEG2MCB(ctxt)); + mcb->mcb_ownerPSP = 8; + kswapContext->dyn_ctxt = ctxt; + return TRUE; } @@ -214,7 +289,19 @@ int kswapLoadStruc(void) fddebug = kswapContext->debug; #endif grabComFilename(1, kswapContext->shell); - if((ctxt = kswapContext->dyn_ctxt) == 0) { + ctxt = kswapContext->dyn_ctxt; + kswapContext->dyn_ctxt = 0; + if(ctxt) { + struct MCB _seg *mcb = MK_SEG_PTR(struct MCB, SEG2MCB(ctxt)); + /* While the shell runs, ordinary context relocation and DOS exit + own this allocation again. KSSF owns it only across the swap. */ + mcb->mcb_ownerPSP = _psp; + if(!kswapRestoreStatus()) { + freeBlk(ctxt); + ctxt = 0; + } + } + if(!ctxt) { error_no_context_after_swap(); ctxtCreate(); } diff --git a/tools/kssf.asm b/tools/kssf.asm index aec40efa..6de6f58b 100644 --- a/tools/kssf.asm +++ b/tools/kssf.asm @@ -136,6 +136,13 @@ dbg_1: mov es, ax ; MCB of context mov WORD [ES:1], ds ; owner of context := kssf mov ax, [ES:10h+?envSegm] ; environment segm + mov bx, [ES:10h+?dyn_ctxt] ; pending context if shell reload failed + or bx, bx + jz exit_noDynamic + dec bx + mov es, bx + mov WORD [ES:1], ds ; release it on KSSF termination, too +exit_noDynamic: or ax, ax jz exit_noCtxt dec ax From a5a1601889a06302cbd381d6b0567846d78520cf Mon Sep 17 00:00:00 2001 From: Nakata Maho Date: Sat, 3 Oct 2026 09:04:34 +0900 Subject: [PATCH 3/9] kssf: preserve the child DOS return code across shell reload (cherry picked from commit 820f994e3dc6625498d8922207dc4ebf4347b889) (cherry picked from commit f2bcfe7c69e99cd696761283126fe841a6c4d23d) --- tools/kssf.asm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/kssf.asm b/tools/kssf.asm index 6de6f58b..c01df0db 100644 --- a/tools/kssf.asm +++ b/tools/kssf.asm @@ -92,7 +92,9 @@ mainloop: mov ax, 4b00h int 21h ; execute external program jc execPrg1 - xor ax, ax ; no error + mov ah, 4dh ; collect the child's DOS return code before reload + int 21h + xor ah, ah execPrg1: mov sp, tsrend + STACK_SIZE ; standard stack position mov dx, cs From 8522bae460bf4cbea220e1a0a20d6f2c9da60c52 Mon Sep 17 00:00:00 2001 From: Nakata Maho Date: Sat, 3 Oct 2026 09:23:14 +0900 Subject: [PATCH 4/9] kswap: keep allocation-failure fallback independent of STRINGS loading (cherry picked from commit 376c5b70ae73187a8b76f4b250a02d8fc0c24da3) (cherry picked from commit 9ccd11cc2c5f0374ccdc9875ba5b8a4efc00b158) --- docs/k-swap.txt | 5 ++++- shell/kswap.c | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/k-swap.txt b/docs/k-swap.txt index 034b1130..3bc845cf 100644 --- a/docs/k-swap.txt +++ b/docs/k-swap.txt @@ -32,7 +32,10 @@ Swapping is NOT supported in: - called batch files accessing any shell functions or patch the master environment. -Swapping does not preserve the history. +The matching repaired FreeCOM/KSSF pair preserves the dynamic context, +including aliases and history, across a supported interactive swap. Build +and use both programs together; saved state is not a cross-version format. +See tests/kswap/README.md for the bounded regression and unqualified cases. === How to use KSSF? diff --git a/shell/kswap.c b/shell/kswap.c index 9efd08c6..b3e0dcad 100644 --- a/shell/kswap.c +++ b/shell/kswap.c @@ -194,7 +194,10 @@ unsigned kswapMkStruc(const char * const prg, const char * const cmdline) if(!segm || mcb_length(segm) < envSize) { word replacement = allocSysBlk(envSize, forceLow ? 0x02 : 0x82); if(!replacement) { - error_kswap_allocmem(); + /* Loading STRINGS can itself run out of memory and prompt for + another resource file. Keep this fallback diagnostic resident. */ + fputs("KSWAP: Not enough memory to save the environment; not swapping.\n", + stderr); return FALSE; /* Execute normally; never overrun the backup. */ } if(segm) From 11a6c8519a37dc326173e4046e6e98d76d19c4d4 Mon Sep 17 00:00:00 2001 From: Nakata Maho Date: Sat, 3 Oct 2026 17:53:31 +0900 Subject: [PATCH 5/9] kswap: emit the OOM fallback warning through the resident DOS writer (cherry picked from commit bb7037ab5c633d793775154045e94f2ffe569702) (cherry picked from commit 67dd684f6bb1f949c52111e2c1ca447f3b0bd72e) --- shell/kswap.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/shell/kswap.c b/shell/kswap.c index b3e0dcad..b30f90a5 100644 --- a/shell/kswap.c +++ b/shell/kswap.c @@ -194,10 +194,12 @@ unsigned kswapMkStruc(const char * const prg, const char * const cmdline) if(!segm || mcb_length(segm) < envSize) { word replacement = allocSysBlk(envSize, forceLow ? 0x02 : 0x82); if(!replacement) { + static const char message[] = + "KSWAP: Not enough memory to save the environment; not swapping.\r\n"; /* Loading STRINGS can itself run out of memory and prompt for - another resource file. Keep this fallback diagnostic resident. */ - fputs("KSWAP: Not enough memory to save the environment; not swapping.\n", - stderr); + another resource file. Also avoid CRT stdio: release FreeCOM + uses handle-valued FILE pointers, not CRT FILE structures. */ + dos_write(fileno(stderr), message, sizeof(message) - 1); return FALSE; /* Execute normally; never overrun the backup. */ } if(segm) From 78b875b7eaaa9623e20b3865c6090fc42ebe8f0e Mon Sep 17 00:00:00 2001 From: Nakata Maho Date: Sat, 3 Oct 2026 19:28:35 +0900 Subject: [PATCH 6/9] kswap: constrain the expanded reload branch to 8086 instructions (cherry picked from commit fd2b3eb5586ac94ec7624742497ac6ecd5c4926c) (cherry picked from commit 8cdffa3cb0cf40b1592ee1901ef6fd0a41c72885) --- tests/kswap/test_cpu_contract.py | 43 ++++++++++++++++++++++++++++++++ tools/kssf.asm | 1 + 2 files changed, 44 insertions(+) create mode 100644 tests/kswap/test_cpu_contract.py diff --git a/tests/kswap/test_cpu_contract.py b/tests/kswap/test_cpu_contract.py new file mode 100644 index 00000000..d8a2805e --- /dev/null +++ b/tests/kswap/test_cpu_contract.py @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +"""Assemble KSSF's actual reload span at its required 8086 ISA boundary.""" +from pathlib import Path +import re +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[2] + + +class CpuContractTests(unittest.TestCase): + def test_long_reload_branch_does_not_silently_require_a_386(self): + source = (ROOT / 'tools/kssf.asm').read_text() + cpu = re.search(r'^CPU\s+8086\b[^\n]*', source, re.MULTILINE) + self.assertIsNotNone(cpu, 'KSSF must explicitly constrain its instruction set') + span = source[source.index('mainloop:'):source.index('exit:', source.index('mainloop:'))] + # Symbolic data addresses stand in for the generated context interface; + # its operands retain their actual instruction widths. No guest artifact + # or saved generated header is an input to this boundary regression. + names = set(re.findall(r'\?[A-Za-z_]+', span)) | { + 'ctxt_owner', 'execBlock', 'tsrend', 'STACK_SIZE', 'context', + 'eb_envseg', 'eb_cmdline', 'shellCmdLine', + } + definitions = ''.join('%define ' + name + ' 512\n' for name in sorted(names)) + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + for directive in (cpu.group(0), ''): + assembly = 'BITS 16\n' + directive + '\n' + definitions + span + '\nexit:\nerrExecShell:\n' + (root / 'span.asm').write_text(assembly) + subprocess.run(['nasm', '-f', 'bin', str(root / 'span.asm'), + '-o', str(root / 'span.bin')], check=True) + binary = (root / 'span.bin').read_bytes() + if directive: + self.assertEqual(binary[-5:-2], b'\x74\x03\xe9') + else: + # Prove the original unconstrained span does cross rel8. + self.assertEqual(binary[-4:-2], b'\x0f\x85') + self.assertEqual(len(binary) + int.from_bytes(binary[-2:], 'little', signed=True), 0) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/kssf.asm b/tools/kssf.asm index c01df0db..2eacb05d 100644 --- a/tools/kssf.asm +++ b/tools/kssf.asm @@ -28,6 +28,7 @@ cmd_buf: SEGMENT .text +CPU 8086 ; expand out-of-range conditionals without 386-only opcodes ORG 100h ; this is an executable program, but as small as possible start: From 20b697c91f98c9e7887dd088607d24062a24f824 Mon Sep 17 00:00:00 2001 From: Nakata Maho Date: Sat, 3 Oct 2026 18:56:42 +0900 Subject: [PATCH 7/9] kswap: separate successful child status from EXEC errors and qualify diagnostics (cherry picked from commit 52dd9a9af1048ceecd147e100c740c9d3f9b2c70) (cherry picked from commit 9b3dedb1724397cd3dc1a02251761e49c346038b) --- criter/context.x | 3 ++- include/misc.h | 1 + lib/exec1.c | 43 ++++++++++++++++++-------------- shell/kswap.c | 9 +++++-- tests/kswap/test_cpu_contract.py | 43 -------------------------------- tools/kssf.asm | 11 ++++++-- 6 files changed, 43 insertions(+), 67 deletions(-) delete mode 100644 tests/kswap/test_cpu_contract.py diff --git a/criter/context.x b/criter/context.x index adcc74c9..202fd8a3 100644 --- a/criter/context.x +++ b/criter/context.x @@ -35,7 +35,8 @@ typedef kswap_t # Type currently defined word envSegm 0 # segment to be passed forth as environment pointer prg 0 # program to be executed pointer cmdline 0 # command line arguments of program -word execErr 0 # exit code of DOS-4B of external program +word execErr 0 # error code of DOS-4B API for external program +word childStatus 0 # successful child's full DOS-4D termination status pointer shell 0 # absolute path to shell to be executed word canexit 0 # true unless FreeCOM was invoked with /P word dfltSwap 0 # default value of swapOnExec diff --git a/include/misc.h b/include/misc.h index 48e9eaea..d0c106b5 100644 --- a/include/misc.h +++ b/include/misc.h @@ -150,6 +150,7 @@ void convert(unsigned long num, unsigned int billions, char * const des); void goxy(const unsigned char x, const unsigned char y); void setErrorLevel(int rc); +void setChildStatus(unsigned status); void execute(char *first, char *rest, int lh_lf); int exec(const char *, char *, const unsigned); void restoreSession(void); diff --git a/lib/exec1.c b/lib/exec1.c index 7b53f411..fb0994b0 100644 --- a/lib/exec1.c +++ b/lib/exec1.c @@ -59,31 +59,36 @@ void displayExitcode(void); /* also in include/misc.h */ #endif +/* Decode a successful child's saved AH=4Dh status without treating its AL + as an EXEC API error or consuming another child's return code. */ +void setChildStatus(unsigned status) +{ +#ifndef DISP_EXITCODE + int exitReason; +#endif + int rc = status & 0xFF; + exitReason = (status >> 8) & 0xFF; + dprintf(("[exec: exit code: %u:%u]\n", exitReason, rc)); + if(exitReason == 0x1 || exitReason == 0x2) + ctrlBreak = 1; + if(ctrlBreak && !rc) + rc = CBREAK_ERRORLEVEL; + errorlevel = rc; +#ifdef DISP_EXITCODE + displayExitcode(); +#endif +} + void setErrorLevel(int rc) { IREGS rp; dprintf(("[exec: DOS error code of exec(): %d]\n", rc)); if(!rc) { -#ifndef DISP_EXITCODE - int exitReason; /* else we use the global variable */ -#endif - rp.r_ax = 0x4d00; /* get return code */ - intrpt(0x21, &rp); - rc = rp.r_ax & 0xFF; - exitReason = (rp.r_ax >> 8) & 0xFF; - /* 0 -> normal - 1 -> ^Break / ^C - 2 -> Critical Error - 3 -> TSR - */ - dprintf(("[exec: exit code: %u:%u]\n", exitReason, rc)); - if(exitReason == 0x1) - ctrlBreak = 1; - if(exitReason == 0x2) /* Shallt change in the future */ - ctrlBreak = 1; - if(ctrlBreak && !rc) /* Make sure this condition is reflected */ - rc = CBREAK_ERRORLEVEL; + rp.r_ax = 0x4d00; + intrpt(0x21, &rp); + setChildStatus(rp.r_ax); + return; } else { #ifdef DISP_EXITCODE diff --git a/shell/kswap.c b/shell/kswap.c index b30f90a5..7ab3e2c5 100644 --- a/shell/kswap.c +++ b/shell/kswap.c @@ -286,8 +286,6 @@ int kswapLoadStruc(void) /* kswapContext->prg had been disabled in kswapInit() */ assert(!kswapContext->prg); - setErrorLevel(kswapContext->execErr); - canexit = kswapContext->canexit; defaultToSwap = kswapContext->dfltSwap; #ifndef NDEBUG @@ -320,6 +318,13 @@ int kswapLoadStruc(void) /* if the size does not change this function performs no actions */ env_setsize(0, kswapContext->envSize); + /* Message resources and context must be ready before an EXEC error is + diagnosed. A successful child's nonzero AL is not a DOS-4B error. */ + if(kswapContext->execErr) + setErrorLevel(kswapContext->execErr); + else + setChildStatus(kswapContext->childStatus); + return TRUE; } diff --git a/tests/kswap/test_cpu_contract.py b/tests/kswap/test_cpu_contract.py deleted file mode 100644 index d8a2805e..00000000 --- a/tests/kswap/test_cpu_contract.py +++ /dev/null @@ -1,43 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-or-later -"""Assemble KSSF's actual reload span at its required 8086 ISA boundary.""" -from pathlib import Path -import re -import subprocess -import tempfile -import unittest - -ROOT = Path(__file__).resolve().parents[2] - - -class CpuContractTests(unittest.TestCase): - def test_long_reload_branch_does_not_silently_require_a_386(self): - source = (ROOT / 'tools/kssf.asm').read_text() - cpu = re.search(r'^CPU\s+8086\b[^\n]*', source, re.MULTILINE) - self.assertIsNotNone(cpu, 'KSSF must explicitly constrain its instruction set') - span = source[source.index('mainloop:'):source.index('exit:', source.index('mainloop:'))] - # Symbolic data addresses stand in for the generated context interface; - # its operands retain their actual instruction widths. No guest artifact - # or saved generated header is an input to this boundary regression. - names = set(re.findall(r'\?[A-Za-z_]+', span)) | { - 'ctxt_owner', 'execBlock', 'tsrend', 'STACK_SIZE', 'context', - 'eb_envseg', 'eb_cmdline', 'shellCmdLine', - } - definitions = ''.join('%define ' + name + ' 512\n' for name in sorted(names)) - with tempfile.TemporaryDirectory() as temporary: - root = Path(temporary) - for directive in (cpu.group(0), ''): - assembly = 'BITS 16\n' + directive + '\n' + definitions + span + '\nexit:\nerrExecShell:\n' - (root / 'span.asm').write_text(assembly) - subprocess.run(['nasm', '-f', 'bin', str(root / 'span.asm'), - '-o', str(root / 'span.bin')], check=True) - binary = (root / 'span.bin').read_bytes() - if directive: - self.assertEqual(binary[-5:-2], b'\x74\x03\xe9') - else: - # Prove the original unconstrained span does cross rel8. - self.assertEqual(binary[-4:-2], b'\x0f\x85') - self.assertEqual(len(binary) + int.from_bytes(binary[-2:], 'little', signed=True), 0) - - -if __name__ == '__main__': - unittest.main() diff --git a/tools/kssf.asm b/tools/kssf.asm index 2eacb05d..f93e7607 100644 --- a/tools/kssf.asm +++ b/tools/kssf.asm @@ -93,15 +93,22 @@ mainloop: mov ax, 4b00h int 21h ; execute external program jc execPrg1 - mov ah, 4dh ; collect the child's DOS return code before reload + mov ah, 4dh ; keep status separate from a failed DOS-4B API int 21h - xor ah, ah + mov sp, tsrend + STACK_SIZE + mov dx, cs + mov ds, dx + mov es, [context] + mov [ES:?childStatus], ax ; AL exit code, AH termination reason + mov WORD [ES:?execErr], 0 + jmp execShell execPrg1: mov sp, tsrend + STACK_SIZE ; standard stack position mov dx, cs mov ds, dx mov es, [context] mov [ES:?execErr], ax ; error code of DOS-4B API + mov WORD [ES:?childStatus], 0 execShell: mov ax, shellCmdLine mov [eb_cmdline], ax From ba7e1ccf29fa7bd48e4319d1c74a9ef4b85e8a47 Mon Sep 17 00:00:00 2001 From: Nakata Maho Date: Mon, 5 Oct 2026 15:36:46 +0900 Subject: [PATCH 8/9] docs: do not refer to a test directory that is not part of this tree --- docs/k-swap.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/k-swap.txt b/docs/k-swap.txt index 3bc845cf..1a64c31b 100644 --- a/docs/k-swap.txt +++ b/docs/k-swap.txt @@ -35,7 +35,7 @@ Swapping is NOT supported in: The matching repaired FreeCOM/KSSF pair preserves the dynamic context, including aliases and history, across a supported interactive swap. Build and use both programs together; saved state is not a cross-version format. -See tests/kswap/README.md for the bounded regression and unqualified cases. +Swapping remains unsupported in the cases listed above. === How to use KSSF? From 2aea1b33b8dc52ed3d54407bf514e3679cbd5693 Mon Sep 17 00:00:00 2001 From: Nakata Maho Date: Mon, 5 Oct 2026 16:52:37 +0900 Subject: [PATCH 9/9] kswap: execute redirected, piped and batch commands without swapping The swap terminates the shell process: DOS closes the redirected or piped handles and the batch/FOR context is lost, so the program wrote to the console and the target file stayed empty under /SWAP. docs/k-swap.txt lists these cases as unsupported; fall back to an ordinary EXEC for them. (cherry picked from commit b646c5567dd76e0c29bbe7035b8f47a72bac030a) --- shell/command.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/shell/command.c b/shell/command.c index 965c62dd..19495534 100644 --- a/shell/command.c +++ b/shell/command.c @@ -247,6 +247,12 @@ void execute(char *first, char *rest, int lh_lf) /* Execute the external program */ #ifdef FEATURE_KERNEL_SWAP_SHELL + /* The swap ends this process, so DOS closes redirected and piped handles + and the batch or FOR context is lost. docs/k-swap.txt lists these cases + as unsupported; execute them without swapping, also under /SWAP. */ + if(swapOnExec == TRUE + && (oldinfd != -1 || oldoutfd != -1 || bc != 0)) + swapOnExec = FALSE; if(swapOnExec == TRUE && kswapMkStruc(fullname, rest)) { /* The Criter and ^Break handlers has been installed within