2 Commits

Author SHA1 Message Date
Luke Parker
c98d757c0f Ensure the signed arithmetic won't overflow, expand shells tested with (#701)
* Normalize naming for the stack size CI file

* Extend stack size CI with `posh` and `lksh`

`posh` is a derivative of `pdksh` explicitly intended for ensuring
Debian-policy-compliance.

`lksh` is more-POSIX-esque, legacy shell included along-side `mksh`.

* Ensure a signed long overflow won't occur

Also fixes `write_bytes` when the written bytes have alphabetical digits when
encoded as hexadecimal.

* Improve sh semantics in stack-size workflow
2025-12-09 04:03:24 -05:00
Luke Parker
6603100c7e Add a CI for increase_default_stack_size.sh
This runs whenever the script is modified, or weekly to ensure the CI doesn't
inadvertently decay (due to using the latest packages for a variety of shells).

This runs with `sh` (presumably `dash`), `ksh`, `bash`, `dash` (explicitly),
`zsh`, `ash` (Busybox), `hush` (Busybox), `mksh`, `yash`, and `brush`. While
none of these guarantee this script is POSIX-compliant, as a fully and
explicitly-only POSIX-compliant environment is not constructed, this does
reasonably test the script itself to be POSIX-compliant. The tools called have
been reviewed for being used to the POSIX standard (although not audited to
that degree).

The script itself is modified with the following changes for compliance with
POSIX:
1) `hexdump` is replaced with `od` (`od` suggested by @PlasmaPower)
2) `printf \xFF` replaced with octal escapes, as `\x` is not part of POSIX
3) `head -c` is replaced with `cut`, as the `-c` option is not standardized
   under POSIX (despite it being present for `tail`). This was identified by
   @PlasmaPower. As we used `head -c-2` to truncate the last two characters of
   a string, we now use `wc -c` for a `strlen` to enable the necessary
   arithmetic to calculate what two bytes in from the end of the string is.

This entire effort can be argued pointless, as we could simply run `monerod` on
Debian. This script is useful, the journey down the rabbithole of POSIX
compliance fascinating, and the methodology applicable to other potential
futures though (whether running binaries on Alpine or testing other `sh`
scripts for their portability). As part of this effort overall, our CI was
extended with `shellcheck` for all `sh` scripts in-tree, including all of our
existing `sh` scripts. That there is an actual, direct benefit past this
specific effort.
2025-12-09 00:57:26 -05:00
4 changed files with 124 additions and 13 deletions

View File

@@ -62,7 +62,7 @@ runs:
docker system prune -a --volumes
sudo apt remove -y *docker*
# Install uidmap which will be required for the explicitly installed Docker
sudo apt install uidmap
sudo apt install -y uidmap
if: runner.os == 'Linux'
- name: Update system dependencies

View File

@@ -208,7 +208,7 @@ jobs:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: shellcheck
run: |
sudo apt install shellcheck
sudo apt install -y shellcheck
find . -iname "*.sh" | while read -r script; do
shellcheck --enable=all --shell=sh --severity=info $script
done

93
.github/workflows/stack-size.yml vendored Normal file
View File

