DEV Community

mattleeee
mattleeee

Posted on Originally published at hkcode.dpdns.org

Exit Code 267009: Debugging CMD Encoding Corruption

Scheduled tasks fail in the dark. There is no console, no visible stderr, and often no useful log — just a status code in Task Scheduler's "Last Run Result" column. When that code is 267009, the usual suspects (missing file, wrong working directory, permissions) all look wrong, because the task itself is fine. What is broken is the encoding of the launcher script.

This post walks through a real failure: a .bat file saved as UTF-8 containing Chinese comments and paths, executed by cmd.exe under a GBK system codepage. We'll reproduce it, decode the exit code, and lay out a checklist for batch files that need to survive being copied between machines.

The Symptom

A scheduled task ran a small batch launcher:

@echo off
REM 启动数据抓取流程
cd /d "C:\项目\抓取"
python fetch.py --output "C:\项目\数据\out.csv"
Enter fullscreen mode Exit fullscreen mode

The task's "Last Run Result" showed 267009 (0x41301). The Python script ran fine when invoked manually from a terminal. Under the scheduler, it never produced output and never wrote a log line. Nothing in Event Viewer beyond the exit code.

That 0x41301 is the give-away. So the first move is not to touch the script — it's to learn what the code actually means.

Reading Opaque CMD Exit Codes

Windows exit codes are 32-bit values. Task Scheduler displays them in decimal and hex. When a code looks arbitrary, check the hex form against the documented HRESULT/Win32 ranges:

  • 0x00000000–0x0000FFFF: normal Win32 error codes (ERROR_*).
  • 0x80000000–0xFFFFFFFF: HRESULT-style values, usually from COM or the shell.
  • 0x41301 specifically: SCHED_S_TASK_RUNNING — the task is still running.

That last one is the trap. 267009 is not an error at all. It is SCHED_S_TASK_RUNNING, a success-severity status meaning the task was still executing when the query was made. Task Scheduler reports it when:

  1. The task genuinely takes a long time and the status snapshot was taken mid-run.
  2. The launched process never exits — it hangs, usually waiting on input or blocked on a parse error.

In this case it was case 2: cmd.exe hit a malformed line, dropped into an interactive prompt, and sat there forever. The scheduled task "ran," produced no output, and never completed. 267009 was the only clue.

A quick way to normalize and classify codes before you go down a rabbit hole:

import ctypes
from ctypes import wintypes

def describe_exit_code(code: int) -> str:
    """Classify a Windows exit code into its documented family."""
    code &= 0xFFFFFFFF  # normalize to unsigned 32-bit

    if code == 0:
        return "success"
    if code == 0x41301:
        return "SCHED_S_TASK_RUNNING (task still executing or hung)"
    if code < 0x10000:
        msg = ctypes.FormatError(code).strip()
        return f"Win32 error {code} ({msg})" if msg else f"Win32 error {code}"
    if code >= 0x80000000:
        return f"HRESULT 0x{code:08X} (facility {code >> 16 & 0x1FFF})"
    return f"unknown 0x{code:08X}"

for c in (0, 267009, 2, 0x80070002, 1):
    print(f"{c:>10} 0x{c & 0xFFFFFFFF:08X}  {describe_exit_code(c)}")
Enter fullscreen mode Exit fullscreen mode

Output:

         0 0x00000000  success
    267009 0x00041301  SCHED_S_TASK_RUNNING (task still executing or hung)
         2 0x00000002  Win32 error 2 (The system cannot find the file specified.)
  2147942402 0x80070002  HRESULT 0x80070002 (facility 7)
         1 0x00000001  Win32 error 1 (Incorrect function.)
Enter fullscreen mode Exit fullscreen mode

Two rules of thumb fall out of this:

  • If you see 0x41301, the task is not failing — it is not finishing. Go look for a hang, not a crash.
  • 0x8007xxxx is a Win32 error wrapped in an HRESULT; strip the 0x8007 prefix and you get the plain ERROR_* code. 0x80070002 is just "file not found."

Reproducing the Corruption

The hang was caused by encoding, not logic. Here is the minimal reproduction.

Take a .bat file with non-ASCII content — Chinese comments, a Chinese path, or both — and save it as UTF-8 without BOM. On a machine whose active code page is GBK (chcp returns 936), run it:

