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
56 changes: 56 additions & 0 deletions docs/linux.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,62 @@ Windows does not need this policy: proxy DLL and manual DLL injection load UE4SS

Marker-free manual `LD_PRELOAD` and explicit `dlopen` do not auto-start UE4SS by default. Use `run_ue4ss.sh` for the supported process-scoped workflow. Deliberate legacy compatibility can be enabled with `UE4SS_ALLOW_LEGACY_START=1`, but that mode retains normal Linux child inheritance and may initialize UE4SS in descendant or helper processes. If the user's original `LD_PRELOAD` already contains UE4SS, exact preservation takes precedence.

## SELinux preflight and game updates

Do not enable the global `execheap` boolean solely for UE4SS. Use a narrowly
scoped SELinux domain that permits executable trampoline memory only for the
intended UE4SS-enabled host process.

The launcher supports these SELinux preflight modes:

```bash
UE4SS_SELINUX_PREFLIGHT=off
UE4SS_SELINUX_PREFLIGHT=warn
UE4SS_SELINUX_PREFLIGHT=strict
```

The default is `warn`. A scoped deployment should also identify the expected
entrypoint type:

```bash
UE4SS_SELINUX_PREFLIGHT=strict \
UE4SS_EXPECTED_SELINUX_TYPE=palworld_ue4ss_exec_t \
"$stage/run_ue4ss.sh" \
--host-executable "$server" \
"$wrapper"
```

When SELinux is Enforcing, `strict` mode exits with status 8 if the host
executable does not have the expected type. This prevents a later executable
trampoline fault when the process fails to transition into its scoped domain.

Game updates and validation operations can replace the host executable with a
new inode. The replacement may receive the directory's default SELinux type
instead of the persistent custom entrypoint type.

Define a persistent file-context mapping once:

```bash
semanage fcontext -a \
-t palworld_ue4ss_exec_t \
'/srv/palworld/Pal/Binaries/Linux/PalServer-Linux-Shipping'
```

After every game update or validation, restore the configured context before
starting UE4SS:

```bash
restorecon -Fv \
/srv/palworld/Pal/Binaries/Linux/PalServer-Linux-Shipping

matchpathcon -V \
/srv/palworld/Pal/Binaries/Linux/PalServer-Linux-Shipping
```

Use `semanage fcontext -m` instead of `-a` when changing an existing mapping.
Do not rely on `chcon`; it changes the current label without defining the
persistent file-context rule.

## Diagnostics and logs

Set `UE4SS_DIAGNOSE=1` for a support-oriented startup report:
Expand Down
64 changes: 56 additions & 8 deletions packaging/linux/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,62 @@ When no wrapper is required:
-NoAsyncLoadingThread \
-UseMultithreadForDS

## SELinux preflight and game updates

Do not enable the global `execheap` boolean solely for UE4SS. Use a narrowly
scoped SELinux domain that permits executable trampoline memory only for the
intended UE4SS-enabled host process.

The launcher supports these SELinux preflight modes:

```bash
UE4SS_SELINUX_PREFLIGHT=off
UE4SS_SELINUX_PREFLIGHT=warn
UE4SS_SELINUX_PREFLIGHT=strict
```

The default is `warn`. A scoped deployment should also identify the expected
entrypoint type:

```bash
UE4SS_SELINUX_PREFLIGHT=strict \
UE4SS_EXPECTED_SELINUX_TYPE=palworld_ue4ss_exec_t \
"$stage/run_ue4ss.sh" \
--host-executable "$server" \
"$wrapper"
```

When SELinux is Enforcing, `strict` mode exits with status 8 if the host
executable does not have the expected type. This prevents a later executable
trampoline fault when the process fails to transition into its scoped domain.

Game updates and validation operations can replace the host executable with a
new inode. The replacement may receive the directory's default SELinux type
instead of the persistent custom entrypoint type.

Define a persistent file-context mapping once:

```bash
semanage fcontext -a \
-t palworld_ue4ss_exec_t \
'/srv/palworld/Pal/Binaries/Linux/PalServer-Linux-Shipping'
```

After every game update or validation, restore the configured context before
starting UE4SS:

```bash
restorecon -Fv \
/srv/palworld/Pal/Binaries/Linux/PalServer-Linux-Shipping

matchpathcon -V \
/srv/palworld/Pal/Binaries/Linux/PalServer-Linux-Shipping
```

Use `semanage fcontext -m` instead of `-a` when changing an existing mapping.
Do not rely on `chcon`; it changes the current label without defining the
persistent file-context rule.

## Diagnostics

Enable the Linux startup diagnostic report with:
Expand All @@ -131,11 +187,3 @@ Review:
Do not unload `libUE4SS.so` or native C++ mods with `dlclose`.

Stop the host game process to unload the loader and its native mods.

## SELinux

Do not enable global `execheap` solely for UE4SS.

The validated Palworld environment used a narrowly scoped SELinux domain and
policy for the isolated UE4SS-enabled server process. Deployments using SELinux
should apply an equally restricted local policy.
81 changes: 81 additions & 0 deletions tests/RunLinuxLauncherTests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,85 @@ if [[ ${non_regular_host_status} -eq 0 ]]; then
fi
grep -Fq 'host executable not found or not executable' <<<"${non_regular_host_output}"

selinux_mock_bin="${stage_directory}/selinux mock bin"
mkdir -p -- "${selinux_mock_bin}"

cat >"${selinux_mock_bin}/getenforce" <<'MOCK_GETENFORCE'
#!/usr/bin/env bash
printf '%s\n' Enforcing
MOCK_GETENFORCE

