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
9 changes: 6 additions & 3 deletions .github/workflows/pyre-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -573,9 +573,12 @@ jobs:
name: CPython suite (gate)
runs-on: ubuntu-24.04
# Artifacts are named for this Linux runner, so wait for the Linux prepare
# leg; waiting for macOS races the Linux upload. Separately, the baseline is
# darwin-arm64-specific (`CPYTHON_SUITE_BASELINE_HOST`); `PLATFORM_GATED`
# only handles modules CPython skips wholesale on this host.
# leg; waiting for macOS races the Linux upload. This job is the only place
# CI runs the suite — `pyre/check.py` keeps the stage behind
# `--cpython-suite`, which no job passes, so its wall time is paid once.
# Separately, the baseline is darwin-arm64-specific
# (`CPYTHON_SUITE_BASELINE_HOST`); `PLATFORM_GATED` only handles modules
# CPython skips wholesale on this host.
needs: prepare-charon-llbc-linux
if: ${{ !cancelled() && needs.prepare-charon-llbc-linux.result == 'success' }}
timeout-minutes: 30
Expand Down
15 changes: 15 additions & 0 deletions pyre/bench/synth/load_name_builtin_cell_fold.cranelift.jitstats
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
bridges_compiled=0
descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_blackhole_adopted_multi_frame=0
fbw_blackhole_adopted_single_frame=0
fbw_rolled_back_with_effects=0
fbw_store_journal_rollback_failed=0
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=2
internal_compile_panics=0
loops_aborted=0
loops_compiled=2
retraces_compiled=0
15 changes: 15 additions & 0 deletions pyre/bench/synth/load_name_builtin_cell_fold.dynasm.jitstats
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
bridges_compiled=0
descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_blackhole_adopted_multi_frame=0
fbw_blackhole_adopted_single_frame=0
fbw_rolled_back_with_effects=0
fbw_store_journal_rollback_failed=0
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=2
internal_compile_panics=0
loops_aborted=0
loops_compiled=2
retraces_compiled=0
21 changes: 21 additions & 0 deletions pyre/bench/synth/load_name_builtin_cell_fold.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# pyre-check: max-pypy-ratio=8
# A module-scope LOAD_NAME whose name misses the module dict resolves through
# the frame's builtin module. The builtins cell folds under the module dict's
# version? (so a later global binding shadows the builtin) and the builtins
# dict's own version?. The second loop proves that invalidation is seen.
# Output verified against CPython and PyPy.

N = 90000000
M = 400000
s = "xx"

total = 0
for i in range(N):
total = total + len(s)
print(total)

len = lambda x: 100
shadowed = 0
for i in range(M):
shadowed = shadowed + len(s)
Comment on lines +13 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Make the benchmark pass Ruff.

Ruff reports unused loop variables, builtin shadowing, lambda assignment, and an unused lambda argument. Keep the module-scope len binding for invalidation coverage, but use a named function and an explicit A001 suppression.

Proposed fix
-for i in range(N):
+for _i in range(N):
     total = total + len(s)
 print(total)

-len = lambda x: 100
+def len(_x):  # noqa: A001
+    return 100
+
 shadowed = 0
-for i in range(M):
+for _i in range(M):
     shadowed = shadowed + len(s)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for i in range(N):
total = total + len(s)
print(total)
len = lambda x: 100
shadowed = 0
for i in range(M):
shadowed = shadowed + len(s)
for _i in range(N):
total = total + len(s)
print(total)
def len(_x): # noqa: A001
return 100
shadowed = 0
for _i in range(M):
shadowed = shadowed + len(s)
🧰 Tools
🪛 Ruff (0.16.1)

[warning] 13-13: Loop control variable i not used within loop body

Rename unused i to _i

(B007)


[error] 17-17: Variable len is shadowing a Python builtin

(A001)


[error] 17-17: Do not assign a lambda expression, use a def

Rewrite len as a def

(E731)


[warning] 17-17: Unused lambda argument: x

(ARG005)


[warning] 19-19: Loop control variable i not used within loop body

Rename unused i to _i

