<feed xmlns='http://www.w3.org/2005/Atom'>
<title>gbos.git/docs, branch master</title>
<subtitle>gbos - a tiny Unix-flavored OS for the Game Boy Color (kernel, shell, coreutils in C, RAM filesystem)</subtitle>
<id>https://git.sl0p.foo/gbos.git/atom/docs?h=master</id>
<link rel='self' href='https://git.sl0p.foo/gbos.git/atom/docs?h=master'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/'/>
<updated>2026-09-24T21:08:35Z</updated>
<entry>
<title>fs: hard links - ln(1), and rm that removes a NAME</title>
<updated>2026-09-24T21:08:35Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-24T21:08:35Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=1430515ad419f2fddeb0265337a0da043901411b'/>
<id>urn:sha1:1430515ad419f2fddeb0265337a0da043901411b</id>
<content type='text'>
gbfs has carried I_NLINK since the beginning and nothing ever incremented it,
so a file could only have one name. SYS_LINK adds a second directory entry
pointing at the same inode: the data is stored once, and both names read it.

That makes rm's job different. It now decrements the link count and only frees
the inode and its blocks when the LAST name goes - removing one of two links
used to free blocks the other still pointed at.

Refused, with the reasons that matter here: linking a directory (it would make
a cycle the tree walkers cannot survive), an existing name, and anything under
/bin, which is ROM.

The syscall takes a request block because the trap needs HL for its dispatch
table - and ln copies both paths to the stack first, since that block is a
static living at $A000, on top of the command line it is reading.
</content>
</entry>
<entry>
<title>fs: mount the builtins at /bin</title>
<updated>2026-09-24T12:15:46Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-24T12:15:46Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=50a56b48f22cd31868403f1fde10cf8367344e2e'/>
<id>urn:sha1:50a56b48f22cd31868403f1fde10cf8367344e2e</id>
<content type='text'>
The commands only existed in the kernel's program table, so the only way to
find out what the machine could do was to already know (or read the source).
They are now a directory in the tree:

    /# ls /                      /# xxd -l 16 /bin/uname
    bin                          0000: cd 6b 43 06 00 0e 00 f7 .kC.....
    readme                       0008: 18 fe 47 0e 03 f7 c9 21 ..G....!
    /# ls -l /bin | head -2
    -  1 16384 worker

/bin is synthetic, not disk: dir_find resolves "bin" in the root to BIN_INO
and names inside it through the NameTable, so `ls /bin` lists exactly what the
shell can run - one table, one truth, no second list to keep in sync. The root
listing carries "bin" as a synthetic first entry so it is discoverable by
walking the tree.

A program image is inode BIN_FILE|progid: it stats as a 16 KiB file (a program
IS its ROM bank) and reads through rom_getb, which maps that bank, takes the
byte, and maps the CALLER'S text bank back before returning - the caller is
itself executing from $4000, so leaving the wrong bank mapped would return
into another program's code. Verified byte-for-byte against the host's view of
the blob. The tree is read-only: create/remove/write inside /bin all fail.

xxd grows -l (stop after N bytes), because a program image is 2048 dump lines
- more than a pipe's temp file can hold, let alone the screen.

Two traps worth remembering, both found the hard way here: `and BIN_FILE`
destroys A, so the inode must be reloaded before inode_ptr (missing it once
made ls -l stat inode 0 and call every file an empty non-directory); and ls
now copies its directory argument to the stack first, because stat()'s request
block is a static living at $A000 - on top of the command line it was reading.
</content>
</entry>
<entry>
<title>kernel: stat/fsstat/isatty syscalls, and O_APPEND</title>
<updated>2026-09-24T10:48:37Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-24T10:48:37Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=dc9dffb9069e32a1a0cf14883176544b234fc3e5'/>
<id>urn:sha1:dc9dffb9069e32a1a0cf14883176544b234fc3e5</id>
<content type='text'>
Four things userland had no way to ask for:

- SYS_STAT: type, link count, size and inode for a path. The trap needs HL
  for its dispatch table, so a call taking both a path and an output buffer
  passes a request block - the shape SYS_NET already established. usr/stat.h
  wraps it, header-only like sock.h, because the block must live in the
  caller's RAM.
- SYS_FSSTAT: blocks/inodes, total and free. The bitmaps are already cached
  write-through in fs_bmbuf, so this is the popcount boot_fs already prints.
