Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/system-hooks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Verify system hooks

on:
pull_request:
paths:
- "ql-assets/data/system-hooks/**"
- "Makefile"
- ".github/workflows/system-hooks.yml"
push:
branches:
- main
paths:
- "ql-assets/data/system-hooks/**"
- "Makefile"
- ".github/workflows/system-hooks.yml"

jobs:
force-rate:
name: Rebuild force_rate.so
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Verify committed system hook binaries
run: make verify-system-hooks
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
!README.md
!requirements.txt
!VERSION
!Makefile
!pytest.ini
!docker-compose.yml
!host-init/
Expand Down
13 changes: 13 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CC := gcc
FORCE_RATE_SRC := ql-assets/data/system-hooks/force_rate.c
FORCE_RATE_SO := ql-assets/data/system-hooks/force_rate.so
FORCE_RATE_BUILD := /tmp/qlsm-force_rate.verify.so

.PHONY: force-rate.so verify-system-hooks

force-rate.so:
$(CC) -shared -fPIC -Wall -Wextra -Werror -Wl,--build-id=none -o $(FORCE_RATE_SO) $(FORCE_RATE_SRC)

verify-system-hooks:
$(CC) -shared -fPIC -Wall -Wextra -Werror -Wl,--build-id=none -o $(FORCE_RATE_BUILD) $(FORCE_RATE_SRC)
cmp $(FORCE_RATE_BUILD) $(FORCE_RATE_SO)
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.12.11
1.12.12
1 change: 1 addition & 0 deletions docs/user/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ QLSM uses `v<major>.<minor>.<patch>` tags. Every merged pull request is listed a

| Version | Date | PR | Changes |
| --- | --- | --- | --- |
| `v1.12.12` | 2026-06-24 | [#128](https://github.com/dngrtech/qlsm/pull/128) | Add retry handling to `force_rate.so` so the LD_PRELOAD hook waits briefly for qzeroded's target text page before giving up on the LAN-rate patch. |
| `v1.12.11` | 2026-06-23 | [#127](https://github.com/dngrtech/qlsm/pull/127) | Apply static network hardening to all hosts, not just non-standalone hosts. |
| `v1.12.10` | 2026-06-23 | [#126](https://github.com/dngrtech/qlsm/pull/126) | Auto-recover hosts after reboot timeouts: failed restart playbooks now probe for post-reboot reachability before marking ERROR, and the status poller can restore stale ERROR hosts to ACTIVE when live instance status is available. |
| `v1.12.9` | 2026-06-22 | [#125](https://github.com/dngrtech/qlsm/pull/125) | Fix the Test Connection button staying disabled for standalone hosts in the Add Host modal. |
Expand Down
2 changes: 1 addition & 1 deletion docs/user/version.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"latest": "1.12.11",
"latest": "1.12.12",
"releaseNotesUrl": "https://dngrtech.github.io/qlsm/releases/"
}
8 changes: 7 additions & 1 deletion ql-assets/data/system-hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ added to `RESERVED_HOOK_FILENAMES`.
## Rebuilding

```
gcc -shared -fPIC -o ql-assets/data/system-hooks/force_rate.so ql-assets/data/system-hooks/force_rate.c
gcc -shared -fPIC -Wall -Wextra -Werror -Wl,--build-id=none -o ql-assets/data/system-hooks/force_rate.so ql-assets/data/system-hooks/force_rate.c
```

or:

```
make force-rate.so
```

x86-64 Linux only. Commit the rebuilt `.so` alongside source changes.
63 changes: 59 additions & 4 deletions ql-assets/data/system-hooks/force_rate.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#include <sys/mman.h>
#include <unistd.h>
#include <errno.h>
#include <limits.h>
#include <time.h>

/*
* force_rate.so - LD_PRELOAD library for Quake Live Dedicated Server
Expand All @@ -20,6 +22,45 @@

/* Address of Sys_IsLANAddress in qzeroded.x64 */
#define SYS_ISLANADDRESS_ADDR 0x004518d0
#define PATCH_RETRY_ATTEMPTS 50
#define PATCH_RETRY_DELAY_NS 20000000L

static int is_qzeroded_process(void)
{
char exe_path[PATH_MAX];
ssize_t len = readlink("/proc/self/exe", exe_path, sizeof(exe_path) - 1);
if (len < 0)
return 0;
exe_path[len] = '\0';
return strstr(exe_path, "qzeroded") != NULL;
}

static int page_is_mapped(void *page_start, long page_size)
{
unsigned char vec;
return mincore(page_start, (size_t)page_size, &vec) == 0;
}

static void sleep_retry_delay(void)
{
struct timespec delay = { .tv_sec = 0, .tv_nsec = PATCH_RETRY_DELAY_NS };
struct timespec remaining;

while (nanosleep(&delay, &remaining) != 0 && errno == EINTR)
delay = remaining;
}

static int wait_for_target_page(void *page_start, long page_size)
{
/* 50 attempts at 20ms each gives qzeroded up to ~1s to map its text page. */
for (int attempt = 0; attempt < PATCH_RETRY_ATTEMPTS; attempt++) {
if (page_is_mapped(page_start, page_size))
return 1;
sleep_retry_delay();
}

return 0;
}

/*
* Patch Sys_IsLANAddress to always return 1.
Expand All @@ -34,14 +75,27 @@ static void patch_sys_islanaddress(void)
{
void *target = (void *)SYS_ISLANADDRESS_ADDR;
long page_size = sysconf(_SC_PAGESIZE);

if (page_size <= 0)
return;

void *page_start = (void *)((uintptr_t)target & ~(page_size - 1));

/* Silently skip if the target page isn't mapped — this constructor runs in
* every process that inherits LD_PRELOAD (e.g. the bash wrapper script and
* any subshells it spawns). Only qzeroded.x64 maps this address. */
unsigned char vec;
if (mincore(page_start, page_size, &vec) != 0)
return;
* any subshells it spawns). Only qzeroded.x64 maps this address. For the
* real qzeroded process, retry briefly so constructor/load ordering does
* not cause a false negative before the text page appears. */
if (!page_is_mapped(page_start, page_size)) {
if (!is_qzeroded_process())
return;
if (!wait_for_target_page(page_start, page_size)) {
fprintf(stderr,
"[force_rate] target page %p not mapped after retry; patch skipped\n",
page_start);
return;
}
}

if (mprotect(page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {
perror("[force_rate] mprotect failed");
Expand All @@ -51,6 +105,7 @@ static void patch_sys_islanaddress(void)
/* Write: MOV EAX, 1; RET */
uint8_t patch[] = { 0xB8, 0x01, 0x00, 0x00, 0x00, 0xC3 };
memcpy(target, patch, sizeof(patch));
__builtin___clear_cache((char *)target, (char *)target + sizeof(patch));

if (mprotect(page_start, page_size, PROT_READ | PROT_EXEC) != 0)
perror("[force_rate] mprotect restore failed");
Expand Down
Binary file modified ql-assets/data/system-hooks/force_rate.so
Binary file not shown.
Loading