(B007)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/bench/synth/load_name_builtin_cell_fold.py` around lines 13 - 20, Update
the benchmark loops to avoid unused loop-variable warnings, and replace the
module-scope lambda assignment to len with a named function that preserves the
constant-returning behavior and unused argument. Retain the module-scope len
binding for invalidation coverage, adding an explicit A001 suppression for the
intentional builtin shadowing.

Source: Linters/SAST tools

print(shadowed)
15 changes: 15 additions & 0 deletions pyre/bench/synth/load_name_builtin_cell_fold.wasm.jitstats
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
bridges_compiled=0
descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_blackhole_adopted_multi_frame=0
fbw_blackhole_adopted_single_frame=0
fbw_rolled_back_with_effects=0
fbw_store_journal_rollback_failed=0
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=2
internal_compile_panics=0
loops_aborted=0
loops_compiled=2
retraces_compiled=0
2 changes: 1 addition & 1 deletion pyre/bench/synth/str_fstring.cranelift.jitstats
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ fbw_rolled_back_with_effects=0
fbw_store_journal_rollback_failed=0
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=659
guard_failures=658
internal_compile_panics=0
loops_aborted=0
loops_compiled=6
Expand Down
18 changes: 12 additions & 6 deletions pyre/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -2432,9 +2432,14 @@ def run_cpython_suite(self):

The baseline records one verdict per module per backend, observed on
darwin-arm64, and dynasm's codegen is arch-specific -- so the
comparison only means anything there, which is also why the CI job
pins `runs-on: macos-latest`. On any other host the stage reports
that it did not run instead of counting as a pass.
comparison only means anything there. On any other host the stage
reports that it did not run instead of counting as a pass.

Off by default and reached only through `--cpython-suite`: the suite
costs more wall time than every other stage here put together, and the
`cpython-tests` CI job already runs it on its own schedule. Pass the
flag locally when a change could move a verdict the synthetic corpus
does not cover.
"""
name = "cpython-suite"
backend = "dynasm"
Expand Down Expand Up @@ -2824,9 +2829,10 @@ def parse_backend_specs(specs):
help="skip pyre/bench/synth feature-parity benchmarks",
)
parser.add_argument(
"--no-cpython-suite",
"--cpython-suite",
action="store_true",
help="skip the vendored CPython suite gate (pyre/cpython_tests)",
help="also run the vendored CPython suite gate (pyre/cpython_tests); "
"off by default because it dominates this script's wall time",
)
parser.add_argument(
"--synthetic-only",
Expand Down Expand Up @@ -3011,7 +3017,7 @@ def main():
print()
chk.run_synthetic_suite()

if not args.no_cpython_suite and not args.synthetic_only:
if args.cpython_suite and not args.synthetic_only:
print()
print(bold("vendored CPython suite"))
chk.run_cpython_suite()
Expand Down
4 changes: 2 additions & 2 deletions pyre/cpython_tests/baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -1069,7 +1069,7 @@
"dynasm": "PASS"
},
"test.test_struct": {
"dynasm": "FAIL"
"dynasm": "PASS"
},
"test.test_structseq": {
"dynasm": "PASS"
Expand Down Expand Up @@ -1145,7 +1145,7 @@
"dynasm": "PASS"
},
"test.test_threading": {
"dynasm": "FAIL"
"dynasm": "PASS"
},
"test.test_threading_local": {
"dynasm": "PASS"
Expand Down
40 changes: 21 additions & 19 deletions pyre/pyre-interpreter/src/importing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1697,32 +1697,34 @@ pub fn remove_sys_module(name: &str) {
}
}

/// Drop the Python-visible import cache during process shutdown.
///
/// CPython's `finalize_modules()` clears `sys.modules` before module-global
/// teardown; PyPy's object space likewise owns modules through
/// `space.sys.modules`, rather than a permanent side table. Keeping every
/// imported module in the cache until `process::exit` prevents its managed
/// module/dict cycles and their `__del__` objects from ever becoming
/// unreachable. Retain `sys` and `builtins` themselves because pyre's
/// unraisable-error path still consults their live dictionaries while the
/// released modules are finalized.
pub fn release_sys_modules_for_shutdown() -> Vec<PyObjectRef> {
/// `finalize_remove_modules`: snapshot real modules, then detach import-cache
/// entries so module/dict cycles can become unreachable during shutdown.
pub fn release_sys_modules_for_shutdown() -> Vec<(Wtf8Buf, PyObjectRef)> {
let dict = sys_modules_dict();
if dict.is_null() {
return Vec::new();
}
let entries = unsafe { pyre_object::w_dict_str_entries(dict) };
let entries = unsafe { pyre_object::w_dict_items(dict) };
let mut modules = Vec::new();
for (name, module) in entries {
if matches!(name.as_str(), "sys" | "builtins") {
continue;
}
for (key, module) in entries {
let name = if unsafe { pyre_object::is_str(key) } {
Some(unsafe { pyre_object::w_str_get_wtf8(key) }.to_owned())
} else {
None
};
if !module.is_null() && unsafe { pyre_object::is_module(module) } {
modules.push(module);
if let Some(name) = &name {
modules.push((name.clone(), module));
}
}
unsafe {
pyre_object::w_dict_delitem_str(dict, &name);
let keep_entry = name.as_deref().is_some_and(|name| {
let bytes = name.as_bytes();
bytes == b"sys" || bytes == b"builtins"
});
if !keep_entry {
unsafe {
pyre_object::w_dict_delitem(dict, key);
}
}
}
modules
Expand Down
6 changes: 0 additions & 6 deletions pyre/pyre-interpreter/src/module/_blake2/_blake2_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,6 @@ def __new__(cls, *args, **kwargs):
"digest_size must be between 1 and %d bytes" %
cls.MAX_DIGEST_SIZE
)
if len(key) > cls.MAX_KEY_SIZE:
raise ValueError("maximum key length is %d bytes" % cls.MAX_KEY_SIZE)
if len(salt) > cls.SALT_SIZE:
raise ValueError("maximum salt length is %d bytes" % cls.SALT_SIZE)
if len(person) > cls.PERSON_SIZE:
raise ValueError("maximum person length is %d bytes" % cls.PERSON_SIZE)
if not 0 <= fanout <= 255:
raise ValueError("fanout must be between 0 and 255")
if not 1 <= depth <= 255:
Expand Down
71 changes: 55 additions & 16 deletions pyre/pyre-interpreter/src/module/_hashlib/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,10 @@ pub unsafe fn w_hmac_dealloc(obj: PyObjectRef) {
mod hmac_class {
use super::*;

#[crate::pyre_methods]
// lib_pypy/_hashlib/__init__.py:232: `class HMAC(HASH)`. HMAC keeps its
// own native payload layout, but its Python type relationship is the same
// TypeDef inheritance PyPy exposes.
#[crate::pyre_methods(base = hash_state_class::type_object())]
impl W_Hmac {
#[staticmethod]
fn __new__(
Expand Down Expand Up @@ -566,13 +569,23 @@ mod hmac_class {
fn resolve_hmac_digestmod(digestmod: PyObjectRef) -> Result<&'static str, crate::PyError> {
let name_obj = if unsafe { is_str(digestmod) } {
digestmod
} else if unsafe {
pyre_object::py_type_check(digestmod, &crate::function::BUILTIN_FUNCTION_TYPE)
} {
crate::baseobjspace::getattr_str(digestmod, "__name__")?
} else {
return Err(unsupported_digestmod("unsupported hash type"));
match crate::baseobjspace::getattr_str(digestmod, "__name__") {
Ok(name) => name,
// PyPy's structural rule is to accept every object that exposes
// `__name__` (lib_pypy/_hashlib/__init__.py:547-554). CPython
// 3.14 additionally normalizes a missing name to the module's
// public UnsupportedDigestmodError; preserve both without
// restricting the accepted object type again.
Err(err) if err.kind == crate::PyErrorKind::AttributeError => {
return Err(unsupported_digestmod("unsupported hash type"));
}
Err(err) => return Err(err),
}
};
if !unsafe { is_str(name_obj) } {
return Err(unsupported_digestmod("unsupported hash type"));
}
let name = unsafe { w_str_get_wtf8(name_obj) };
let bytes = name.as_bytes();
let bytes = bytes.strip_prefix(b"openssl_").unwrap_or(bytes);
Expand Down Expand Up @@ -671,11 +684,8 @@ fn pbkdf2_hmac(args: &[PyObjectRef]) -> Result<PyObjectRef, crate::PyError> {

check_digest_name(hash_name)?;
let requested = unsafe { w_str_get_wtf8(hash_name) };
let name = lookup_digest_name(requested.as_bytes()).ok_or_else(|| {
crate::PyError::value_error(format!(
"[digital envelope routines] unsupported: {requested}"
))
})?;
let name = lookup_digest_name(requested.as_bytes())
.ok_or_else(|| unsupported_digestmod("unsupported hash type"))?;
let password = read_hash_buffer(password)?;
let salt = read_hash_buffer(salt)?;
let iterations = crate::baseobjspace::int_w(crate::baseobjspace::space_index(iterations)?)?;
Expand All @@ -694,7 +704,7 @@ fn pbkdf2_hmac(args: &[PyObjectRef]) -> Result<PyObjectRef, crate::PyError> {
_ => pyre_native::hash::digest_output_size(name).unwrap_or(0),
};
let result = pyre_native::hash::compute_pbkdf2_hmac(name, &password, &salt, iterations, dklen)
.ok_or_else(|| crate::PyError::value_error("unsupported hash type"))?;
.ok_or_else(|| unsupported_digestmod("unsupported hash type"))?;
Ok(w_bytes_from_bytes(&result))
}

Expand Down Expand Up @@ -788,8 +798,11 @@ fn scrypt_kdf(args: &[PyObjectRef]) -> Result<PyObjectRef, crate::PyError> {
}

// RFC 7914's dominant allocation is V[N] with 128*r-byte entries. OpenSSL
// also needs B[p] and a working block. Honor an explicit caller limit;
// maxmem=0 retains OpenSSL's implementation-defined default behavior.
// also needs B[p] and a working block. PyPy passes maxmem=0 through to
// EVP_PBE_scrypt (lib_pypy/_hashlib/__init__.py:430-433), where OpenSSL
// applies its private 32 MiB default. The Rust backend has no such layer,
// so spell out that same default here rather than treating zero as
// unlimited memory.
let memory = usize::try_from(n)
.ok()
.and_then(|n| n.checked_mul(r as usize))
Expand All @@ -799,9 +812,15 @@ fn scrypt_kdf(args: &[PyObjectRef]) -> Result<PyObjectRef, crate::PyError> {
.ok_or_else(|| {
crate::PyError::value_error("Invalid parameter combination for n, r, p, maxmem")
})?;
if maxmem != 0 && memory > maxmem {
const OPENSSL_DEFAULT_SCRYPT_MAXMEM: usize = 32 * 1024 * 1024;
let effective_maxmem = if maxmem == 0 {
OPENSSL_DEFAULT_SCRYPT_MAXMEM
} else {
maxmem
};
if memory > effective_maxmem {
return Err(crate::PyError::value_error(
"Invalid parameter combination for n, r, p, maxmem",
"[digital envelope routines] memory limit exceeded",
));
}
let output = pyre_native::hash::compute_scrypt(&password, &salt, log_n, r, p, dklen)
Expand Down Expand Up @@ -834,6 +853,26 @@ fn blake2_new(args: &[PyObjectRef]) -> Result<PyObjectRef, crate::PyError> {
let key = read_hash_buffer(arg(3))?;
let salt = read_hash_buffer(arg(4))?;
let person = read_hash_buffer(arg(5))?;
let (max_key_size, salt_size, person_size) = match name {
"blake2b" => (64, 16, 16),
"blake2s" => (32, 8, 8),
_ => unreachable!(),
};
if key.len() > max_key_size {
return Err(crate::PyError::value_error(format!(
"maximum key length is {max_key_size} bytes"
)));
}
if salt.len() > salt_size {
return Err(crate::PyError::value_error(format!(
"maximum salt length is {salt_size} bytes"
)));
}
if person.len() > person_size {
return Err(crate::PyError::value_error(format!(
"maximum person length is {person_size} bytes"
)));
Comment on lines +866 to +874

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve BLAKE2 buffer-error ordering

Moving these size checks into _blake2_new means they run only after the app-level wrapper has validated fanout, depth, and the remaining tree parameters. For example, hashlib.blake2b(salt=b'x' * 17, fanout=256) now reports the fanout error, whereas the prior PyPy-shaped implementation and CPython report the oversized-salt error; the same regression occurs for person combined with invalid fanout or depth. Keep byte-accurate buffer validation, but perform it at the original point before the later range checks so exception precedence remains compatible.

AGENTS.md reference: AGENTS.md:L231-L233

Useful? React with 👍 / 👎.

}
Comment on lines +856 to +875

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- target context ---'
sed -n '800,890p' pyre/pyre-interpreter/src/module/_hashlib/mod.rs
printf '%s\n' '--- read_hash_buffer definitions and calls ---'
rg -n -C 5 'read_hash_buffer' pyre
printf '%s\n' '--- BLAKE2 constructor call sites ---'
rg -n -C 4 '_blake2_new|blake2b|blake2s' pyre/pyre-interpreter/src/module/_hashlib

Repository: youknowone/pyre

Length of output: 22416


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- read_hash_buffer implementation ---'
sed -n '955,1010p' pyre/pyre-interpreter/src/module/_hashlib/mod.rs
printf '%s\n' '--- buffer protocol implementation references ---'
rg -n -C 5 'PyBUF_SIMPLE|buffer.*view|BufferView|view.*len|as_bytes|to_vec|Vec::from|copy_from_slice' pyre/pyre-interpreter/src pyre/pyre-object/src | head -n 240
printf '%s\n' '--- BLAKE2 app-level validation ---'
sed -n '1,180p' pyre/pyre-interpreter/src/module/_hashlib/_hashlib_app.py

Repository: youknowone/pyre

Length of output: 21510


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- simple_buffer_bytes definition ---'
rg -n -C 10 'fn simple_buffer_bytes|simple_buffer_bytes' pyre
printf '%s\n' '--- buffer view types and byte conversion ---'
rg -n -C 8 'struct BufferView|impl BufferView|fn as_bytes|pub fn as_bytes|Buffer::|getbuffer|buffer.*bytes' pyre/pyre-object/src pyre/pyre-interpreter/src | head -n 320
printf '%s\n' '--- large-buffer constructors and allocation paths ---'
rg -n -C 5 'bytearray|w_bytearray|memoryview|Buffer::Owned|Vec<u8>|with_capacity' pyre/pyre-object/src | head -n 320

Repository: youknowone/pyre

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- buffer acquisition implementation ---'
sed -n '8460,8615p' pyre/pyre-interpreter/src/baseobjspace.rs
printf '%s\n' '--- SimpleBufferBytes definition ---'
rg -n -C 12 'struct SimpleBufferBytes|impl SimpleBufferBytes|enum BufferRequest' pyre/pyre-interpreter/src/baseobjspace.rs

Repository: youknowone/pyre

Length of output: 8542


🏁 Script executed:

#!/bin/bash
set -e
sed -n '8613,8755p' pyre/pyre-interpreter/src/baseobjspace.rs

Repository: youknowone/pyre

Length of output: 5373


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- _blake2 bindings and app-level callers ---'
rg -n -C 8 '_blake2_new|_blake2|blake2_new' pyre --glob '*.rs' --glob '*.py'
printf '%s\n' '--- buffer gather implementation ---'
rg -n -C 8 'fn memoryview_gather_bytes|memoryview_gather_bytes' pyre
printf '%s\n' '--- BLAKE2 tests and size limits ---'
rg -n -C 6 'maximum key length|maximum salt length|maximum person length|blake2b.*key|blake2s.*key|memoryview' pyre --glob '*test*' --glob '*.py' --glob '*.rs' | head -n 260

Repository: youknowone/pyre

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -e
sed -n '35,175p' pyre/pyre-interpreter/src/module/_blake2/_blake2_app.py

Repository: youknowone/pyre

Length of output: 5849


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
import re

hashlib = Path("pyre/pyre-interpreter/src/module/_hashlib/mod.rs").read_text()
base = Path("pyre/pyre-interpreter/src/baseobjspace.rs").read_text()
builtins = Path("pyre/pyre-interpreter/src/builtins.rs").read_text()

def body(source, signature, end_marker):
    start = source.index(signature)
    end = source.index(end_marker, start)
    return source[start:end]

blake = body(hashlib, "fn blake2_new(", "/// Keyword names")
reader = body(hashlib, "fn read_hash_buffer(", "/// Keyword names")
buffer = body(base, "fn buffer_bytes(", "/// Look up a descriptor")
gather = body(builtins, "pub(crate) unsafe fn memoryview_gather_bytes", "/// `PyBuffer_ToContiguous")