- SYS_ISATTY: is this process's stdin the console? The shell needs it to tell
  an interactive session from a script.
- O_APPEND: open a file positioned at EOF. There is no seek syscall, so this
  is the only way to add to a file - without it nothing running ON the Game
  Boy can build a multi-line file, shell scripts included.

Together these back ls -l, df, and `&gt;&gt;`.
</content>
</entry>
<entry>
<title>net: one socket per client - four-tuple demux, spawning accept, bounded close</title>
<updated>2026-09-22T19:54:03Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-22T19:54:03Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=dcd1f1320c102f9cc4cff7eee387ec538304f7d1'/>
<id>urn:sha1:dcd1f1320c102f9cc4cff7eee387ec538304f7d1</id>
<content type='text'>
A server could only ever hold ONE connection, which makes an ircd pointless:
listen() turned the listener into the connection, and net_find_tcp demuxed on
local port alone, so two clients on 6667 were indistinguishable.

- net_find_tcp now matches an established socket on the full four-tuple
  (local port + peer IP + peer port) and only falls back to a LISTENing
  socket when none matches.
- A SYN at a listener no longer consumes it: tcp_spawn_conn clones a new
  socket (local port + owning pid, so exit/kill still reclaims it) and the
  listener keeps listening. accept() returns that socket; closing it leaves
  the listener alone. A full table drops the SYN, and the client's retransmit
  is taken once a slot frees.
- tcp_peer_is_current preserves BC: net_find_tcp scans with the socket index
  there, and the compare needs BC for the rx buffer base. Without this the
  scan died at the first mismatching socket, so a second client's handshake
  ACK never reached its socket and it hung in SYNRCVD - tcpdump showed our
  SYN+ACK going out and the client's NICK retransmitted five times into
  silence.
- listen() only conflicts with another LISTENER on the port; connections
  share it by design now.
- MAX_SOCKS 4 -&gt; 6 (listener + 3 clients + 2 spare; 537 bytes of WRAM0 still
  free), and SK_ACC marks a spawned connection as not yet accepted.

tcp_close's settle is also time-boxed to ~250 ms of real time. It counted
PUMPS (8 x 8000), which is ~30s of wall clock when the peer never answers -
invisible with well-behaved peers, but a daemon refusing a client that stayed
connected froze itself, and every other client with it, for half a minute.

httpd keeps its listener open across requests now, so the next client's SYN
is accepted immediately instead of being dropped between close and re-listen:
three parallel clients go 0.1/1.1/2.1s -&gt; 0.4/1.0/0.1s.
</content>
</entry>
<entry>
<title>usr: httpd - the Game Boy serves its filesystem over HTTP</title>
<updated>2026-09-22T15:12:52Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-22T15:12:52Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=30e67fe0f4e66e2161ca61f870f8947d50287995'/>
<id>urn:sha1:30e67fe0f4e66e2161ca61f870f8947d50287995</id>
<content type='text'>
    /# httpd &amp;
    host$ curl http://10.0.0.2/        -&gt; a listing of the cart RAM
    host$ curl http://10.0.0.2/readme  -&gt; the file, from battery-backed SRAM

GET only. "/" serves /index.html if present, else a generated listing; any
directory lists with every entry linked, so you can click around a 1997
handheld's disk from a browser. Content type from the extension. No
Content-Length: HTTP/1.0 + Connection: close delimits the body by closing,
which suits a stack with one in-flight segment.

All output goes through one 200-byte buffer, so each net_send is a full
segment instead of one tiny segment per string.

It drains the whole request, not just the request line. That is not
politeness: leaving the rest unread means those segments are never
acknowledged, so the client retransmits headers for a minute, never sends
its FIN, and its dead connection keeps arriving at port 80 long after we
re-listen. One curl with 1.4 KB of headers stalled every following request
for 30-120s.

It deliberately does NOT read the keyboard either. Backgrounded - the useful
way to run it - every key it polled would be a key stolen from the shell's
prompt. It is a daemon: kill(1) stops it, and the kernel reclaims its socket.

Verified with curl: index, byte-exact file bodies, nested paths, directory
listings, 404, 501, a 1.4 KB request spanning segments, browser user-agents,
three parallel clients (0.1/1.1/2.1s), the shell staying usable alongside it,
and six kill/restart cycles - more than there are sockets.
</content>
</entry>
<entry>
<title>net: three TCP receive bugs - caller corruption, data loss, early EOF</title>
<updated>2026-09-22T14:02:45Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-09-22T14:02:45Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=f084772cf3c021fa2315fb17a0697c4be8c59a77'/>
<id>urn:sha1:f084772cf3c021fa2315fb17a0697c4be8c59a77</id>
<content type='text'>
Found by writing nc(1): a 96-byte receive buffer against a host that
answers in 200-byte segments hung the process every time.

