kernel/fs: new SF's 70.11 (create symlink) and 70.12 (read symlink) with EXT driver support #570

Merged
Burer merged 6 commits from Matou1306/kolibrios:ext-symlinks into main 2026-08-21 10:30:07 +00:00
Contributor

Tested the changes on qemu and umka and everything appears to be working fine

Tested the changes on qemu and umka and everything appears to be working fine
Doczom added the
Category
Kernel
Kind
Feature
Priority
High
2
Subsystem/VFS
labels 2026-07-11 16:14:17 +00:00
Matou1306 force-pushed ext-symlinks from eebe70633f to 7a8183cd7c 2026-07-11 16:44:35 +00:00 Compare
Matou1306 force-pushed ext-symlinks from 7a8183cd7c to 8f6d083aee 2026-07-11 17:01:31 +00:00 Compare
Matou1306 force-pushed ext-symlinks from 8f6d083aee to f8353df646 2026-07-11 17:10:33 +00:00 Compare
Matou1306 marked the pull request as work in progress 2026-07-11 19:09:09 +00:00
Matou1306 marked the pull request as ready for review 2026-07-11 22:10:34 +00:00
Burer requested changes 2026-07-18 14:00:01 +00:00
Dismissed
Burer left a comment
Owner

1. Kernel heap overflow in ext_CreateSymlink (write)

ecx is checked only at line 3307 (cmp ecx, maxPathLength), then line 3369 copies that many user-controlled bytes into tempBlockBuffer. On a 1024-byte block volume a target of 1024+ characters writes past the end of the allocation. Triggerable by any caller of f70.11 with write access.

2. Kernel memory disclosure in ext_ReadSymlink (read)

Lines 3429-3434 clamp ecx to the user buffer and to INODE.fileSize, but never to bytesPerBlock, and only one block is read (3450). An inode whose fileSize exceeds the block size - trivial on a crafted image, and exactly
what defect 1 produces - makes line 3456 copy kernel heap past mainBlockBuffer into the user buffer.

Both paths read or write a single block, so the copy length has to be clamped
to bytesPerBlock as well as maxPathLength.

3. Pre-existing, same class:

findInode.slow_symlink clamps to maxPathLength (1980) but not to bytesPerBlock, so line 1993 over-reads tempBlockBuffer by up to 3 KB. Not introduced here, but this PR makes it reachable by letting users create such symlinks.

**1. Kernel heap overflow in `ext_CreateSymlink` (write)** `ecx` is checked only at line 3307 (`cmp ecx, maxPathLength`), then line 3369 copies that many user-controlled bytes into `tempBlockBuffer`. On a 1024-byte block volume a target of 1024+ characters writes past the end of the allocation. Triggerable by any caller of f70.11 with write access. **2. Kernel memory disclosure in `ext_ReadSymlink` (read)** Lines 3429-3434 clamp `ecx` to the user buffer and to `INODE.fileSize`, but never to `bytesPerBlock`, and only one block is read (3450). An inode whose `fileSize` exceeds the block size - trivial on a crafted image, and exactly what defect 1 produces - makes line 3456 copy kernel heap past `mainBlockBuffer` into the user buffer. Both paths read or write a single block, so the copy length has to be clamped to `bytesPerBlock` as well as `maxPathLength`. **3. Pre-existing, same class:** `findInode.slow_symlink` clamps to `maxPathLength` (1980) but not to `bytesPerBlock`, so line 1993 over-reads `tempBlockBuffer` by up to 3 KB. Not introduced here, but this PR makes it reachable by letting users create such symlinks.
Burer changed title from fs: New subfunctions 11 (create symlink) and 12 (read symlink) with added support for ext driver to kernel/fs: new SF's 70.11 (create symlink) and 70.12 (read symlink) with EXT driver support 2026-07-18 14:01:43 +00:00
Doczom added the Subsystem/API label 2026-07-18 15:14:16 +00:00
Author
Contributor

@Burer Thanks for the help, please take a look now.