calls = [blake.index(f"read_hash_buffer(arg({i}))?") for i in (3, 4, 5)]
checks = [blake.index(token) for token in ("if key.len()", "if salt.len()", "if person.len()")]

assert calls == sorted(calls), "BLAKE2 parameter buffers are not read in source order"
assert max(calls) < min(checks), "A parameter buffer is checked before materialization"
assert "buf.as_bytes().to_vec()" in reader, "read_hash_buffer no longer copies the acquired view"
assert "memoryview_gather_bytes" in buffer, "buffer_bytes does not gather memoryview contents"
assert "-> Vec<u8>" in gather, "memoryview_gather_bytes is not Vec-returning"

print("read_hash_buffer copies acquired buffer contents")
print("buffer_bytes gathers memoryview contents into Vec<u8>")
print("all BLAKE2 key/salt/person buffers are materialized before size checks")
PY

Repository: youknowone/pyre

Length of output: 294


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path

hashlib = Path("pyre/pyre-interpreter/src/module/_hashlib/mod.rs").read_text()
base = Path("pyre/pyre-interpreter/src/baseobjspace.rs").read_text()
builtins = Path("pyre/pyre-interpreter/src/builtins.rs").read_text()

def after(source, start_text, end_text):
    start = source.index(start_text)
    end = source.index(end_text, start)
    return source[start:end]

