fix: CSPRNG on Windows, and a window's first title is the package's app name

Crypto.random_* and Uuid.* read /dev/urandom, which Windows lacks, so every
byte was zero; the Windows target now calls RtlGenRandom (advapi32).
ludicc takes --title, which ludic build/run/bundle pass from package.ludic's
app name, so rt_init opens the window under that name instead of the
program name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-17 16:22:05 +03:00
parent 28f661c36f
commit 7f9f8de08a
9 changed files with 33592 additions and 33300 deletions

View file

@ -0,0 +1,5 @@
bump: patch
type: fix
`Crypto.random_bytes`, `Crypto.random_hex`, `Crypto.random_u32` and `Uuid.*` draw from the
system CSPRNG on Windows (`RtlGenRandom`, advapi32). They read `/dev/urandom`, which Windows
does not have, and every byte came back zero.

View file

@ -0,0 +1,7 @@
bump: patch
type: fix
A windowed game opens under its package's `app name`. `ludic build`, `ludic run` and
`ludic bundle` pass it to the compiler (`ludicc --title <name>`), so `game_title()` - the
window's first title - is the name the player knows rather than the `program` name, which
showed for up to two seconds before the renderer retitled the window. Without an `app name`
it is the `program` name, as before.

View file

@ -10,6 +10,14 @@ function emit_str_const(s: pointer) -> pointer {
let n = len(s)
emith(name); emith(" = private unnamed_addr constant [")
emith(itoa(n + 1)); emith(" x i8] c\"")
emit_escaped(s)
emith("\\00\"\n")
return name
}
# the bytes of s as the body of an IR c"..." string: " \ and non-print -> \XX
function emit_escaped(s: pointer) -> void {
let n = len(s)
var i = 0
while i < n {
let c = s[i]
@ -20,8 +28,6 @@ function emit_str_const(s: pointer) -> pointer {
} else { buf_putc(head, c) }
i += 1
}
emith("\\00\"\n")
return name
}
# the constant initializer for a global var: a literal, or 0/null
@ -144,7 +150,7 @@ function emit_header() -> void {
emith("@L_argv = internal global ptr null\n")
emith("@L_clock = internal global i32 0\n") # Clock.* — the game-controlled simulated clock
emith("@.gametitle = private unnamed_addr constant [")
emith(itoa(len(g_game_name) + 1)); emith(" x i8] c\""); emith(g_game_name); emith("\\00\"\n")
emith(itoa(len(g_game_name) + 1)); emith(" x i8] c\""); emit_escaped(g_game_name); emith("\\00\"\n")
emith("%LSlice = type { ptr, i32, i32 }\n")
# property layouts — a %Cmp_ record of named fields, emitted here so `new`
# works whether or not the program runs the ECS. The per-entity @S_/@H_ arrays

View file

@ -293,14 +293,28 @@ function emit_secure_rand_prelude() -> void {
emith("@.ludic_rbmode = private unnamed_addr constant [3 x i8] c\"rb\\00\"\n")
emith("@.ludic_b64tab = private unnamed_addr constant [64 x i8] c\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\"\n")
# fill %n bytes at %out from the OS CSPRNG. If /dev/urandom cannot be opened the
# buffer is zeroed (documented degraded mode — e.g. wasm), never left uninit.
emith("define void @lp_secure_bytes(ptr %out, i64 %n) {\n")
emith("entry:\n call ptr @memset(ptr %out, i32 0, i64 %n)\n")
emith(" %fp = call ptr @fopen(ptr @.ludic_urandom, ptr @.ludic_rbmode)\n")
emith(" %isnull = icmp eq ptr %fp, null\n br i1 %isnull, label %fail, label %ok\n")
emith("ok:\n %rd = call i64 @fread(ptr %out, i64 1, i64 %n, ptr %fp)\n %cl = call i32 @fclose(ptr %fp)\n ret void\n")
emith("fail:\n ret void\n}\n")
# Windows has no /dev/urandom (fopen fails, and every byte came back zero): there the
# bytes come from RtlGenRandom (advapi32's SystemFunction036), the system CSPRNG,
# in chunks that fit its ULONG length. main.ludic links advapi32 for it.
if g_target_win {
emith("declare i8 @SystemFunction036(ptr, i32)\n")
emith("define void @lp_secure_bytes(ptr %out, i64 %n) {\n")
emith("entry:\n call ptr @memset(ptr %out, i32 0, i64 %n)\n %op = alloca i64\n store i64 0, ptr %op\n br label %c\n")
emith("c:\n %o = load i64, ptr %op\n %left = sub i64 %n, %o\n %more = icmp sgt i64 %left, 0\n br i1 %more, label %b, label %d\n")
emith("b:\n %big = icmp sgt i64 %left, 65536\n %k = select i1 %big, i64 65536, i64 %left\n %k32 = trunc i64 %k to i32\n")
emith(" %p = getelementptr i8, ptr %out, i64 %o\n %ok = call i8 @SystemFunction036(ptr %p, i32 %k32)\n")
emith(" %on = add i64 %o, %k\n store i64 %on, ptr %op\n br label %c\n")
emith("d:\n ret void\n}\n")
} else {
# fill %n bytes at %out from the OS CSPRNG. If /dev/urandom cannot be opened the
# buffer is zeroed (documented degraded mode — e.g. wasm), never left uninit.
emith("define void @lp_secure_bytes(ptr %out, i64 %n) {\n")
emith("entry:\n call ptr @memset(ptr %out, i32 0, i64 %n)\n")
emith(" %fp = call ptr @fopen(ptr @.ludic_urandom, ptr @.ludic_rbmode)\n")
emith(" %isnull = icmp eq ptr %fp, null\n br i1 %isnull, label %fail, label %ok\n")
emith("ok:\n %rd = call i64 @fread(ptr %out, i64 1, i64 %n, ptr %fp)\n %cl = call i32 @fclose(ptr %fp)\n ret void\n")
emith("fail:\n ret void\n}\n")
}
# %n secure bytes -> a fresh 2n-char lowercase hex string
emith("define ptr @lp_random_hex(i64 %n) {\n")

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -135,6 +135,7 @@ entry {
var save = false # --save-temps: keep the intermediate .ll
var run = false # compile then execute the result
var target = null # --target <triple>: null means the host
var title = null # --title <name>: game_title() - the window's first title - instead of the program name
var gui = false # --gui: a Windows GUI-subsystem program (no console behind the window)
var links = new []pointer # --link <file>: an extra input to the link (a compiled resource)
g_coverage = false # --coverage: instrument each statement with a per-line hit counter
@ -153,6 +154,7 @@ entry {
else if a == "--coverage" { g_coverage = true }
else if a == "--target" { ai += 1; if ai < arg_count() { target = arg(ai) } }
else if a == "--gui" { gui = true }
else if a == "--title" { ai += 1; if ai < arg_count() { title = arg(ai) } }
else if a == "--link" { ai += 1; if ai < arg_count() { push(links, arg(ai)) } }
else if a == "-o" { ai += 1; if ai < arg_count() { out = arg(ai) } }
else if a[0] == '-' {
@ -178,7 +180,10 @@ entry {
g_parse_file = path # for the compiler's own file:line diagnostics
lex(src)
parse_program()
g_prog_user_end = len(prog) # decls from the user's source; runtime splice appends after
g_prog_user_end = len(prog)
# `ludic build` passes the package's `app name`, so the window opens under the name the
# player knows rather than the `program` name and is not retitled a moment later
if (title != null) and (len(title) > 0) { g_game_name = title } # decls from the user's source; runtime splice appends after
# --fmt is the doc-check gate: reaching here means it lexed and parsed. A parse
# error would already have exited nonzero, so a clean parse exits 0. (Canonical
@ -281,6 +286,8 @@ entry {
}
# Process.* links the child-process layer: posix_spawn from libSystem on macOS,
# CreateProcessW from kernel32 on Windows.
# Crypto.random_* / Uuid.* read RtlGenRandom on Windows, which advapi32 exports
if g_target_win and g_uses_cryptort { cmd = `{cmd} -ladvapi32` }
if g_uses_process {
if g_target_win { cmd = `{cmd} {join_path(home, "runtime/native/process_win.ll")}` }
else { cmd = `{cmd} {join_path(home, "runtime/native/process.ll")}` }

View file

@ -43,17 +43,17 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
var flags = "--windowed"
if mode == 2 { flags = "--headless" }
if save { flags = flags + " --save-temps" }
return shq(`{ludicc()} {flags} {src} -o {out}`)
return shq(`{ludicc()} {flags}{title_flag()} {src} -o {out}`)
}
if mode == 2 {
if not shq(`{ludicc()} --headless {src} --emit-llvm -o {ll}`) { return false }
if not shq(`{ludicc()} --headless{title_flag()} {src} --emit-llvm -o {ll}`) { return false }
if not shq(`{cc()} -O2 {ll}{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{udp_link_flags(ll)}{process_link_flags(ll)}{threads_link_flags(ll)}{pbf} -o {out}`) { return false }
if not save { shell(`rm -f {ll}`) }
return true
}
if not shq(`{ludicc()} --windowed {src} --emit-llvm -o {ll}`) { return false }
if not shq(`{ludicc()} --windowed{title_flag()} {src} --emit-llvm -o {ll}`) { return false }
# audio.ll (#22) is always linked here — unused snd_* are dead-stripped; the
# canonical `ludicc -o` path links it only when Audio.* is used.
let cocoa = `{home}runtime/native/cocoa.ll`
@ -63,6 +63,21 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
return true
}
# ` --title "<app name>"` when the project's package.ludic names the app, so the window
# opens under that name rather than the `program` name; "" otherwise, and for a name the
# shell line could not carry safely.
function title_flag() -> pointer {
let name = manifest_app(read_root_manifest(), "name")
if name == "" { return "" }
var i = 0
while name[i] != 0 {
let c = name[i]
if c == '"' or c == '`' or c == '$' or c == 92 or c == '%' or c < ' ' { return "" }
i += 1
}
return ` --title "{name}"`
}
# A program that uses Gl.* references the @lgl_* thunks; link the OpenGL backend
# (gl.ll + gl_thunks.ll + OpenGL.framework) only then, so other builds are untouched.
function gl_link_flags(ll: pointer) -> pointer {

View file

@ -288,7 +288,7 @@ function cmd_bundle_windows() -> int {
let staged = tmp_path("bundle_exe")
var flags = "--windowed --gui"
if res != "" { flags = flags + ` --link "{res}"` }
if not shq(`{ludicc()} {flags} {entry} -o {staged}`) { return 1 }
if not shq(`{ludicc()} {flags}{title_flag()} {entry} -o {staged}`) { return 1 }
if not shq(`cp "{staged}.exe" "{root}/{name}.exe"`) {
err("ludic bundle: could not place the executable\n")
return 1