@Burer Thanks for the help, please take a look now.
Burer requested changes 2026-07-23 07:03:58 +00:00
Dismissed
Burer left a comment
Owner

The new bytesPerBlock checks are good, but they bound only the symlink target, not target + path tail - and the first hop skips the check that would catch it.

.resolve_symlink sends the first hop straight past .nested_symlink:

        cmp     [ebp+EXTFS.symlink_depth], SYMLINK_MAX_DEPTH - 1
        je      .setup_extraction       ; skips the bound at 2262-2268

So .symlink_copy_remaining (2325-2335) appends '/' + the remaining tail with an unbounded lodsb/stosb, edi already at symlink_workspace + fileSize. symlink_workspace is rb maxPathLength (4096).

Trigger: ext image, block size 4096, a slow symlink with a 4095-byte target used as a non-terminal component, e.g. /L/x. Target fills the buffer, then the tail writes out of bounds - length attacker-controlled, into symlink_depth, MOUNT_POLICY, c_inode, … and adjacent kernel heap.

Fix: hoist the .nested_symlink bound (2262-2268) out from under je .setup_extraction so target + tail is checked on the first hop too.

The new `bytesPerBlock` checks are good, but they bound only the symlink target, not target + path tail - and the first hop skips the check that would catch it. `.resolve_symlink` sends the first hop straight past `.nested_symlink`: ```asm cmp [ebp+EXTFS.symlink_depth], SYMLINK_MAX_DEPTH - 1 je .setup_extraction ; skips the bound at 2262-2268 ``` So `.symlink_copy_remaining` (2325-2335) appends `'/'` + the remaining tail with an unbounded `lodsb`/`stosb`, `edi` already at `symlink_workspace + fileSize`. `symlink_workspace` is `rb maxPathLength` (4096). **Trigger:** ext image, block size 4096, a slow symlink with a 4095-byte target used as a non-terminal component, e.g. `/L/x`. Target fills the buffer, then the tail writes out of bounds - length attacker-controlled, into `symlink_depth`, `MOUNT_POLICY`, `c_inode`, … and adjacent kernel heap. **Fix:** hoist the `.nested_symlink` bound (2262-2268) out from under `je .setup_extraction` so target + tail is checked on the first hop too.
Author
Contributor

Hi @Burer,
The check inside the nested loop is built differently (since it depends on esi pointing on the remaining path part inside symlink_workspace). I added a check if the tail fits in the remaining space before the copy operation instead.

Hi @Burer, The check inside the nested loop is built differently (since it depends on esi pointing on the remaining path part inside `symlink_workspace`). I added a check if the tail fits in the remaining space before the copy operation instead.
Burer requested changes 2026-08-07 16:34:04 +00:00
Dismissed
Burer left a comment
Owner

🔴 ext_CreateSymlink follows the final path component. It calls the regular findInode, so creating a symlink where one already exists resolves the old link and creates the new one at its target's path (for a dangling link - at the dead target's path). POSIX symlink() never follows the last component and fails even on a dangling link. Fix: call reachSymlink instead of findInode (as ext_ReadSymlink does) and clear symlink_no_follow afterwards - an existing link will then correctly hit .exist.

🟡 symlink_no_follow is declared dw but cleared with a byte store in ext_ReadSymlink. Works today (value is only 0/1), but make the store word-sized to match the field.

🟡 70.12 return convention: sysfns 30.2/30.5, which also return a path into a user buffer, always NUL-terminate (even when truncating) and count the terminator in the returned size. 70.12 currently returns raw bytes with no terminator, which introduces a second, conflicting convention. Since the API is new, better to match 30.2: always write a terminating 0 (truncate to size-1 if needed) and return the byte count including it - or, if you keep the readlink-style contract, state explicitly in the docs that the result is not NUL-terminated.

🔵 reachSymlink's mov dword [...], SYMLINK_MAX_DEPTH + 10000h sets two word fields with one instruction - clever, but add a one-line comment, otherwise it reads as a typo.