@echo off
REM 抓取任务
echo starting
cd /d "C:\项目\数据"
echo done
Enter fullscreen mode Exit fullscreen mode

Under GBK, cmd.exe reads each byte of the UTF-8 sequence as a separate GBK character. UTF-8 encodes 项 as E9 A1 B9. GBK sees E9 A1 as one character and B9 as the start of another, consuming the next byte — which may be a quote, a newline, or part of echo. The parse decouples from the intended text.

The failure modes are consistent and annoying:

  • A REM comment line absorbs the following line, silently dropping a command.
  • A quoted path loses its closing quote, so cmd.exe keeps reading.
  • The script reaches end-of-file mid-command and cmd.exe falls back to reading from stdin — which, under a scheduled task, is an empty pipe. It blocks. Forever. Hence 267009.

You can reproduce this deterministically without the scheduler:

import subprocess

# A launcher with a UTF-8 Chinese comment, no BOM.
bat_utf8 = (
    "@echo off\r\n"
    "REM 抓取任务\r\n"
    "echo starting\r\n"
    "echo done\r\n"
)

with open("probe_utf8.bat", "wb") as f:
    f.write(bat_utf8.encode("utf-8"))

# Run it under a GBK console code page, capturing stdin from an empty pipe
# so a hang is observable as a timeout rather than a block.
try:
    proc = subprocess.run(
        ["cmd.exe", "/c", "chcp 936 >nul & probe_utf8.bat"],
        stdin=subprocess.DEVNULL,
        capture_output=True,
        timeout=5,
    )
    print("returncode:", proc.returncode)
    print("stdout:", proc.stdout.decode("gbk", "replace"))
except subprocess.TimeoutExpired:
    print("HANG: cmd.exe is waiting on stdin (this is the 267009 case)")
Enter fullscreen mode Exit fullscreen mode

On a GBK system this prints HANG. The same content saved as GBK runs cleanly. That difference is the whole bug.

Why UTF-8 "Works" Until It Doesn't

Modern Windows 10/11 still defaults cmd.exe to the OEM code page — 936 (GBK) on Simplified Chinese installs, 437/850 on US installs — unless you enable the "Beta: Use Unicode UTF-8 for worldwide language support" option. Editors, by contrast, default to UTF-8. So the file is written in one encoding and read in another. It works on the developer's machine only if that machine happens to be configured the same way, which is exactly the kind of coincidence that breaks when you copy the file to a server or a VM.

The scheduled task made it worse: the interactive terminal had a code page that happened to tolerate the file, while the scheduler ran cmd.exe with no console and the system default. Same file, different reader.

The Fixes

Three changes, applied together, kill the class of bug. None of them require the "UTF-8 beta" checkbox.

1. Keep .bat files pure ASCII

The most robust launcher contains only ASCII bytes: no comments in Chinese, no non-ASCII paths inline. If you need a localized comment, put it in a .txt sidecar or in the Python script, not in the batch file. cmd.exe has no encoding problem with ASCII because ASCII is a subset of nearly every code page in use.

2. If non-ASCII is unavoidable, save as GBK and use CRLF

If a path genuinely must appear literally, encode the file in the system's OEM code page (GBK on Chinese Windows) rather than UTF-8, and terminate lines with \r\n. cmd.exe's parser treats a bare \n inconsistently across versions; CRLF is what it expects. Do not add a UTF-8 BOM — cmd.exe will try to execute the BOM bytes as part of the first command.

def write_bat(path: str, text: str, encoding: str = "gbk") -> None:
    """Write a .bat file cmd.exe can parse.

    - encoding: OEM code page of the target machine (gbk, cp437, ...)
    - line endings forced to CRLF
    - no BOM
    """
    normalized = text.replace("\r\n", "\n").replace("\r", "\n")
    data = normalized.replace("\n", "\r\n").encode(encoding, errors="strict")
    with open(path, "wb") as f:
        f.write(data)  # never a BOM
Enter fullscreen mode Exit fullscreen mode

errors="strict" is deliberate: if a character cannot be represented in GBK, you want the write to fail loudly rather than silently replace it with ? and produce a path that doesn't exist.

3. Use 8.3 short paths for non-ASCII directories

Even with correct encoding, non-ASCII paths are fragile across tools, quoting layers, and remote sessions. Windows exposes an ASCII-only 8.3 alias for every path. Resolve it once and put the short form in the launcher:

import ctypes
from ctypes import wintypes

def short_path(long_path: str) -> str:
    """Return the 8.3 short form of a path (ASCII-only)."""
    GetShortPathNameW = ctypes.windll.kernel32.GetShortPathNameW
    GetShortPathNameW.argtypes = [wintypes.LPCWSTR, wintypes.LPWSTR, wintypes.DWORD]
    GetShortPathNameW.restype = wintypes.DWORD

    buf = ctypes.create_unicode_buffer(260)
    n = GetShortPathNameW(long_path, buf, len(buf))
    if n == 0:
        raise OSError(ctypes.get_last_error(), f"no short path for {long_path!r}")
    if n > len(buf):
        buf = ctypes.create_unicode_buffer(n)
        GetShortPathNameW(long_path, buf, n)
    return buf.value

print(short_path(r"C:\项目\数据"))  # e.g. C:\A1B2~1\A3C4~1
Enter fullscreen mode Exit fullscreen mode

The generated launcher is now pure ASCII:

@echo off
cd /d "C:\A1B2~1\A3C4~1"
python fetch.py --output "C:\A1B2~1\A3C4~1\out.csv"
Enter fullscreen mode Exit fullscreen mode

8.3 name generation can be disabled per-volume (fsutil 8dot3name query), so verify it is enabled on volumes where you rely on this. If it is off, fall back to fix 1 or 2.

A Checklist for Portable .bat Files

Before a batch file goes into a scheduled task or gets copied to another machine, run through this:

  1. Bytes are ASCII-only. Verify with a script, not by eye — invisible non-ASCII is easy to miss.
  2. Line endings are CRLF. No lone \n, no lone \r.
  3. No BOM. The first three bytes must not be EF BB BF.
  4. No non-ASCII paths inline. Use 8.3 short paths or pass paths as arguments from a caller that handles Unicode.
  5. Encoding matches the target. If ASCII is impossible, the file is GBK (or the target's OEM code page), not UTF-8.
  6. chcp is explicit if needed. If you must change the code page, do it as the first line and document why.
  7. No interactive fallback. End with an explicit exit /b %ERRORLEVEL% so cmd.exe never drops into a prompt on parse failure.
  8. Test under the scheduler's environment. A terminal session and a scheduled task are not the same reader.

A guard you can run in CI or as a pre-commit hook:

def audit_bat(path: str) -> list[str]:
    problems = []
    raw = open(path, "rb").read()

    if raw.startswith(b"\xef\xbb\xbf"):
        problems.append("has UTF-8 BOM")
    if b"\r\n" not in raw:
        problems.append("no CRLF line endings")
    if raw.replace(b"\r\n", b"").count(b"\n") or raw.replace(b"\r\n", b"").count(b"\r"):
        problems.append("mixed or bare line endings")
    try:
        raw.decode("ascii")
    except UnicodeDecodeError as e:
        problems.append(f"non-ASCII byte at offset {e.start}: {raw[e.start:e.start+1]!r}")
    return problems

for p in ("probe_utf8.bat", "launcher.bat"):
    issues = audit_bat(p)
    print(p, "OK" if not issues else issues)
Enter fullscreen mode Exit fullscreen mode

Run it against the original failing launcher and it reports the non-ASCII byte immediately — the same byte cmd.exe choked on.

Takeaway

267009 is not an error code in the usual sense; it is SCHED_S_TASK_RUNNING, and it most often means cmd.exe is blocked on a parse failure that turned into a stdin read. The parse failure came from an encoding mismatch between the file (UTF-8) and the reader (GBK). Fix the file's bytes — ASCII where possible, GBK where not, CRLF always, no BOM — and use 8.3 short paths for anything non-ASCII. The exit code disappears because there is nothing left for cmd.exe to misread.

More notes like this ship every week on this site.


Daily Picks

The following pairs are selected from the multi-timeframe trend scanner (Gate.io futures) and are for technical-analysis study only — not investment advice.
Data updated: 2026-10-06 12:36:33

Long

Pair Signal Price Take Profit Stop Loss R/R
SKYAI $0.0416 $0.0433 $0.0406 1:1.6
RE $0.4991 $0.5181 $0.4866 1:1.5

2 picks selected. Scanner runs every 15 minutes.

Top comments (0)