@@ -0,0 +1,93 @@
name: Check Update Default Stack Size
on:
push:
paths:
- "orchestration/increase_default_stack_size.sh"
pull_request:
paths:
- "orchestration/increase_default_stack_size.sh"
workflow_dispatch:
# Also run weekly to ensure this doesn't inadvertently decay
schedule:
- cron: "0 0 * * 1"
jobs:
stack_size:
strategy:
matrix:
os: [ubuntu-latest, ubuntu-24.04, ubuntu-22.04]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Download Monero
uses: ./.github/actions/monero
- name: Verify expected behavior
shell: bash
run: |
cp /usr/bin/monerod monerod
STACK=$((8 * 1024 * 1024))
cp monerod monerod-chelf
git clone https://github.com/Gottox/chelf
cd chelf
git checkout b2994186cea7b7d61a588fd06c1cc1ae75bcc21a
make
./chelf -s "$STACK" ../monerod-chelf
cd ..
cp monerod monerod-muslstack
GOBIN=$(pwd) go install github.com/yaegashi/muslstack@d19cc5866abce3ca59dfc1666df7cc97097d0933
./muslstack -s "$STACK" ./monerod-muslstack
sudo apt update -y
sudo apt install -y ksh bash dash zsh busybox mksh posh yash
sudo ln -s "$(which busybox)" /usr/bin/ash
sudo ln -s "$(which busybox)" /usr/bin/hush
cargo install brush-shell
for shell in sh ksh bash dash zsh ash hush mksh lksh posh yash brush; do
cp monerod monerod-idss-$shell
ln -s "$(which $shell)" sh
./sh ./orchestration/increase_default_stack_size.sh monerod-idss-$shell
rm ./sh
done
sha256() {
sha256sum "$1" | cut -d' ' -f1
}
CHELF=$(sha256 monerod-chelf)
find . -iname "monerod-*" | while read -r bin; do
BIN=$(sha256 "$bin")
if [ ! "$CHELF" = "$BIN" ]; then
echo "Different artifact between monerod-chelf ($CHELF) and $bin ($BIN)"
exit 1
fi
done
read_stack() {
STACK_INFO=$(readelf "$1" -l | grep STACK -A1)
MEMSZ=$(printf "%s\n" "$STACK_INFO" | tail -n1 | sed -E s/^[[:space:]]*//g | cut -f2 -d' ')
printf "%i" $((MEMSZ))
}
INITIAL_STACK=$(read_stack monerod)
if [ "$INITIAL_STACK" -ne "0" ]; then
echo "Initial \`PT_GNU_STACK\` wasn't 0"
exit 2
fi
UPDATED_STACK=$(read_stack monerod-chelf)
if [ "$UPDATED_STACK" -ne "$STACK" ]; then
echo "Updated \`PT_GNU_STACK\` ($UPDATED_STACK) wasn't 8 MB ($STACK)"
exit 3
fi
# Only one byte should be different due to the bit pattern of 8 MB
BYTES_DIFFERENT=$(cmp -l monerod monerod-chelf | wc -l || true)
if [ "$BYTES_DIFFERENT" -ne 1 ]; then
echo "More than one byte was different between the two binaries"
exit 4
fi

View File

@@ -26,23 +26,29 @@ hex() {
read_bytes() {
dd bs=1 skip="$1" count="$2" if="$ELF" 2> /dev/null | hex
}
hex_to_octal() {
HEX=$(printf "%s" "$1" | tr "[:lower:]" "[:upper:]")
printf "ibase=16; obase=8; %s\n" "$HEX" | bc
}
write_bytes() {
POS=$1
BYTES=$2
while [ ! "$BYTES" = "" ]; do
NEXT=$(printf "%s" "$BYTES" | head -c2)
NEXT=$(printf "%s" "$BYTES" | cut -c-2)
# Advance to the third byte, as in, after the first two bytes
BYTES=$(printf "%s" "$BYTES" | tail -c+3)
NEXT=$(hex_to_octal "$NEXT")
# shellcheck disable=SC2059
printf "\x$NEXT" | dd conv=notrunc bs=1 seek="$POS" of="$ELF" 2> /dev/null
printf \\"$NEXT" | dd conv=notrunc bs=1 seek="$POS" of="$ELF" 2> /dev/null
POS=$((POS + 1))
done
}
# Magic
MAGIC=$(read_bytes 0 4)
EXPECTED_MAGIC=$(printf "\x7ELF" | hex)
# shellcheck disable=SC2059
EXPECTED_MAGIC=$(printf \\"$(hex_to_octal 7f)"ELF | hex)
if [ ! "$MAGIC" = "$EXPECTED_MAGIC" ]; then
echo "Not ELF"
exit 2
@@ -73,6 +79,12 @@ read_integer_by_offset() {
OFFSET=$(value_per_bits "$1" "$2")
BYTES=$(read_bytes "$OFFSET" "$3")
BYTES=$(swap_native_endian "$BYTES")
BYTES=$(printf "%s" "$BYTES" | tr "[:lower:]" "[:upper:]")
LESS_THAN_SANITY=$(printf "ibase=16; if(%s < 6FFFFFFF)1;\n" "$BYTES" | bc)
if [ ! "$LESS_THAN_SANITY" = "1" ]; then
echo "Integer value is approximate to 2**31, risking a signed long overflow"
exit 4
fi
printf "%i" $(( 0x$BYTES ))
}
@@ -83,7 +95,7 @@ case $LITTLE_ENDIAN in
"02") LITTLE_ENDIAN=0;;
*)
echo "Not little- or big- endian"
exit 4
exit 5
;;
esac
@@ -101,22 +113,26 @@ swap_native_endian() {
return
fi
while [ ! "$BYTES" = "" ]; do
while :; do
printf "%s" "$BYTES" | tail -c2
BYTES=$(printf "%s" "$BYTES" | head -c-2)
NEW_LENGTH=$(( $(printf "%s" "$BYTES" | wc -c) - 2 ))
if [ "$NEW_LENGTH" -eq 0 ]; then
break
fi
BYTES=$(printf "%s" "$BYTES" | cut -c-$NEW_LENGTH)
done
}
ELF_VERSION=$(read_bytes 6 1)
if [ ! "$ELF_VERSION" = "01" ]; then
echo "Unknown ELF Version ($ELF_VERSION)"
exit 5
exit 6
fi
ELF_VERSION_2=$(read_bytes $((0x14)) 4)
if [ ! "$ELF_VERSION_2" = "$(swap_native_endian 00000001)" ]; then
echo "Unknown secondary ELF Version ($ELF_VERSION_2)"
exit 6
exit 7
fi
# Find where the program headers are
@@ -125,7 +141,7 @@ PROGRAM_HEADER_SIZE=$(value_per_bits 0x20 0x38)
DECLARED_PROGRAM_HEADER_SIZE=$(read_integer_by_offset 0x2a 0x36 2)
if [ ! "$PROGRAM_HEADER_SIZE" -eq "$DECLARED_PROGRAM_HEADER_SIZE" ]; then
echo "Unexpected size of a program header ($DECLARED_PROGRAM_HEADER_SIZE)"
exit 7
exit 8
fi
program_header_start() {
printf "%i" $((PROGRAM_HEADERS_OFFSET + ($1 * PROGRAM_HEADER_SIZE)))
@@ -144,7 +160,7 @@ while [ "$NEXT_PROGRAM_HEADER" -ne -1 ]; do
NEXT_PROGRAM_HEADER=$(( NEXT_PROGRAM_HEADER - 1 ))
PROGRAM_HEADER=$(read_program_header "$THIS_PROGRAM_HEADER")
HEADER_TYPE=$(printf "%s" "$PROGRAM_HEADER" | head -c8)
HEADER_TYPE=$(printf "%s" "$PROGRAM_HEADER" | cut -c-8)
HEADER_TYPE=$(swap_native_endian "$HEADER_TYPE")
# `PT_GNU_STACK`
# https://github.com/torvalds/linux/blob/c2f2b01b74be8b40a2173372bcd770723f87e7b2/include/uapi/linux/elf.h#L41
@@ -153,6 +169,8 @@ while [ "$NEXT_PROGRAM_HEADER" -ne -1 ]; do
fi
FOUND=1
# This line is the only line really risking an arithmetic overflow, yet the bound on the start of
# the section, combined with a maximum section length of `0xffff * 0x38`, makes this fit
MEMSZ_OFFSET=$(( $(program_header_start "$THIS_PROGRAM_HEADER") + $(value_per_bits 0x14 0x28) ))
MEMSZ_LEN=$(value_per_bits 4 8)
# `MEMSZ_OFFSET MEMSZ_OFFSET` as we've already derived it depending on the amount of bits
@@ -169,7 +187,7 @@ done
if [ "$FOUND" -eq 0 ]; then
echo "\`PT_GNU_STACK\` program header not found"
exit 8
exit 9
fi
echo "All instances of \`PT_GNU_STACK\` patched to be at least 8 MB"