🔴 **`ext_CreateSymlink` follows the final path component.** It calls the regular `findInode`, so creating a symlink where one already exists resolves the old link and creates the new one at its *target's* path (for a dangling link - at the dead target's path). POSIX `symlink()` never follows the last component and fails even on a dangling link. Fix: call `reachSymlink` instead of `findInode` (as `ext_ReadSymlink` does) and clear `symlink_no_follow` afterwards - an existing link will then correctly hit `.exist`. 🟡 `symlink_no_follow` is declared `dw` but cleared with a byte store in `ext_ReadSymlink`. Works today (value is only 0/1), but make the store word-sized to match the field. 🟡 70.12 return convention: sysfns 30.2/30.5, which also return a path into a user buffer, always NUL-terminate (even when truncating) and count the terminator in the returned size. 70.12 currently returns raw bytes with no terminator, which introduces a second, conflicting convention. Since the API is new, better to match 30.2: always write a terminating 0 (truncate to size-1 if needed) and return the byte count including it - or, if you keep the readlink-style contract, state explicitly in the docs that the result is not NUL-terminated. 🔵 `reachSymlink`'s `mov dword [...], SYMLINK_MAX_DEPTH + 10000h` sets two word fields with one instruction - clever, but add a one-line comment, otherwise it reads as a typo.
Owner

@Matou1306

Sorry for long delay, was really busy on work.

Now I'm back to KOS and will try to review updates to your PR's as fast as possible.

@Matou1306 Sorry for long delay, was really busy on work. Now I'm back to KOS and will try to review updates to your PR's as fast as possible.
Matou1306 force-pushed ext-symlinks from 7e5112b962 to 046beab0fd 2026-08-10 20:54:43 +00:00 Compare
Author
Contributor

Hello @Burer, changes on both PRs are applied now, please let me know if you find anything else. Thanks for your time and help.

Hello @Burer, changes on both PRs are applied now, please let me know if you find anything else. Thanks for your time and help.
Burer force-pushed ext-symlinks from 6e52f8af7e to d7fb1a5f73 2026-08-18 06:18:03 +00:00 Compare
Burer approved these changes 2026-08-18 06:18:11 +00:00
dunkaist approved these changes 2026-08-19 14:03:20 +00:00
Leency approved these changes 2026-08-19 14:17:21 +00:00
Egor00f approved these changes 2026-08-19 15:34:22 +00:00
Doczom approved these changes 2026-08-19 17:49:55 +00:00
Doczom left a comment
Owner

It would be good to add a flag to the information about the file/directory (system function 70.5) that indicates that the object is a symbolic link.

It would be good to add a flag to the information about the file/directory (system function 70.5) that indicates that the object is a symbolic link.
Burer added 6 commits 2026-08-21 10:20:42 +00:00
infere symlink_no_folow word specifier instead of hardcoded byte

Null terminate path in 70.12 and add it to the returned count

add comment to explain the two value store in reachSymlink
docs: sync 70.11/70.12 into both languages, state the 70.12 NUL contract
Check kernel codestyle / Check kernel codestyle (pull_request) Successful in 21s
Test PR / Build (es_ES) (pull_request) Successful in 2m15s
Test PR / Build (ru_RU) (pull_request) Successful in 2m20s
Test PR / Build (en_US) (pull_request) Successful in 2m25s
3719dba28f
sysfuncs.txt: 70.12 now always writes a terminating zero (even when
truncating to buffer size - 1) and counts it in ebx, matching the
convention of function 30.2 - say so in the Remarks.

sysfuncr.txt: the Russian doc is maintained (it has 70.10) but never
received the new subfunctions: add the 70.11/70.12 sections, extend
the subfunction list and the SSF constants block.
Burer force-pushed ext-symlinks from d7fb1a5f73 to 3719dba28f 2026-08-21 10:20:42 +00:00 Compare
Burer merged commit 931071a0e9 into main 2026-08-21 10:30:07 +00:00
Burer deleted branch ext-symlinks 2026-08-21 10:30:07 +00:00
Sign in to join this conversation.
6 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: KolibriOS/kolibrios#570