mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-12 09:28:09 +00:00
build: fix native GHC ELF section alignment
This commit is contained in:
1 parent
2951153b49
commit
00afd113ad
11 files changed
+206
-5
No files matched your search
@@ -88,6 +88,11 @@ jobs:
|
||||
- name: Show outputs from this checkout
|
||||
run: make nix-list
|
||||
|
||||
- name: Check native Linux GHC ELF section alignment
|
||||
if: ${{ matrix.system == 'x86_64-linux' && matrix.target == 'native' }}
|
||||
timeout-minutes: 120
|
||||
run: nix build .#checks.x86_64-linux.ghc-elf-alignment --no-link -L --max-jobs 1 --timeout "$NIX_BUILD_TIMEOUT"
|
||||
|
||||
# Use the compiler fork in flake.lock, including both Android ABIs.
|
||||
# Stock GHC installed by ghcup is not interchangeable with this compiler.
|
||||
- name: Build existing Haskell outputs
|
||||
|
||||
@@ -680,6 +680,13 @@
|
||||
# build all packages in hydra.
|
||||
hydraJobs = packages;
|
||||
|
||||
checks = pkgs.lib.optionalAttrs (system == "x86_64-linux") {
|
||||
ghc-elf-alignment = import ./scripts/nix/check-elf-alignment.nix {
|
||||
inherit pkgs;
|
||||
compiler = (compilerSelection pkgs).ghc914-sh;
|
||||
};
|
||||
};
|
||||
|
||||
devShell = let
|
||||
updateCmd = pkgs.writeShellApplication {
|
||||
name = "update-sha256map";
|
||||
|
||||
@@ -9,6 +9,20 @@ platform; Android ARMv7 does not require a separate branch or tag.
|
||||
build context, including auxiliary iserv projects, so the pinned compiler can use
|
||||
its existing cache. Application dependencies keep the project's current packages.
|
||||
|
||||
On Linux x86_64, `native-rts-project.nix` patches the final compiler's RTS ELF
|
||||
loader to honor section alignment even when its file offset is unaligned.
|
||||
The stage2 RTS package patch preserves the configured source, stage1 compiler,
|
||||
plan, and package IDs, but rebuilds the final compiler and its dependent slices.
|
||||
Native, auxiliary, and cross compiler selection share this patched compiler.
|
||||
Darwin and ARM compiler configurations are unchanged. This native compiler fix
|
||||
does not patch target RTS sources or target iserv runtimes, including musl.
|
||||
|
||||
The focused regression runs before the native x86_64 Linux CI build. To run it
|
||||
from this checkout, use `nix build .#checks.x86_64-linux.ghc-elf-alignment -L`.
|
||||
It loads the same 8192-byte C object with aligned and deliberately packed section
|
||||
file offsets, requiring both to retain 64-byte memory alignment and their data.
|
||||
It is a flake check, separate from the 21 existing Hydra artifact outputs.
|
||||
|
||||
Cross projects add `ghc-internal` to the Cabal dependencies of the pinned
|
||||
`composition-1.0.2.2` source. This preserves its deliberate avoidance of `base`
|
||||
while making GHC 9.14's wired-in types available in isolated target package
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{ pkgs, compiler }:
|
||||
assert pkgs.stdenv.hostPlatform.isLinux && pkgs.stdenv.hostPlatform.isx86_64;
|
||||
assert pkgs.stdenv.buildPlatform.config == pkgs.stdenv.hostPlatform.config;
|
||||
pkgs.runCommandCC "ghc914-elf-section-alignment-check" {
|
||||
nativeBuildInputs = [ pkgs.python3 ];
|
||||
} ''
|
||||
set -euo pipefail
|
||||
mkdir -p "$out"
|
||||
$CC -ffreestanding -std=c11 -Werror=vla -pedantic -Wall -Wextra -O2 \
|
||||
-c ${./elf-alignment/probe.c} -o "$out/original.o"
|
||||
python3 ${./elf-alignment/repack.py} \
|
||||
"$out/original.o" "$out/packed.o" "$out/elf-layout.json"
|
||||
# A valid ET_REL section's file offset need not match its memory alignment.
|
||||
# Both paths must resolve the same bytes at a 64-byte-aligned address.
|
||||
for variant in original packed; do
|
||||
${compiler}/bin/ghc -ignore-dot-ghci -package ghci ${./elf-alignment/Probe.hs} \
|
||||
-e "probeObject \"$out/$variant.o\"" | tee "$out/$variant.txt"
|
||||
grep -Fx '(0,1,0)' "$out/$variant.txt"
|
||||
done
|
||||
printf '%s\n' 'PASS: original and packed ELF sections honor 64-byte alignment' \
|
||||
| tee "$out/result.txt"
|
||||
''
|
||||
@@ -0,0 +1,22 @@
|
||||
module Probe (probeObject) where
|
||||
|
||||
import Control.Monad (unless)
|
||||
import Data.Word (Word8)
|
||||
import Foreign.Ptr (castPtr, ptrToWordPtr)
|
||||
import Foreign.Storable (peekElemOff)
|
||||
import GHCi.ObjLink (ShouldRetainCAFs (RetainCAFs), initObjLinker, loadObj, lookupSymbol, resolveObjs)
|
||||
|
||||
probeObject :: FilePath -> IO ()
|
||||
probeObject object = do
|
||||
initObjLinker RetainCAFs
|
||||
loadObj object
|
||||
resolved <- resolveObjs
|
||||
unless resolved (fail "resolveObjs returned False")
|
||||
address <- lookupSymbol "probe"
|
||||
case address of
|
||||
Nothing -> fail "probe symbol not found"
|
||||
Just pointer -> do
|
||||
first <- peekElemOff (castPtr pointer) 0 :: IO Word8
|
||||
lastByte <- peekElemOff (castPtr pointer) 8191 :: IO Word8
|
||||
unless (first == 1 && lastByte == 0) (fail "probe payload changed")
|
||||
print (ptrToWordPtr pointer `mod` 64, first, lastByte)
|
||||
@@ -0,0 +1,3 @@
|
||||
#include <stdint.h>
|
||||
|
||||
_Alignas(64) const uint8_t probe[8192] = {1};
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Repack the tiny ELF64 probe without changing section data or alignment."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import pathlib
|
||||
import struct
|
||||
import sys
|
||||
|
||||
original_path, packed_path, report_path = map(pathlib.Path, sys.argv[1:])
|
||||
original = original_path.read_bytes()
|
||||
assert original[:6] == b"\x7fELF\x02\x01" # ELF64, little endian
|
||||
header_format = "<16sHHIQQQIHHHHHH"
|
||||
section_format = "<IIQQQQIIQQ"
|
||||
header = list(struct.unpack_from(header_format, original))
|
||||
assert header[1:3] == [1, 62] # ET_REL, EM_X86_64
|
||||
assert header[10] == 0 # No program headers in this fixture.
|
||||
section_offset, entry_size, count, string_index = header[6], *header[11:14]
|
||||
assert entry_size == struct.calcsize(section_format) == 64
|
||||
sections = [
|
||||
list(struct.unpack_from(section_format, original, section_offset + i * entry_size))
|
||||
for i in range(count)
|
||||
]
|
||||
strings = sections[string_index]
|
||||
names = original[strings[4] : strings[4] + strings[5]]
|
||||
|
||||
|
||||
def section_name(section):
|
||||
return names[section[0] :].split(b"\0", 1)[0].decode()
|
||||
|
||||
|
||||
rodata = next(section for section in sections if section_name(section) == ".rodata")
|
||||
assert rodata[5] == 8192 and rodata[8] == 64 and rodata[4] % 64 == 0
|
||||
cut = rodata[4]
|
||||
padding = 8
|
||||
packed = bytearray(original[:cut] + b"\0" * padding + original[cut:])
|
||||
header[6] += padding
|
||||
struct.pack_into(header_format, packed, 0, *header)
|
||||
observations = []
|
||||
for i, section in enumerate(sections):
|
||||
moved = section.copy()
|
||||
if moved[4] >= cut and moved[4] != 0:
|
||||
moved[4] += padding
|
||||
struct.pack_into(section_format, packed, header[6] + i * entry_size, *moved)
|
||||
if section[1] not in (0, 8): # SHT_NULL/SHT_NOBITS have no file payload.
|
||||
assert original[section[4] : section[4] + section[5]] == packed[
|
||||
moved[4] : moved[4] + moved[5]
|
||||
], section_name(section)
|
||||
observations.append(
|
||||
{
|
||||
"name": section_name(section),
|
||||
"size": section[5],
|
||||
"alignment": section[8],
|
||||
"originalOffset": section[4],
|
||||
"packedOffset": moved[4],
|
||||
}
|
||||
)
|
||||
packed_path.write_bytes(packed)
|
||||
report = {
|
||||
"paddingBytes": padding,
|
||||
"allSectionPayloadsUnchanged": True,
|
||||
"symbolsAndRelocationsUnchanged": True,
|
||||
"sha256": {
|
||||
path.name: hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
for path in (original_path, packed_path)
|
||||
},
|
||||
"sections": observations,
|
||||
}
|
||||
report_path.write_text(json.dumps(report, indent=2) + "\n")
|
||||
print(json.dumps(report, indent=2))
|
||||
@@ -0,0 +1,16 @@
|
||||
--- a/linker/Elf.c
|
||||
+++ b/linker/Elf.c
|
||||
@@ -1008,8 +1008,11 @@
|
||||
}
|
||||
// use the m32 allocator if either the image is not mapped
|
||||
// (i.e. we cannot map the sections separately), or if the section
|
||||
- // size is small.
|
||||
- else if (!oc->imageMapped || size < getPageSize() / 3) {
|
||||
+ // size is small. A section's file offset need not satisfy its
|
||||
+ // in-memory alignment. mmap preserves the offset within the page,
|
||||
+ // so copy such sections into aligned storage as well.
|
||||
+ else if (!oc->imageMapped || size < getPageSize() / 3
|
||||
+ || (align > 1 && offset % align != 0)) {
|
||||
bool executable = kind == SECTIONKIND_CODE_OR_RODATA;
|
||||
m32_allocator *allocator = executable ? oc->rx_m32 : oc->rw_m32;
|
||||
// Correctly align the section. This is particularly important for
|
||||
@@ -2,8 +2,13 @@
|
||||
system:
|
||||
# Keep haskell.nix's existing host pins, including Intel Darwin's 26.05 pin.
|
||||
# Its flake omits ARM64 Linux, so construct that native set from the same inputs.
|
||||
haskellNix.legacyPackages.${system} or (import haskellNix.inputs.nixpkgs {
|
||||
localSystem = { inherit system; };
|
||||
config = haskellNix.config;
|
||||
overlays = [ haskellNix.overlay ];
|
||||
})
|
||||
let
|
||||
native = haskellNix.legacyPackages.${system} or (import haskellNix.inputs.nixpkgs {
|
||||
localSystem = { inherit system; };
|
||||
config = haskellNix.config;
|
||||
overlays = [ haskellNix.overlay ];
|
||||
});
|
||||
in
|
||||
if system == "x86_64-linux"
|
||||
then native.appendOverlays [ (import ./native-rts-overlay.nix) ]
|
||||
else native
|
||||
@@ -0,0 +1,17 @@
|
||||
final: prev:
|
||||
let
|
||||
inherit (prev.stdenv) buildPlatform hostPlatform targetPlatform;
|
||||
active = hostPlatform.isLinux && hostPlatform.isx86_64
|
||||
&& buildPlatform.config == hostPlatform.config
|
||||
&& hostPlatform.config == targetPlatform.config;
|
||||
in prev.lib.optionalAttrs active {
|
||||
haskell-nix = prev.haskell-nix // {
|
||||
# Cover the compiler's own stage2 project as well as evalWith variants.
|
||||
# Reusing the public project hook keeps native/auxiliary/cross users on
|
||||
# one compiler rather than patching only an application's final link.
|
||||
cabalProject' = projectModule: prev.haskell-nix.cabalProject' (
|
||||
[ ./native-rts-project.nix ]
|
||||
++ (if builtins.isList projectModule then projectModule else [ projectModule ])
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
let
|
||||
inherit (pkgs.stdenv) buildPlatform hostPlatform targetPlatform;
|
||||
in
|
||||
lib.mkIf (
|
||||
hostPlatform.isLinux && hostPlatform.isx86_64
|
||||
&& buildPlatform.config == hostPlatform.config
|
||||
&& hostPlatform.config == targetPlatform.config
|
||||
&& config.name == "ghc914-sh-stage2"
|
||||
&& config.compiler-nix-name == "ghc914-sh-stage1"
|
||||
) {
|
||||
modules = [
|
||||
({ lib, ... }: {
|
||||
# Local RTS package patches reach the v2 tarball without changing the
|
||||
# compiler's configured source, stage1 bootstrap, plan, or package IDs.
|
||||
# A valid ELF section may have an unaligned file offset; the loader
|
||||
# must still honor its declared memory alignment for TH object code.
|
||||
packages.rts.patches = lib.mkAfter [ ./ghc914-rts-elf-section-alignment.patch ];
|
||||
})
|
||||
];
|
||||
}
|
||||
Reference in new issue
Block a user