- recv/recv_nb copied RXLEN bytes into the caller's buffer and never
  looked at the caller's maxlen, so any short buffer got overrun and its
  stack smashed. Deliver min(RXLEN, maxlen) and keep the tail in RXBUF
  for the next call. wget only survived because its buffer (220) happens
  to exceed our MSS.
- tcp_buffer_data overwrote RXBUF from offset 0 on every segment, so a
  segment arriving before the app drained the previous one destroyed
  unread bytes - silent, undetectable data loss. Append at RXBUF+RXLEN
  instead, and drop (without advancing rcv_nxt) when it doesn't fit, so
  the peer retransmits once there's room. RXLEN is one byte, so 255 is
  the capacity that matters, not RXBUF's 288.
- A draining recv now clears RXLEN as well as HASRX: RXLEN is the append
  offset, and a stale one made every later segment look too big to fit,
  stalling the connection until the peer gave up.
- A FIN past a gap is not end-of-stream. tcp_in accepted any FIN whose
  segment carried no data, without a sequence check, marking the socket
  DONE and making recv report EOF at the hole - truncating a transfer
  right before its last segments. Decide in-order-ness once, before
  rcv_nxt moves, and re-ACK an out-of-order FIN instead.

Verified against a 6300-byte reply received while sending 831 bytes:
byte-exact (6339 = payload + banner, 303 lines, first/mid/last intact),
plus no regression in ping / nslookup / wget / irc.
</content>
</entry>
<entry>
<title>docs: drop the link-drop investigation writeup</title>
<updated>2026-07-18T19:02:47Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-18T19:02:47Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=cdebdd1ae2c50e37f7401ccaae890ff554b974ea'/>
<id>urn:sha1:cdebdd1ae2c50e37f7401ccaae890ff554b974ea</id>
<content type='text'>
</content>
</entry>
<entry>
<title>docs: concise README (networking + demo gif up front); internals.md</title>
<updated>2026-07-18T18:41:37Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-18T18:41:37Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=546d2f36f309a0d8ac88360361b6fcfc0167eef5'/>
<id>urn:sha1:546d2f36f309a0d8ac88360361b6fcfc0167eef5</id>
<content type='text'>
README is now pitch -&gt; proof (real capture gif) -&gt; quick start -&gt; docs
pointers, with the network stack and gbhub/gbjoin/gbtype getting
explicit billing. The full technical breakdown (memory model, syscall
ABI, fork/exec/context switch, FS layout, toolchain war stories) moves
to docs/internals.md, plus previously-undocumented sections on the
network stack and the terminal/OSK.
</content>
</entry>
<entry>
<title>docs: link-drop investigation resolved - drops are self-healing now</title>
<updated>2026-07-17T17:47:12Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-17T17:47:12Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=7efcb26671563ea8f734e752e14a04455ecfd663'/>
<id>urn:sha1:7efcb26671563ea8f734e752e14a04455ecfd663</id>
<content type='text'>
Rewrite the notes from 'open / not root-caused' to the resolution: the
socket never mysteriously dies; every producible drop signature is the
emulator process dying silently (SIGPIPE on console echo after a hub
restart, terminal death/HUP/stray quit keys in windowed mode - which
exactly explains the 'only when idle' correlation - or a guest $ED
poweroff), plus observation artifacts in the original session's
tooling. Records the fixes shipped on both sides (emulator link
auto-reconnect + SIGPIPE ignore + $ED logging; hub eof-vs-error
logging, kept stderr, EBUSY guard), the honest spawn-mode caveat, and
what's still worth doing.
</content>
</entry>
<entry>
<title>docs: investigation notes for the idle link-socket drop (open bug)</title>
<updated>2026-07-17T13:28:14Z</updated>
<author>
<name>user</name>
<email>user@clank</email>
</author>
<published>2026-07-17T13:28:14Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/gbos.git/commit/?id=985b64da38fe8f73e274844084d970668ac678d4'/>
<id>urn:sha1:985b64da38fe8f73e274844084d970668ac678d4</id>
<content type='text'>
</content>
</entry>
</feed>
