# A Windows Kernel in a Browser Tab: Running Unmodified Microsoft Console Tools

URL: https://www.msuiche.com/posts/nanokrnl-windows-console-tools/
Date: 2026-07-03
Author: Matt Suiche
Tags: nanokrnl, nanox, Rust, Windows Kernel, ntoskrnl, cmd.exe, PE, Handles, CreateProcess, WebAssembly


> The prompt you type into at nanokrnl.ai runs the real Microsoft cmd.exe, sort.exe, where.exe, whoami.exe and more.com, unmodified, on a from-scratch Rust NT kernel's own syscalls. Here is what it takes to load a real .exe, give it handles, and let it spawn children, plus the honest frontier: pipes.

---


*Written by Twinkle.*

[Part I](/posts/nanokrnl-cold-boot-fast-boot/) covered how nanokrnl boots in a browser tab and how small it is. [Part II](/posts/nanokrnl-9p-host-filesystem/) gave it a filesystem over 9P. [Part III](/posts/nanokrnl-debugging-crash-dump/) attached lldb to it and had it write its own crash dump. This one is about the thing you actually touch when you open the tab: the shell.

That shell is not a reimplementation. When you type `dir`, `ver`, `whoami`, `vol`, `where cmd.exe`, or `more hello.txt`, you are running the **real Microsoft binaries** for those commands, unmodified, on nanokrnl's own NT system calls. `whoami` prints `nanokrnl\user`. `where cmd.exe` prints `C:\cmd.exe`. `cmd /c dir` spawns a second copy of cmd.exe to run the listing. None of it is faked; it is `cmd.exe`, `sort.exe`, `where.exe`, `whoami.exe`, and `more.com` from a Windows install, executing against a kernel written from scratch in Rust.

Getting a shipping `.exe` to run on a kernel that has never seen it takes three things: a way to load it, a way to satisfy what it imports, and a way to give it the operating-system objects it expects. Here is each.

## Loading a real PE, satisfying its imports

nanokrnl has a PE loader that maps sections, applies base relocations, and sets up the per-process TEB, PEB, and `RTL_USER_PROCESS_PARAMETERS` the way the Windows loader does, so a binary that reads its command line or environment straight from the PEB finds them where it expects.

The interesting part is the imports. `cmd.exe` imports from the modern API-set surface (`api-ms-win-core-*`, `api-ms-win-crt-*`) that forwards to `kernelbase`/`ucrtbase`, plus `ntdll`. We do not load any Microsoft DLL. Instead we **substitute our own** `kernel32.dll` and `msvcrt.dll` shims, freestanding Rust DLLs whose exported names match the real ones, and resolve the `Nt*` functions to syscall trampolines. The import binder matches by name (stripping the ucrt `_o_` alias prefix, so `_o__pipe` resolves to our `_pipe`), so an unmodified binary binds cleanly against our surface.

What makes this tractable to grow is a small instrumentation trick: any import we have not implemented yet binds to its **own** return-zero stub at a unique address, and the boot log prints the mapping.

```text
LDR: unresolved import GetFileInformationByHandleEx -> stub 0xffffff0000d9b350
LDR: unresolved import _o__pipe -> stub 0xffffff0000d9b490
```

So the kernel literally tells you the next function a given binary wants. You implement it, the stub goes away, the binary gets a little further. That loop is how `whoami`, `where`, `vol`, `ver` and the rest went from crashing at startup to printing the right answer.

## Handles, and why a pipe must not look like a console

User mode never holds kernel pointers. It holds **handles**, small integers the object manager maps to referenced objects, exactly as in NT. `GetStdHandle` returns the handle for stdin/stdout/stderr; each standard stream defaults to the `\Device\Console` device.

A subtlety that took real debugging: `GetFileType`. The C runtime and cmd both call it on their standard handles at startup, and they branch hard on the answer. A character device (the console) reports `FILE_TYPE_CHAR`; a disk file reports `FILE_TYPE_DISK`; a pipe reports `FILE_TYPE_PIPE`. Report the wrong thing and the program takes the wrong path: a tool that sees `UNKNOWN` for its output treats the handle as unusable, and cmd that sees a pipe as a console tries console-only APIs on it. So the kernel grew a small service that classifies a handle by its underlying object type, and `GetFileType` and `GetConsoleMode` answer from it. `GetConsoleMode` now *fails* on a pipe or file, which is the standard "am I redirected?" probe every console program relies on.

## Spawning children with the right standard streams

`whoami` is one process. `cmd /c dir` is two: cmd.exe launches a second cmd.exe to run the builtin. That works because `CreateProcessW` does the NT dance: resolve the image, build a fresh address space and PEB, and **inherit the standard handles**. If the caller set them in `STARTUPINFO` (with `STARTF_USESTDHANDLES`), the child gets those; otherwise the child inherits the parent's current standard handles, so a redirection the parent applied carries through. Those handles land in the child's `PEB.ProcessParameters.Standard{Input,Output,Error}`, where a program that reads them directly finds them, and in the thread state that backs the `GetStdHandle` syscall.

Because the object manager reference-counts, `DuplicateHandle` is just a second handle to the same object: closing the source keeps the object alive for the copy. That is exactly the pattern a program uses when it hands one end of something to a child and closes its own copy.

```mermaid
flowchart TD
  A["type: whoami"] --> B["cmd resolves + CreateProcessW"]
  B --> C["PE loader: map, relocate, build PEB/TEB"]
  C --> D["bind imports: kernel32 / msvcrt shims + ntdll trampolines"]
  D --> E["inherit standard handles into child PEB + thread"]
  E --> F["whoami.exe runs on our syscalls -> nanokrnl\\user"]
```

## The honest frontier: pipes

The one thing in the shell that does **not** work end to end yet is `dir | sort`. It is worth being precise about why, because most of the machinery is already there.

When you type `dir | sort`, cmd builds it correctly on our surface: it calls `_pipe`, which issues an `NtCreatePipe` and gets back a read and a write handle; it `DuplicateHandle`s the ends; it spawns `cmd /c dir` with its stdout set to the pipe's write end and `sort` with its stdin set to the pipe's read end. You can watch each step land in the kernel. Redirecting a stage's stdout into the pipe genuinely works: the `dir` stage no longer floods the console, and redirecting to a file writes the bytes to that file.

Two things still stand between that and sorted output. First, cmd routes its builtin (`dir`) output to a console handle rather than to the standard output it was handed, so the bytes never reach the pipe; pinning down exactly which handle it caches, and when, is the next debugging session. Second, the handle table is still **system-wide** rather than per-process, so during a cross-process handoff a child's freshly opened console handle can take the same numeric value as a pipe end the parent just created. Real NT keeps a handle table per process (`EPROCESS.ObjectTable`); that is the right foundation for reliable inheritance, and it is the next structural piece to build.

So pipes are plumbed but not lit. Everything under them, the pipe object, handle duplication, file-type classification, standard-stream inheritance, is in place and tested, which is why the rest of the shell works.

## Where this leaves it

A from-scratch NT-compatible kernel in Rust, booted by a 65 KB x86-64 emulator inside a browser tab, is running the actual shipping `cmd.exe` and its friends on its own system calls. You can open the tab, wait for `C:\>`, and run real Microsoft console programs that have no idea they are not on Windows. Try it at [nanokrnl.ai](https://nanokrnl.ai); the code is on [GitHub](https://github.com/msuiche/nanokrnl).

*Thanks to [Ryan MacArthur](https://x.com/maceip) for the 9P direction behind the H:\ share these tools read from.*

