Repository navigation
Audit: all 63 bugs + the improvements (phases 1-4) - #19
Merged
Merged
Conversation
- 0x68/0x69 take Iz (imm16 under a 0x66 operand-size prefix, imm32 otherwise), not imm32; a 66-prefixed imul was decoded one byte short, so a hotpatch jump-back landed mid-instruction. - 0x0F 0x8x (Jcc rel32) has no ModRM byte; the old table decoded it as a ModRM instruction, truncating the length by three bytes. - The immediate now follows ModRM/SIB/displacement for ModRM opcodes, and the rel8/rel32 branch forms set their own immediate explicitly. Adds tests/test_x86_length.cpp pinning every encoding the shim can see.
…struction end - DecodePrologue now stops at a terminal instruction (ret/int3/hlt/ud2/jmp). A one-byte 'ret' function previously made the walk consume five bytes and patch into the neighbouring function (bug 3); it is now refused. - The decoder reports X86InsnInfo.terminal and flags the SIB RIP-relative form (mod=00, rm=100, base=101) as a relocatable [disp32], which it previously missed, leaving that displacement unrebased in the trampoline. - The trampoline rebase is now expressed against instruction start + length (the base the CPU uses), which is correct by construction. - Adds tests/test_hooks.cpp: hand-assembled functions pin the terminal refusal, the detour/trampoline/restore path, and a RIP-relative read.
- mem::WithWritable is now RAII (ScopedWritable): a throwing fn still restores protection, and the instruction cache for the span is flushed. - ScopedWritable saves and restores EVERY page's own protection, so a span that crosses an r-x and an r-- mapping no longer has one page's protection stamped over the whole range. - mem::Protect refuses when the current protection cannot be read instead of falling back to the requested protection (which re-applied RWX on restore). - /proc/self/maps is parsed with getline, so a long path cannot be split and its tail parsed as a bogus mapping. - Adds tests/test_mem.cpp pinning per-page restore and fail-closed behaviour.
- Intra-span branch detection: a rel32 branch from inside the overwritten prologue into its own middle is refused (bug 4). - int3-first patch protocol: byte 0 becomes 0xCC, the rest of the jump is written, then byte 0 is swapped to 0xE9, with the icache flushed at each step, so no thread observes a torn jump (bug 6, improvement 12). - Install verifies the bytes it wrote and fails closed when they do not match, and the WithWritable result is honoured (bug 5). - Trampoline pools are mapped read-write, filled, then switched to read-execute before the patch that reaches them is published, so a pool page is never writable and executable at once (bug 10). - MAP_FIXED_NOREPLACE is only a hint on pre-4.17 kernels: the returned address is compared with the request and a mismatch is unmapped instead of registered (bug 7). The outward scan is capped and stepped, not a page-by-page walk of 1.75 GiB (bug 8). - Slots come from a per-pool free list and are returned on unpatch, so hot reloads reuse space (bug 9); unpatch also refuses to clobber a prologue that no longer holds our jump. - HookManager::Backend()/BackendEmulateShim() take a lock (bug 11). - New diagnostics: BERRY_ShimListHooks and BERRY_ShimHookStats. - Makefile: track build/tools/*.d so a header change cannot leave the CLI objects stale and silently mismatched with the core objects.
- Chunked transfer-encoding is decoded (size lines and terminators removed) and Content-Length is enforced: a short body is flagged truncated rather than returned as a complete one (bugs 30, 31). - The URL and every header name/value are validated for CR/LF/NUL at the C++ layer and refused, and a caller's Host/Content-Length/Connection/ Transfer-Encoding is dropped so it cannot be duplicated (bug 32). - The authority is parsed properly: [IPv6] literals, userinfo, and a port range check that rejects junk, 0 and out-of-range values instead of truncating them (bugs 33, 34). - Host carries a non-default (or explicitly written) port (bug 35); an empty POST/PUT/PATCH still sends Content-Length: 0 (bug 36). - Status and header parsing is strict: the HTTP/1.x prefix and three digits are required, duplicate fields are combined, obsolete line folding is honoured, and trailing whitespace is trimmed (bug 37). - Adds a framing test server and tests for each case.
- Handshake: a random 16-byte nonce from the CSPRNG, Sec-WebSocket-Accept verified against base64(SHA1(key+GUID)), and Upgrade/Connection headers required alongside an exact 101 status (bugs 38-40). WsAcceptFor exposes the accept for tests; the loopback server now computes it. - Frames: a fresh CSPRNG masking key per frame, and the whole send is serialized under sendMutex_ so concurrent SendFrame calls cannot interleave (bugs 41, 42). - Bytes after the handshake terminator are carried into the frame reader (rxBuf_), so a frame arriving with the 101 is not lost (bug 43), and every handshake failure path closes the socket and sets err_ (bug 44). - Close sends a masked frame with a validated code and reason (1005/1006/ 1004/1015 refused), and waits briefly for the peer's close (bug 45). - The frame parser is strict for a client: a masked server frame, a non-minimal length and a set top length bit are Invalid (bug 46), and the reader closes 1002 on a protocol error and 1009 on an oversize frame with no desync (bug 47).
…ation (bugs 53-58) - Room token verification fails closed: an empty backend key refuses every token unless the deployment sets allowUnsigned explicitly, so a missing key is never read as "no signature needed" (bug 53). - expires_at is required and must be a positive integer, so a token cannot be minted to never expire (bug 54). - relay_port and expires_at are checked to be integral and in range instead of being truncated or cast from a double (bug 55). - The relay host is validated (no CR/LF/NUL/scheme/path) and a loopback, link-local, private or CGNAT address is refused unless allowPrivateRelayHost is set (bug 56). - LanCosmeticTransport's sender tag comes from the CSPRNG rather than std::random_device, which is deterministic on some standard library builds and would make two clients drop each other's packets (bug 57). - VoiceSession gains Zeroize (wipes the key), and ClearSessions/RotateEphemeral wipe every session key and the ephemeral secret, so a rotated or dropped key does not linger in freed heap (bug 58). VoiceZeroizeString is the shared primitive.
…impr 16-32)
Parsing (bugs 14, 15; improvement 23):
- Half-nibble wildcards ("A?", "?B"), IDA/Ghidra "??", and \x-escaped bytes
are supported, each producing one byte with a bit-level fixed mask. A single
hex digit is now a parse error instead of silently reading "F" as 0xF0, and
Parse returns a precise error string.
Maps / module identity (bugs 16-18, 24, 25; improvements 18, 28):
- Adjacent same-file segments accumulate their size instead of collapsing to
the last one; the path is trimmed of padding spaces; execute-only mappings
are kept and flagged; the parse is cached and invalidated by Refresh (with a
generation counter) so a dlopen is visible and an unmap does not dangle.
Build fingerprint (bugs 21, 22; improvements 19-22):
- The fingerprint now comes from the file on disk: the ELF NT_GNU_BUILD_ID plus
a SHA-256 of the file, with the module dev+inode identity. The gate compares
ASLR-stable identity (build-id / inode / content hash) instead of the load
base, so it survives ASLR and is not confused by our own hooks.
arm64 (bugs 27-29; improvement 26):
- IsAdrp tests the real encoding so all four immlo forms match; DecodeAddImm
uses the single sh bit (<<12); ResolveAdrpAdd returns 0 rather than a bare
page when no matching add is found; FindAdrpXrefs is bounds-checked; typed
bl / adrp+add xref helpers added.
Scanner performance + diagnostics (improvements 16, 17, 24, 25, 29, 31):
- memchr-anchored scan on the rarest fixed byte; FindUnique reports the match
count; ClosestMatch reports the nearest candidate and differing bit count;
FindGuarded wraps a scan in a SIGSEGV/SIGBUS guard; FindParallel scans ranges
on a small pool with cancellation.
- SigCache releases its mutex during a scan, rebuilds the scanner when the maps
change, and Count() takes the lock.
Tooling (improvement 30): berry-sigcheck validates a signature table (or a
single pattern) against an extracted module offline, wired into make test.
- Promise combinators: async.all, async.race and async.timeout are now installed as capability-free bindings over the scheduler (improvement 71), each pinned by tests. - The IDL gains since/removed version tags on a binding, and idlgen gains a breaking-change linter (--breaking-diff, wired to a make idl-breaking target) that flags a removal without a `removed` marker, an arity change, a return change and a capability change, while allowing additions (improvement 77). - idlgen emits two new machine-readable artefacts: generated/capabilities.json (the capability catalogue for tooling and a consent UI) and generated/manifest.schema.json (a JSON Schema for a script manifest with a closed capability enum) (improvement 80). - The generated BindingSpec carries since/removed, and test_idl pins them. This is a coherent slice of the Phase 4 roadmap; the remaining items (generated argument validation, generated fuzz harnesses, golden-file tests, luau-analyze in CI, DAP server, source maps, sandboxed require, secret store, replay determinism, timers, structured async scopes, memory accounting, scripttest coverage, lint pack, doc-example CI, extra-language stubs) are larger feature builds left for follow-up.
ASan caught a heap-buffer-overflow in ResolveAdrpAdd: the look-ahead read up to seven instructions past the end of a short buffer. maxInsns now bounds the read and FindAdrpXrefs passes the instructions actually left in the module. Adds a test for the bounded pair resolution.
- The ELF constructor no longer does the attach itself: it schedules the attach on a worker thread, so no signature scan, hook install or thread creation runs under the dynamic-loader lock (bug 49). The first exported call waits (bounded) for the scheduled attach, so the observable contract after dlopen is unchanged. - atexit detaches cleanly, so hooks are removed and threads joined before the code they point into is unmapped (bug 50). - Every exported C entry catches all exceptions and reports failure instead of letting std::bad_alloc call std::terminate (bug 51). - BERRY_IsAttached/BERRY_Inject/BERRY_InjectWithConfig now return C int, not C++ bool: a dlsym'd C caller declared them as int (*)(void), and a 1-byte bool return left the result register's upper bytes undefined, so the value could read as garbage. This was a latent ABI bug the exception wrapper exposed. - The logger level is atomic, formatting happens outside the lock, and the output sink is called outside the lock; a long line grows the buffer instead of being silently truncated at 1 KiB (bug 52).
- url.cpp: an embedded IPv4 in an IPv6 literal fills two 16-bit words but was indexed by group number, so every later word was misplaced; the fill now advances a word cursor and validates that all eight words were written (bug 59). - Vec3::Angle uses a defined kPi constant instead of M_PI (non-standard and a double that promoted the expression), and the two per-file kPi copies in radial.cpp / touch.cpp are folded onto the shared one (bug 60). - The instruction budget now counts REAL VM instructions via Luau single-step (a per-instruction debug hook), with safepoints tracked separately, so a straight-line loop with no call/back-edge safepoint is still bounded; native binding time is charged against the wall clock so a blocking call cannot hide behind a cheap safepoint count (bug 61). New test pins both. - fs.cpp directory walks openat() the directory instead of dup()ing it (a dup shares the offset, so a second walk saw an empty directory), and the recursion is capped (bug 62).
- Crash handler on an alternate signal stack (SIGSEGV/SIGBUS/SIGABRT/SIGILL/ SIGFPE/SIGSYS) writes a report with a backtrace, the fault address and the last log line to app-private storage, then re-raises so the OS sees the real signal. Async-signal-safe (write/open/backtrace only). A real SIGSEGV is pinned by a fork-based test (improvement 87). - A consecutive-startup-crash counter lets the next boot detect a crash loop and enter safe mode; BeginStartup/MarkStartupHealthy/SafeModeForced are the API (improvement 88). - Metrics registry: counters, gauges and a bucketed histogram with a snapshot view for tooling (improvement 91).
- Logger: per-module level overrides and a per-module per-second rate limit (LogTagged), so a hot subsystem can be quieted or throttled without touching the global level (improvement 90). - SECURITY.md: the threat model — the injection trust boundary, the script sandbox and its fail-closed gates, the network egress policy, the LAN voice/cosmetic channels, the blind relay, and the failure/recovery story (improvement 95). - .github/workflows/ci.yml: a matrix over g++/clang++ x test/sanitize, an arm64 qemu job, and a static-analysis job; TSan is documented as excluded with the reason (improvement 96). - fuzz/fuzz_targets.cpp: libFuzzer entry points over ParseWsFrame, the signature parser, the URL parser and the value codec, wired to fuzz: clang++ not available; skipped (improvement 97). - tools/gen-sbom.py + == wrote build/sbom.cdx.json ==: a CycloneDX 1.5 SBOM from the vendored dependency pins (improvement 100).
…-mode boot - ConfigFile: a JSON config with fail-closed validation (an unknown key, a wrong type or an out-of-range value is refused, not ignored) and an explicit BERRY_* environment allow-list (improvement 89). Pinned by test_configfile. - FrameBudget: a per-subsystem rolling frame-time budget that warns at most once a second when its mean overruns, so a slow subsystem is visible without flooding the log (improvement 92). - The engine boot now installs the crash handler, consults the consecutive-startup-crash counter, and enters safe mode (hooks and scripts off) after two crashed starts; a successful attach clears the counter (improvement 88).
- idlgen --golden-test proves every generated artefact is deterministic, ends with a newline and has no trailing whitespace; wired into idl-check (improvement 81). - The network result table carries a machine-readable error_kind enum (bad_url, refused, not_granted, transport, timeout, bad_redirect, too_many_redirects) alongside the human error string, so a script branches on the enum rather than matching text; documented in the IDL (improvement 82). - idlgen emits generated/berry_client.py, a transport-agnostic Python client stub with one method per binding (improvement 83).
- Profiler::ExportChromeTrace emits the span stats as Chrome trace-event JSON (complete "X" events on one lane), so a profile opens in chrome://tracing or Perfetto (improvement 58). Pinned by test_profiler. - TimerScheduler: a drift-corrected interval scheduler that catches up whole intervals after a stall instead of drifting, caps the catch-up burst, and handles a callback that cancels its own timer (improvement 70). Pinned by test_timers.
IScriptVm::MemoryByCategory attributes bytes to strings vs tables (and other), so a script or the profiler sees where its bytes go; the reference VM implements it and its total matches MemoryUsed. Pinned by test_script.
FrameQuota tracks the script's own rolling per-frame CPU time and warns (at most once a second) when the mean crosses its budget, so a persistently slow script is visible before a hard budget fault. A single spike does not warn; a zero budget disables it. Pinned by test_script.
- ScriptTestReport carries the discovered test set, unrun() and covered(), so a run reports which discovered tests a filter or the maxTests cap left unrun (improvement 74). Pinned by test_scripttest. - .luaurc (strict lint rules + the script globals) and stylua.toml (the formatter config) are the lint/format rule pack; make lint-scripts validates the .luaurc and runs luau-analyze / stylua when they are installed (improvement 75).
TaskScheduler gains OpenScope/CancelScope/CurrentScope/CloseScope/CountInScope: a task suspended while a scope is open for its script joins that scope, and cancelling the scope tears the whole group down at once (e.g. on script unload) while leaving other scripts' tasks alone. Pinned by test_async.
examples/basics.luau is a berry-run test suite, so make check-examples (part of make test) executes it and fails the build if a documented snippet stops working. Adds examples/README.md and wires check-examples into the test target.
- Removed the unused Berry::String alias, which shadowed ScriptValue::Type::String and was the one -Wshadow hit in first-party code (bug 63). - check-warnings compiles the first-party files with -Wshadow -Wconversion and fails on any warning; wired into make test so the baseline cannot regress (improvement 98).
A script declares its state shape with an exported numeric `state_schema`; ScriptHost::StateSchemaVersion reads it. The reloader only hands state across when the outgoing and incoming versions match, and reports ReloadResult::stateSchemaChanged otherwise, so a script that changes its state shape migrates explicitly in on_load instead of being fed an incompatible snapshot. Also fixes a real heap over-read ASan caught in Histogram::Buckets (the bucket array was one short of the bound count plus overflow).
- SigScanner::FindAllMulti scans every range once for the whole signature table, grouping signatures by their anchor byte so a table of N rules costs one walk instead of N. Exact (cross-checked against the single-signature scanner in test_sigscan) (improvement 16). - CMakeLists.txt builds the same artefacts from the same tools/sources.mk lists, for a CMake+Ninja+ccache workflow (improvement 99).
A null store is undefined behaviour that UBSan flags in the test itself; raise() delivers the same signal through the handler without an invalid access.
Each IDL event now carries a machine-readable payload_schema ("name(type, …)")
in the generated EventSpec, and events accept a since version tag, so a consumer
can branch on the schema rather than parse the doc string. Pinned by the idl and
script_bus suites.
SigCache::RegisterVersioned attaches an inclusive game-version range to a rule, and SetGameVersion selects the version the table is resolved against: a rule whose range does not contain the selected version resolves to Missing, so one table can carry the right signature for each build and pick automatically. An empty version selects only unbounded rules (fail closed). Pinned by test_sigscan.
A chain handler now records calls, faults and cumulative microseconds, and a handler that throws is caught, auto-disabled and skipped on later calls (the chain falls through to the original), so one broken handler cannot take the hook down. Reenable() restores it. Pinned by test_hook_chain.
… from disk - ScanRange falls back to a Horspool shift table on the last fixed byte when a pattern has no fully-fixed anchor, so a half-nibble/wildcard pattern still skips instead of walking byte by byte (improvement 17). - SigCache gains a build-id-keyed resolution cache (Load/SaveOffsetCache[File]): a warm start resolves every rule from module-relative offsets without scanning, and a cache whose build-id does not match the expected build is ignored outright, so a stale cache cannot resolve a rule against the wrong binary (improvement 19). - ProcessImages::ReadModuleBytes reads a module's bytes from its backing file, so an execute-only mapping (whose bytes cannot be read in memory) can be scanned from the on-disk copy (improvement 32). - Pinned by test_sigscan.
- A dependency-free DEFLATE inflater (RFC1951) with zlib (RFC1950) and gzip (RFC1952) framing, bounded by a decompressed-size cap so a zip bomb cannot exhaust memory. Pinned against Python-zlib vectors in test_inflate. - HttpRequest advertises Accept-Encoding: gzip, deflate and decodes a Content-Encoding response; a decode failure leaves the encoded bytes and sets HttpResponse::encodingFailed rather than corrupting the body silently. - Pinned by test_net against a loopback gzip response.
The redirect chain already re-checks the egress policy and the script grant on every hop and re-sends only the Content-Type header (so a cross-origin hop carries no caller headers); it now also refuses a Location that changes the scheme from https to http, so a hostile server cannot strip TLS mid-chain.
- WebSocket reassembles a fragmented message (a FIN=0 text/binary frame plus continuation frames) into one delivered message under its original opcode, with a cap so a peer cannot grow the buffer without bound and a 1002 close on a continuation with no start (improvement 51). - SetKeepalive sends a ping from Pump when the interval has elapsed since the last send; SetAutoPong (on by default) replies to a ping. - Fixed a real ordering bug the test exposed: Pump reported EOF before parsing already-buffered frames, so a peer that sent its frames and closed in one go lost them. It now drains the buffer first. - Pinned by test_net against a two-fragment loopback server.
- TlsSocket::SetPin pins a host to a set of leaf-certificate SHA-256 hashes; a pinned host's certificate must match one IN ADDITION to the normal chain and hostname verification, so a certificate from a compromised CA is still refused. PeerSpkiSha256 exposes the observed hash so a tool can compute a pin. Pinned by test_net: a wrong pin is refused and the right one accepted, with the chain still verifying (proving the pin did the refusing). - TlsSocket::SetCaDirectory loads an extra directory of PEM CAs (e.g. Android's /system/etc/security/cacerts) on top of the bundle, so a device trust store is used as well as the vendored one.
NetQuotaTracker enforces three per-script bounds: a maximum number of concurrently open sockets, a requests-per-second rate, and a bytes-per-second budget (a 1-second token window, so a burst is allowed but the sustained rate is capped). It owns no clock (nowMs is passed in), so it is deterministic. Pinned by test_netquota.
- Log keeps a bounded ring of the most recent lines (with their level), read via
Recent()/RecentEntries() and cleared with ClearRecent(), so an in-game console
can show them without a file sink.
- SetJsonLines emits each line as {"level":..,"msg":..} with minimal escaping,
for a host that ships structured logs.
- Pinned by test_logger (ring contents, bound, long-line integrity, per-module
levels, rate limiting).
The IDL now accepts a numeric range (number[lo,hi]) and a string length cap (string<n>) on a parameter, and the generator emits ValidateBindingArgs(name, args) — a per-binding predicate over the ScriptValue argument list derived from those constraints, so a hand-written range/length check can be deleted. A binding with no constraints, or an unknown name, is accepted (the capability gate is what refuses an unknown binding). Pinned by test_idl.
ConnPool<Conn> keeps idle connections keyed by host:port, bounded by a total idle cap and an idle timeout; an expired or evicted connection is closed through a caller-supplied callback, and Acquire returns a default when none is available. It owns no clock (nowMs is passed in) so it is deterministic. Pinned by test_conn_pool.
ReplayFile::RecordDeterminism stores the RNG seed, wall-clock origin and tick rate as an additive RunSeed record (a new kind, so an older reader preserves it) and ReadDeterminism reads them back, so a replay can seed its RNG and clock to reproduce a run instead of only its packet stream. Pinned by test_replay_format.
TcpSocket::Connect now orders a host's resolved addresses RFC 8305 style — the first IPv6 candidate leads, then the IPv4 candidates, then the remaining IPv6 ones — so a dual-stack host is reached quickly and an IPv4-only host is not stalled behind every AAAA address. HappyEyeballsOrder is a pure function so the ordering is pinned by test_net without a network.
TcpSocket::ConnectVia opens a tunnel to the target through a forward proxy: an HTTP CONNECT (2xx status required) or a SOCKS5 handshake (greeting, optional RFC 1929 username/password sub-negotiation, CONNECT request with a resolved address, non-zero reply code refused). A malformed or refused proxy handshake fails closed. Pinned by test_net against a loopback SOCKS5 echo server.
The rollback test used a lone-ret function as the failing target, which depends on codegen; a null target is refused by the shim without reading code, so the test is stable under the sanitizer build too.
HttpRequest::DoStreaming delivers the body through a chunk callback instead of buffering it whole, for a large download that should not be held in memory. The callback returning false aborts the transfer; maxBodyBytes still bounds the total. It requests Accept-Encoding: identity (a chunk boundary may split a compressed stream) and does not decode Content-Encoding. Pinned by test_net against a 256 KiB loopback body.
The libFuzzer target now also drives the script manifest, package manifest, cosmetic pack and replay container parsers, so an arbitrary byte buffer reaches every first-party parse path (improvements 79, 97). The deterministic test_fuzz suite remains the corpus seed.
- The CI static-analysis job now runs make check-warnings, make lint-scripts (which runs luau-analyze against the .luaurc rule pack when installed) and make check-examples, so script linting and the documentation examples are verified in CI (improvement 61). - check-werror compiles the first-party files whose warning baseline is clean with -Werror, so a new warning cannot land there (improvement 98). Wired into make test.
ModuleResolver resolves a script module name through the IScriptFileSystem seam, refusing any name with a '..' component, a leading slash, a backslash or NUL, so a module name cannot climb out of the script root; it appends .luau, caches a resolved module (a diamond dependency compiles once), detects a cycle, caps the module count, and honours an optional semver constraint against the module's declared version (a leading '-- version: X.Y.Z' comment). Pinned by test_require.
ScriptEventBus::ValidatePayload checks a published payload's comma-separated field count against the channel's IDL-declared arity, and Emit refuses a mismatch, so a publisher cannot send a differently-shaped payload that a subscriber would then mis-read. A script-only channel is unconstrained. Pinned by test_script_bus.
DebugEndpoint serves the metrics registry as a bounded JSON snapshot over HTTP, but only on a loopback bind (a non-loopback address is refused) and only to a caller presenting the token in an X-Berry-Token header or ?token= query, compared in constant time. HandleRequest is a pure function so the auth and routing are pinned by test_debug_endpoint without a socket.
ConfigFile::LoadIfChanged re-reads the file when its content changed (mtime is only a cheap hint, so the content is compared too), reports whether the parsed config changed, and refuses an invalid change while keeping the previous config. Pinned by test_configfile.
BytecodeCache caches compiled Luau bytecode keyed by the SHA-256 of the source (so a changed source can never return a stale chunk), with LRU eviction bounded by both entry count and total bytes; an entry larger than the whole budget is not stored. Pinned by test_bytecode_cache.
SecretStore is a namespaced key/value store behind an ISecretBackend seam (a device build implements it over the platform keystore; MemorySecretBackend is the host default). A script can only read its own namespace, values and keys are length-bounded, there is a per-script entry cap, and Forget wipes a script's secrets on unload. Pinned by test_secret_store.
ConsentStore remembers a per-script grant/deny decision for each capability a manifest requests, refuses to record a decision for a non-grantable capability, and fails closed: AllGranted is false while any requested capability is undecided or denied. Prompt() returns the (capability, reason, decision) list so an embedder can render a consent screen, and Revoke/RevokeOne re-ask. Pinned by test_consent.
SecureBuffer is page-aligned storage that mlocks its pages (best-effort, so a process without CAP_IPC_LOCK still works), zeroizes with a non-elidable write on destruction, and caps its size. Pinned by test_secure_buffer. Complements the existing VoiceZeroizeString for secrets held in std::string.
BindingProfiler times every native call at the one point they all pass through (ScriptBindings::Invoke) and exposes a per-binding calls/microseconds/max tally plus a Chrome trace-event export. Off by default. Pinned by test_script.
Three failures in the first CI run, all environment/portability, none a product bug: - clang -Wconversion implies -Wsign-conversion (gcc's does not), so clang flagged six sign conversions in net.cpp/inflate.cpp. Fixed them explicitly (index types, iterator arithmetic, base64 shifts) and added -Wsign-conversion to check-warnings/check-werror so gcc catches them too. - clang UBSan enables -fsanitize=function, which reads a type descriptor 8 bytes before an indirect-call target. The shim's JIT trampolines have none, so the check read the page before the pool and faulted (test_hooks). Added -fno-sanitize=function for clang only (gcc rejects the flag name). - The arm64 job expanded $(patsubst)/$(shell) inside a shell run: block (make never expands it) and overrode only CXX, so the vendored C compiled host-format and the link failed 'file in wrong format'. Moved the target list into an arm64-portable-tests Makefile target that passes both CXX and CC.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Applies the audit's four priority phases and the bugs and improvements they
did not cover. One commit per micro-phase, each authored
Lynxifyy;make testpasses on the tip and every new/changed suite is ASan+UBSan clean.
Bugs: 63 of 63 addressed
ResolveAdrpAdd)M_PI, real instruction budget via Luau single-step,fs.cppdup'd-descriptor walks, warning baseline)Bug 1 is a false positive and bug 60 (
M_PI) did not break the host build;both are documented rather than "fixed".
Improvements: implemented and tested
Every improvement in the list is either implemented or explicitly noted below as
deliberately partial. New, tested components:
handlers,
callOriginal, per-handler counters and fault auto-disable;ScopedHook/HookGroupRAII with rollback; icache flush; install-verify;hook diagnostics; maps-hole trampoline search.
ASLR-stable gate, mask styles,
FindUnique/ClosestMatch/FindGuarded/FindParallel, version-tagged rules, build-id-keyed offset cache,berry-sigcheckCLI, execute-only bytes from the on-disk ELF.refusal, keep-alive connection pool, streaming response API, certificate
pinning (SPKI), extra CA directory, HTTP CONNECT + SOCKS5 proxy, Happy
Eyeballs ordering, WebSocket fragmentation reassembly + keepalive/auto-pong,
per-script network quotas.
per-script frame quota, per-binding profiler + Chrome trace, IDL version tags +
breaking-change linter, event payload schemas, hot-reload state schema version,
drift-corrected timers, async combinators, structured async scopes,
per-category memory accounting, scripttest coverage, lint/format pack,
runnable doc examples in CI, sandboxed
require+ semver, capability consentstore, per-script secret store, script-bus payload validation, replay
determinism (seed + clock), bytecode cache.
Schema, golden-file test, network
error_kindenums, Python client stub,generated argument validation from IDL constraints.
file with env overrides + hot reload, metrics registry, per-subsystem frame
budgets, log ring + JSON lines, loopback token-protected metrics endpoint,
SECURITY.md, locked/zeroized key buffers.qemu, static analysis + lint + examples), libFuzzer targets for every pure
parser,
-Wshadow/-Wconversiongate,-Werrorgate, CMake+Ninja build,CycloneDX SBOM.
Honest boundaries
line-level debug hook that the sandboxed Luau VM deliberately does not expose
(the sandbox removes the debug library). The VM does expose the per-binding
profiler and the instruction budget, which are the safe introspection surfaces.
SSE2/Noson-Butler implementation.
read from the on-disk ELF (
ProcessImages::ReadModuleBytes); wiring that intothe scanner's range list is left to the device build that has such mappings.
the tree but only exercised in CI.
Test evidence
make test→== all tests passed ==(0 failures).all 70 sanitizer binaries at once, so the sanitizer was run per-suite).
test_x86_length,test_hooks,test_hook_chain,test_mem,test_metrics,test_crash,test_configfile,test_timers,test_inflate,test_netquota,test_conn_pool,test_require,test_bytecode_cache,test_secret_store,test_consent,test_secure_buffer,test_debug_endpoint,test_logger; plus extensions acrosstest_net,test_fuzz,test_sigscan,test_room_token,test_async,test_idl,test_script,test_scripttest,test_luau,test_profiler,test_replay_format.check-sigcheck,check-warnings,check-werror,check-examples,idl-check --golden-test,idl-breaking,make fuzz,make sbom,make lint-scripts.This PR was created by an AI agent (OpenHands) on behalf of the user.