blake = after(hashlib, "fn blake2_new(", "const NEW_KEYWORDS")
reader = after(hashlib, "fn read_hash_buffer(", "const NEW_KEYWORDS")
buffer = after(base, "fn buffer_bytes(", "/// Look up a descriptor")
gather = after(
    builtins,
    "pub(crate) unsafe fn memoryview_gather_bytes",
    "/// `PyBuffer_ToContiguous",
)

calls = [blake.index(f"read_hash_buffer(arg({i}))?") for i in (3, 4, 5)]
checks = [blake.index(token) for token in ("if key.len()", "if salt.len()", "if person.len()")]

assert calls == sorted(calls)
assert max(calls) < min(checks)
assert "buf.as_bytes().to_vec()" in reader
assert "memoryview_gather_bytes" in buffer
assert "-> Vec<u8>" in gather

print("read_hash_buffer copies acquired buffer contents")
print("buffer_bytes gathers memoryview contents into Vec<u8>")
print("all BLAKE2 key/salt/person buffers are materialized before size checks")
PY

Repository: youknowone/pyre

Length of output: 294


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path

hashlib = Path("pyre/pyre-interpreter/src/module/_hashlib/mod.rs").read_text()
base = Path("pyre/pyre-interpreter/src/baseobjspace.rs").read_text()
builtins = Path("pyre/pyre-interpreter/src/builtins.rs").read_text()

