From 11e8a603cee8651712153d386ada6fbe7ae21f05 Mon Sep 17 00:00:00 2001 From: Openclaw Date: Wed, 13 May 2026 07:07:44 +0000 Subject: [PATCH] [Fix] Add backup ASM core primitives --- .github/workflows/backup-asm.yml | 21 +++ coolify-backup-asm/.gitignore | 2 + coolify-backup-asm/Makefile | 32 ++++ coolify-backup-asm/README.md | 21 +++ coolify-backup-asm/docs/CALLING_CONVENTION.md | 18 +++ coolify-backup-asm/docs/RING_BUFFER.md | 15 ++ coolify-backup-asm/include/constants.inc | 18 +++ coolify-backup-asm/include/macros.inc | 10 ++ .../include/syscall_numbers.inc | 35 +++++ coolify-backup-asm/src/main.asm | 138 ++++++++++++++++++ coolify-backup-asm/src/memory.asm | 91 ++++++++++++ coolify-backup-asm/src/string.asm | 97 ++++++++++++ coolify-backup-asm/src/syscall.asm | 59 ++++++++ coolify-backup-asm/tests/test_runner.sh | 29 ++++ 14 files changed, 586 insertions(+) create mode 100644 .github/workflows/backup-asm.yml create mode 100644 coolify-backup-asm/.gitignore create mode 100644 coolify-backup-asm/Makefile create mode 100644 coolify-backup-asm/README.md create mode 100644 coolify-backup-asm/docs/CALLING_CONVENTION.md create mode 100644 coolify-backup-asm/docs/RING_BUFFER.md create mode 100644 coolify-backup-asm/include/constants.inc create mode 100644 coolify-backup-asm/include/macros.inc create mode 100644 coolify-backup-asm/include/syscall_numbers.inc create mode 100644 coolify-backup-asm/src/main.asm create mode 100644 coolify-backup-asm/src/memory.asm create mode 100644 coolify-backup-asm/src/string.asm create mode 100644 coolify-backup-asm/src/syscall.asm create mode 100755 coolify-backup-asm/tests/test_runner.sh diff --git a/.github/workflows/backup-asm.yml b/.github/workflows/backup-asm.yml new file mode 100644 index 0000000000..52e2ba5c63 --- /dev/null +++ b/.github/workflows/backup-asm.yml @@ -0,0 +1,21 @@ +name: backup-asm + +on: + push: + paths: + - 'coolify-backup-asm/**' + - '.github/workflows/backup-asm.yml' + pull_request: + paths: + - 'coolify-backup-asm/**' + - '.github/workflows/backup-asm.yml' + +jobs: + build-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install NASM + run: sudo apt-get update && sudo apt-get install -y nasm + - name: Build and test backup ASM + run: make -C coolify-backup-asm clean test diff --git a/coolify-backup-asm/.gitignore b/coolify-backup-asm/.gitignore new file mode 100644 index 0000000000..94a284ac17 --- /dev/null +++ b/coolify-backup-asm/.gitignore @@ -0,0 +1,2 @@ +*.o +/coolify-backup-asm diff --git a/coolify-backup-asm/Makefile b/coolify-backup-asm/Makefile new file mode 100644 index 0000000000..32f80ae9bd --- /dev/null +++ b/coolify-backup-asm/Makefile @@ -0,0 +1,32 @@ +NASM ?= nasm +NASMFLAGS ?= -f elf64 -g -F dwarf -I. +LD ?= ld +LDFLAGS ?= -nostdlib -static --no-dynamic-linker -z noexecstack +TARGET := coolify-backup-asm +SRCS := $(wildcard src/*.asm) +OBJS := $(SRCS:.asm=.o) +SIZE_LIMIT := 65536 + +$(TARGET): $(OBJS) + $(LD) $(LDFLAGS) -o $@ $^ + @size=$$(stat -c%s $@); \ + if [ $$size -gt $(SIZE_LIMIT) ]; then \ + echo "ERROR: Binary size $$size exceeds 64KB limit ($(SIZE_LIMIT) bytes)"; \ + rm -f $@; \ + exit 1; \ + fi; \ + echo "Binary size: $$size bytes (limit: $(SIZE_LIMIT))" + +%.o: %.asm include/*.inc + $(NASM) $(NASMFLAGS) -o $@ $< + +test: $(TARGET) + @bash tests/test_runner.sh + +objdump: $(TARGET) + @objdump -d $(TARGET) + +clean: + rm -f $(OBJS) $(TARGET) + +.PHONY: test clean objdump diff --git a/coolify-backup-asm/README.md b/coolify-backup-asm/README.md new file mode 100644 index 0000000000..6d97d034f9 --- /dev/null +++ b/coolify-backup-asm/README.md @@ -0,0 +1,21 @@ +# coolify-backup-asm + +Initial NASM/x86_64 foundation for issue #2, the bare-metal Coolify backup daemon. + +This subtree intentionally has no libc, Rust, PHP, or OpenSSL dependency. It builds a static ELF via `nasm` + `ld`, performs direct Linux syscalls, and enforces the requested 64KB binary limit at link time. + +Implemented in this PR: + +- `main.asm` entry point with `--help`, `--version`, and `--self-test` +- `syscall.asm` wrappers for core Linux syscalls needed by upcoming pipeline modules +- `string.asm` primitives: `strlen`, `strcmp`, `memcpy`, `memset`, `atoi`, `itoa` +- `memory.asm` 64KB ring buffer and control block primitives +- Makefile size guard and shell test harness +- Documentation for calling convention and ring buffer layout + +Build and test: + +```bash +cd coolify-backup-asm +make clean test +``` diff --git a/coolify-backup-asm/docs/CALLING_CONVENTION.md b/coolify-backup-asm/docs/CALLING_CONVENTION.md new file mode 100644 index 0000000000..d2735386ef --- /dev/null +++ b/coolify-backup-asm/docs/CALLING_CONVENTION.md @@ -0,0 +1,18 @@ +# coolify-backup-asm calling convention + +Internal routines use the issue-specified register contract rather than libc or the System V function ABI where practical: + +- `rax`: return value or syscall number +- `rbx`: ring buffer base pointer, preserved +- `rcx`: ring write position +- `rdx`: ring read position +- `rsi`: source pointer +- `rdi`: destination pointer / first argument +- `r8`: byte count / length +- `r9`-`r11`: scratch/options +- `r12`: stage context, preserved +- `r13`: error accumulator (`0` success, negative errno) +- `r14`: current file descriptor, preserved +- `r15`: timestamp/counter, preserved + +All exported routines are 16-byte aligned and document inputs/outputs in comments. Linux syscalls are made directly with the kernel syscall ABI. diff --git a/coolify-backup-asm/docs/RING_BUFFER.md b/coolify-backup-asm/docs/RING_BUFFER.md new file mode 100644 index 0000000000..50d5acbac5 --- /dev/null +++ b/coolify-backup-asm/docs/RING_BUFFER.md @@ -0,0 +1,15 @@ +# Ring buffer protocol + +The initial implementation reserves the full 64KB data buffer plus a 64-byte control block in `.bss`: + +| Offset | Field | +| --- | --- | +| `0x00000..0x0ffff` | data buffer | +| `0x10000` | write position | +| `0x10008` | read position | +| `0x10010` | watermark | +| `0x10018` | flags | +| `0x10020` | total bytes written | +| `0x10028` | stage id | + +This PR provides single-producer/single-consumer `ring_write` and `ring_read` primitives with 16-bit masking for wraparound. Later pipeline PRs can replace the simple position stores with the issue's `lock xadd` synchronization when multiple concurrent stages are introduced. diff --git a/coolify-backup-asm/include/constants.inc b/coolify-backup-asm/include/constants.inc new file mode 100644 index 0000000000..757eee166a --- /dev/null +++ b/coolify-backup-asm/include/constants.inc @@ -0,0 +1,18 @@ +%define STDIN 0 +%define STDOUT 1 +%define STDERR 2 +%define EXIT_OK 0 +%define EXIT_USAGE 64 +%define EXIT_SELFTEST 70 +%define RING_SIZE 65536 +%define RING_MASK 0xffff +%define CTRL_WRITE_POS 0x00 +%define CTRL_READ_POS 0x08 +%define CTRL_WATERMARK 0x10 +%define CTRL_FLAGS 0x18 +%define CTRL_TOTAL 0x20 +%define CTRL_STAGE 0x28 +%define CTRL_BYTES 64 +%define FLAG_EOF 1 +%define FLAG_ERROR 2 +%define FLAG_FLUSH 4 diff --git a/coolify-backup-asm/include/macros.inc b/coolify-backup-asm/include/macros.inc new file mode 100644 index 0000000000..339b7de363 --- /dev/null +++ b/coolify-backup-asm/include/macros.inc @@ -0,0 +1,10 @@ +%macro FUNC 1 +global %1 +align 16 +%1: +%endmacro + +%macro SYSCALL 1 + mov rax, %1 + syscall +%endmacro diff --git a/coolify-backup-asm/include/syscall_numbers.inc b/coolify-backup-asm/include/syscall_numbers.inc new file mode 100644 index 0000000000..a39f4bfdb7 --- /dev/null +++ b/coolify-backup-asm/include/syscall_numbers.inc @@ -0,0 +1,35 @@ +%define SYS_read 0 +%define SYS_write 1 +%define SYS_open 2 +%define SYS_close 3 +%define SYS_stat 4 +%define SYS_fstat 5 +%define SYS_lseek 8 +%define SYS_mmap 9 +%define SYS_munmap 11 +%define SYS_pipe 22 +%define SYS_dup2 33 +%define SYS_clone 56 +%define SYS_fork 57 +%define SYS_execve 59 +%define SYS_exit 60 +%define SYS_wait4 61 +%define SYS_kill 62 +%define SYS_uname 63 +%define SYS_fcntl 72 +%define SYS_getcwd 79 +%define SYS_chdir 80 +%define SYS_rename 82 +%define SYS_mkdir 83 +%define SYS_unlink 87 +%define SYS_gettimeofday 96 +%define SYS_getpid 39 +%define SYS_socket 41 +%define SYS_connect 42 +%define SYS_sendto 44 +%define SYS_recvfrom 45 +%define SYS_shutdown 48 +%define SYS_exit_group 231 +%define SYS_clock_gettime 228 +%define SYS_pipe2 293 +%define SYS_getrandom 318 diff --git a/coolify-backup-asm/src/main.asm b/coolify-backup-asm/src/main.asm new file mode 100644 index 0000000000..7a0d0d359a --- /dev/null +++ b/coolify-backup-asm/src/main.asm @@ -0,0 +1,138 @@ +%include "include/constants.inc" +%include "include/syscall_numbers.inc" +%include "include/macros.inc" + +extern sys_write, sys_exit +extern asm_strlen, asm_strcmp, asm_atoi, asm_itoa, asm_memcpy, asm_memset +extern ring_init, ring_write, ring_read + +global _start + +section .rodata +msg_help: db "coolify-backup-asm - zero-runtime backup daemon prototype",10,10,"Usage: coolify-backup-asm [--help|--version|--self-test]",10,10,"Initial PR implements direct syscall wrappers, string primitives, and the 64KB ring buffer foundation for the backup pipeline.",10,0 +msg_version: db "coolify-backup-asm 0.1.0-asm",10,0 +msg_ok: db "self-test: ok",10,0 +msg_fail: db "self-test: failed",10,0 +arg_help: db "--help",0 +arg_version: db "--version",0 +arg_selftest: db "--self-test",0 +sample: db "ring-buffer-check",0 +num12345: db "12345",0 + +section .bss +scratch: resb 128 + +section .text +_start: + mov rbp, rsp + mov rax, [rbp] ; argc + cmp rax, 2 + jb .help + mov rdi, [rbp + 16] ; argv[1] + lea rsi, [rel arg_help] + call asm_strcmp + test rax, rax + je .help + mov rdi, [rbp + 16] + lea rsi, [rel arg_version] + call asm_strcmp + test rax, rax + je .version + mov rdi, [rbp + 16] + lea rsi, [rel arg_selftest] + call asm_strcmp + test rax, rax + je .selftest + jmp .help_usage +.help: + lea rdi, [rel msg_help] + call print_cstr + xor rdi, rdi + call sys_exit +.version: + lea rdi, [rel msg_version] + call print_cstr + xor rdi, rdi + call sys_exit +.help_usage: + lea rdi, [rel msg_help] + call print_cstr + mov rdi, EXIT_USAGE + call sys_exit +.selftest: + call run_selftest + test rax, rax + jne .bad + lea rdi, [rel msg_ok] + call print_cstr + xor rdi, rdi + call sys_exit +.bad: + lea rdi, [rel msg_fail] + call print_cstr + mov rdi, EXIT_SELFTEST + call sys_exit + +print_cstr: + ; in rdi=c string + push rdi + call asm_strlen + mov rdx, rax + pop rsi + mov rdi, STDOUT + call sys_write + ret + +run_selftest: + ; returns rax=0 success else 1 + lea rdi, [rel sample] + call asm_strlen + cmp rax, 17 + jne .fail + lea rdi, [rel sample] + lea rsi, [rel sample] + call asm_strcmp + test rax, rax + jne .fail + lea rdi, [rel num12345] + call asm_atoi + cmp rax, 12345 + jne .fail + mov rdi, 67890 + lea rsi, [rel scratch] + call asm_itoa + cmp rax, 5 + jne .fail + lea rdi, [rel scratch] + lea rsi, [rel expect_67890] + call asm_strcmp + test rax, rax + jne .fail + call ring_init + lea rsi, [rel sample] + mov r8, 17 + call ring_write + cmp rax, 17 + jne .fail + lea rdi, [rel scratch] + xor esi, esi + mov r8, 32 + call asm_memset + lea rdi, [rel scratch] + mov r8, 17 + call ring_read + cmp rax, 17 + jne .fail + lea rdi, [rel scratch] + lea rsi, [rel sample] + call asm_strcmp + test rax, rax + jne .fail + xor rax, rax + ret +.fail: + mov eax, 1 + ret + +section .rodata +expect_67890: db "67890",0 diff --git a/coolify-backup-asm/src/memory.asm b/coolify-backup-asm/src/memory.asm new file mode 100644 index 0000000000..3f15c5d720 --- /dev/null +++ b/coolify-backup-asm/src/memory.asm @@ -0,0 +1,91 @@ +%include "include/constants.inc" +%include "include/macros.inc" + +section .bss +align 4096 +global ring_buffer +ring_buffer: resb RING_SIZE +global ring_control +ring_control: resb CTRL_BYTES + +section .text +FUNC ring_init + ; out: rbx=ring_buffer base, rcx=write_pos 0, rdx=read_pos 0, r13=0 + lea rbx, [rel ring_buffer] + lea rdi, [rel ring_control] + xor rax, rax + mov rcx, CTRL_BYTES / 8 + rep stosq + mov qword [rel ring_control + CTRL_WATERMARK], 49152 + xor rcx, rcx + xor rdx, rdx + xor r13, r13 + ret + +FUNC ring_write + ; in: rsi=src, r8=len; out: rax=bytes written, r13=0 or -ENOSPC + ; simple single-producer primitive for initial pipeline tests + push rbx + push r12 + lea rbx, [rel ring_buffer] + mov rax, [rel ring_control + CTRL_WRITE_POS] + mov r10, [rel ring_control + CTRL_READ_POS] + mov r11, rax + sub r11, r10 + mov r12, RING_SIZE + sub r12, r11 + cmp r8, r12 + ja .no_space + mov r10, r8 +.copy_loop: + test r10, r10 + je .copied + mov r11, rax + and r11, RING_MASK + mov dl, [rsi] + mov [rbx + r11], dl + inc rsi + inc rax + dec r10 + jmp .copy_loop +.copied: + mov [rel ring_control + CTRL_WRITE_POS], rax + add [rel ring_control + CTRL_TOTAL], r8 + mov rax, r8 + xor r13, r13 + pop r12 + pop rbx + ret +.no_space: + mov r13, -28 + mov rax, r13 + pop r12 + pop rbx + ret + +FUNC ring_read + ; in: rdi=dst, r8=max bytes; out: rax=bytes read + push rbx + lea rbx, [rel ring_buffer] + mov rdx, [rel ring_control + CTRL_READ_POS] + mov rcx, [rel ring_control + CTRL_WRITE_POS] + sub rcx, rdx + cmp rcx, r8 + cmova rcx, r8 + mov rax, rcx +.read_loop: + test rcx, rcx + je .done + mov r10, rdx + and r10, RING_MASK + mov r11b, [rbx + r10] + mov [rdi], r11b + inc rdi + inc rdx + dec rcx + jmp .read_loop +.done: + mov [rel ring_control + CTRL_READ_POS], rdx + xor r13, r13 + pop rbx + ret diff --git a/coolify-backup-asm/src/string.asm b/coolify-backup-asm/src/string.asm new file mode 100644 index 0000000000..500c991582 --- /dev/null +++ b/coolify-backup-asm/src/string.asm @@ -0,0 +1,97 @@ +%include "include/macros.inc" + +section .text +FUNC asm_strlen + ; in: rdi=null-terminated string; out: rax=length; preserves callee-saved regs + xor rax, rax +.len_loop: + cmp byte [rdi + rax], 0 + je .done + inc rax + jmp .len_loop +.done: + ret + +FUNC asm_strcmp + ; in: rdi=a, rsi=b; out: rax=0 equal, non-zero signed byte diff +.cmp_loop: + movzx eax, byte [rdi] + movzx edx, byte [rsi] + cmp al, dl + jne .diff + test al, al + je .equal + inc rdi + inc rsi + jmp .cmp_loop +.diff: + sub eax, edx + cdqe + ret +.equal: + xor eax, eax + ret + +FUNC asm_memcpy + ; in: rdi=dst, rsi=src, r8=len; out: rax=dst + mov rax, rdi + mov rcx, r8 + rep movsb + ret + +FUNC asm_memset + ; in: rdi=dst, sil=value, r8=len; out: rax=dst + mov rax, rdi + mov rcx, r8 + movzx eax, sil + push rdi + rep stosb + pop rax + ret + +FUNC asm_atoi + ; in: rdi=string; out: rax=unsigned integer parsed until non-digit + xor rax, rax +.atoi_loop: + movzx rdx, byte [rdi] + cmp dl, '0' + jb .atoi_done + cmp dl, '9' + ja .atoi_done + imul rax, rax, 10 + sub rdx, '0' + add rax, rdx + inc rdi + jmp .atoi_loop +.atoi_done: + ret + +FUNC asm_itoa + ; in: rdi=value, rsi=buffer; out: rax=len; writes decimal and NUL + mov rax, rdi + mov r9, rsi + test rax, rax + jne .nonzero + mov byte [rsi], '0' + mov byte [rsi+1], 0 + mov eax, 1 + ret +.nonzero: + xor rcx, rcx + mov r10, 10 +.push_digits: + xor rdx, rdx + div r10 + add dl, '0' + push rdx + inc rcx + test rax, rax + jne .push_digits + mov rax, rcx +.pop_digits: + pop rdx + mov [rsi], dl + inc rsi + loop .pop_digits + mov byte [rsi], 0 + ret diff --git a/coolify-backup-asm/src/syscall.asm b/coolify-backup-asm/src/syscall.asm new file mode 100644 index 0000000000..0bb20f1d11 --- /dev/null +++ b/coolify-backup-asm/src/syscall.asm @@ -0,0 +1,59 @@ +%include "include/syscall_numbers.inc" +%include "include/macros.inc" + +section .text +; syscall wrappers use Linux syscall register ABI and return raw rax. +; They intentionally do not touch heap or libc. + +FUNC sys_write + ; in: rdi=fd, rsi=buf, rdx=len; out: rax bytes or -errno + mov rax, SYS_write + syscall + ret + +FUNC sys_read + ; in: rdi=fd, rsi=buf, rdx=len; out: rax bytes or -errno + mov rax, SYS_read + syscall + ret + +FUNC sys_close + ; in: rdi=fd + mov rax, SYS_close + syscall + ret + +FUNC sys_exit + ; in: rdi=status; does not return + mov rax, SYS_exit_group + syscall + hlt + +FUNC sys_clock_gettime + ; in: rdi=clock_id, rsi=timespec* + mov rax, SYS_clock_gettime + syscall + ret + +FUNC sys_pipe2 + ; in: rdi=int[2], rsi=flags + mov rax, SYS_pipe2 + syscall + ret + +FUNC sys_fork + mov rax, SYS_fork + syscall + ret + +FUNC sys_execve + ; in: rdi=path, rsi=argv, rdx=envp + mov rax, SYS_execve + syscall + ret + +FUNC sys_wait4 + ; in: rdi=pid, rsi=status*, rdx=options, r10=rusage* + mov rax, SYS_wait4 + syscall + ret diff --git a/coolify-backup-asm/tests/test_runner.sh b/coolify-backup-asm/tests/test_runner.sh new file mode 100755 index 0000000000..d146d1bf07 --- /dev/null +++ b/coolify-backup-asm/tests/test_runner.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +fail() { echo "not ok - $*" >&2; exit 1; } +ok() { echo "ok - $*"; } + +[ -x ./coolify-backup-asm ] || fail "binary missing" + +size=$(stat -c%s ./coolify-backup-asm) +[ "$size" -le 65536 ] || fail "binary size $size exceeds 65536" +ok "binary size $size <= 65536" + +if ldd ./coolify-backup-asm 2>&1 | grep -vq "not a dynamic executable"; then + fail "binary must be static/no dynamic interpreter" +fi +ok "static ELF has no dynamic interpreter" + +./coolify-backup-asm --help | grep -q "zero-runtime backup daemon" || fail "help output" +ok "help output" + +./coolify-backup-asm --version | grep -q "0.1.0-asm" || fail "version output" +ok "version output" + +./coolify-backup-asm --self-test | grep -q "self-test: ok" || fail "self-test" +ok "self-test covers string primitives and ring buffer" + +readelf -h ./coolify-backup-asm | grep -q "Type:[[:space:]]*EXEC" || fail "expected static executable type" +ok "ELF executable header"