cat >"${selinux_mock_bin}/stat" <<'MOCK_STAT'
#!/usr/bin/env bash
printf '%s\n' \
"${MOCK_SELINUX_CONTEXT:-unconfined_u:object_r:user_home_t:s0}"
MOCK_STAT

chmod +x \
"${selinux_mock_bin}/getenforce" \
"${selinux_mock_bin}/stat"

selinux_warning_output="$(
env -u LD_PRELOAD \
PATH="${selinux_mock_bin}:${PATH}" \
MOCK_SELINUX_CONTEXT='unconfined_u:object_r:user_home_t:s0' \
UE4SS_DISABLE_AUTO_START=1 \
UE4SS_SELINUX_PREFLIGHT=warn \
"${launcher}" \
"${target}" \
2>&1
)"

grep -Fq \
"SELinux preflight: host executable type 'user_home_t'" \
<<<"${selinux_warning_output}"

grep -Fq \
"restore its persistent file context before launch" \
<<<"${selinux_warning_output}"

set +e

selinux_strict_output="$(
env -u LD_PRELOAD \
PATH="${selinux_mock_bin}:${PATH}" \
MOCK_SELINUX_CONTEXT='unconfined_u:object_r:user_home_t:s0' \
UE4SS_DISABLE_AUTO_START=1 \
UE4SS_SELINUX_PREFLIGHT=strict \
UE4SS_EXPECTED_SELINUX_TYPE=palworld_ue4ss_exec_t \
"${launcher}" \
"${target}" \
2>&1
)"

selinux_strict_status=$?

set -e

if [[ ${selinux_strict_status} -ne 8 ]]; then
echo "strict SELinux preflight returned ${selinux_strict_status}, expected 8" >&2
exit 12
fi

grep -Fq \
"does not match UE4SS_EXPECTED_SELINUX_TYPE='palworld_ue4ss_exec_t'" \
<<<"${selinux_strict_output}"

selinux_matching_output="$(
env -u LD_PRELOAD \
PATH="${selinux_mock_bin}:${PATH}" \
MOCK_SELINUX_CONTEXT='system_u:object_r:palworld_ue4ss_exec_t:s0' \
UE4SS_DISABLE_AUTO_START=1 \
UE4SS_SELINUX_PREFLIGHT=strict \
UE4SS_EXPECTED_SELINUX_TYPE=palworld_ue4ss_exec_t \
"${launcher}" \
"${target}" \
2>&1
)"

if grep -Fq 'SELinux preflight:' <<<"${selinux_matching_output}"; then
echo "strict SELinux preflight warned for the expected host type" >&2
exit 13
fi

rm -rf -- "${stage_directory}"
78 changes: 78 additions & 0 deletions tools/linux/run_ue4ss.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,82 @@ is_elf_file() {
[[ "$(LC_ALL=C od -An -t x1 -N4 -- "$1" | tr -d '[:space:]')" == "7f454c46" ]]
}

selinux_file_type() {
local context
local selinux_user
local selinux_role
local file_type
local selinux_level

context="$(stat -Lc '%C' -- "$1" 2>/dev/null || true)"

if [[ -z "${context}" || "${context}" == "?" ]]; then
return 1
fi

IFS=: read -r \
selinux_user \
selinux_role \
file_type \
selinux_level \
<<<"${context}"

if [[ -z "${file_type}" ]]; then
return 1
fi

printf '%s\n' "${file_type}"
}

selinux_preflight_problem() {
local mode="$1"
shift

echo "UE4SS launcher: SELinux preflight: $*" >&2
echo "UE4SS launcher: a game update may have replaced the labeled host executable; restore its persistent file context before launch." >&2

if [[ "${mode}" == "strict" ]]; then
exit 8
fi
}

selinux_preflight() {
local mode="${UE4SS_SELINUX_PREFLIGHT:-warn}"
local file_type
local expected_type="${UE4SS_EXPECTED_SELINUX_TYPE:-}"

case "${mode}" in
off|warn|strict)
;;
*)
echo "UE4SS launcher: invalid UE4SS_SELINUX_PREFLIGHT value '${mode}'; expected off, warn, or strict" >&2
exit 8
;;
esac

[[ "${mode}" != "off" ]] || return 0
command -v getenforce >/dev/null 2>&1 || return 0
[[ "$(getenforce 2>/dev/null || true)" == "Enforcing" ]] || return 0

file_type="$(selinux_file_type "${host_executable}" || true)"
[[ -n "${file_type}" ]] || return 0

if [[ -n "${expected_type}" && "${file_type}" != "${expected_type}" ]]; then
selinux_preflight_problem \
"${mode}" \
"host executable type '${file_type}' does not match UE4SS_EXPECTED_SELINUX_TYPE='${expected_type}'"
return 0
fi

case "${file_type}" in
user_home_t|home_root_t|default_t|unlabeled_t)
selinux_preflight_problem \
"${mode}" \
"host executable type '${file_type}' does not identify a scoped UE4SS SELinux entrypoint"
;;
esac
}

host_executable=""
if [[ "${1:-}" == "--host-executable" ]]; then
if [[ $# -lt 3 ]]; then
Expand Down Expand Up @@ -57,6 +133,8 @@ if ! is_elf_file "${host_executable}"; then
exit 7
fi

selinux_preflight

export UE4SS_LAUNCH_TARGET_EXE="$(canonical_path "${host_executable}")"
if [[ -v LD_PRELOAD ]]; then
export UE4SS_LAUNCH_LD_PRELOAD_WAS_SET=1
Expand Down