start = hashlib.index("fn blake2_new(")
param_reads = [
    hashlib.index(f"read_hash_buffer(arg({i}))?", start)
    for i in (3, 4, 5)
]
checks = [
    hashlib.index(token, start)
    for token in ("if key.len()", "if salt.len()", "if person.len()")
]

assert param_reads == sorted(param_reads)
assert max(param_reads) < min(checks)
assert "buf.as_bytes().to_vec()" in hashlib
assert "memoryview_gather_bytes" in base
assert "pub(crate) unsafe fn memoryview_gather_bytes" in builtins
assert "-> Vec<u8>" in builtins[builtins.index("pub(crate) unsafe fn memoryview_gather_bytes"):]

print("read_hash_buffer copies acquired buffer contents")
print("buffer_bytes gathers memoryview contents into Vec<u8>")
print("all BLAKE2 key/salt/person buffers are materialized before size checks")
PY

Repository: youknowone/pyre

Length of output: 327


Reject oversized BLAKE2 parameter buffers before copying them.

Use a length-only buffer view for key, salt, and person before read_hash_buffer copies them. A large memoryview can otherwise allocate its full contents and abort on OOM instead of returning ValueError.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyre/pyre-interpreter/src/module/_hashlib/mod.rs` around lines 856 - 875,
Update the BLAKE2 parameter handling around the max_key_size, salt_size, and
person_size checks to obtain length-only buffer views for key, salt, and person
before calling read_hash_buffer. Validate each view’s length against its
corresponding limit first, then copy only validated buffers so oversized
memoryviews return ValueError without allocating their full contents.

Source: MCP tools

let index =
|position| crate::baseobjspace::int_w(crate::baseobjspace::space_index(arg(position))?);
let digest_size = usize::try_from(index(2)?)
Expand Down
Loading
Loading