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
35 changes: 32 additions & 3 deletions crates/compiler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2783,15 +2783,28 @@ fn assignment_target_expr_range(source: &str, start: usize, end: usize) -> Optio
return Some((target_start, target_end));
}
// `def f(): (yield bar)` — skip the suite header so the remaining
// text is the assignment target expression.
// text is the assignment target expression. Other colons (`x: int += 1`)
// are not suite headers for this diagnostic. Grouping parentheses may
// wrap the yield expression.
let colon = top_level_colon(bytes, target_start, target_end)?;
let after = skip_horizontal_whitespace(bytes, colon + 1);
if after >= target_end {
let yield_at = skip_opening_parentheses(bytes, after, target_end);
if yield_at >= target_end || !starts_identifier(bytes, yield_at, b"yield") {
return None;
}
Some((after, target_end))
}

fn skip_opening_parentheses(bytes: &[u8], mut index: usize, end: usize) -> usize {
loop {
index = skip_horizontal_whitespace(bytes, index);
if index >= end || bytes[index] != b'(' {
return index;
}
index += 1;
}
}

fn trim_target_range(bytes: &[u8], mut start: usize, mut end: usize) -> (usize, usize) {
while start < end
&& matches!(
Expand Down Expand Up @@ -3090,6 +3103,11 @@ fn condition_plain_assignment(bytes: &[u8], start: usize, end: usize) -> Option<
let mut nest = Vec::new();
while index < end {
match bytes[index] {
b'#' => {
while index < end && bytes[index] != b'\n' {
index += 1;
}
}
b'\'' | b'"' => index = skip_quoted_string(bytes, index),
b'(' => {
nest.push(if is_call_open(bytes, start, index) {
Expand All @@ -3111,9 +3129,20 @@ fn condition_plain_assignment(bytes: &[u8], start: usize, end: usize) -> Option<
nest.pop();
index += 1;
}
b'=' if is_plain_assignment_operator(bytes, index) && !nest.contains(&b'c') => {
b':' if nest.last() == Some(&b'l') => {
nest.pop();
index += 1;
}
b'=' if is_plain_assignment_operator(bytes, index)
&& !nest.contains(&b'c')
&& !nest.contains(&b'l') =>
{
return Some(index);
}
_ if starts_identifier(bytes, index, b"lambda") => {
nest.push(b'l');
index += 6;
}
_ => index += 1,
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/vm/src/object/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1949,8 +1949,8 @@ impl PyObject {
// during __del__, preventing safe_inc from seeing 0.
zelf.0.ref_count.inc_by(2);

let del_method = zelf.get_class_attr(identifier!(vm, __del__)).unwrap();
if let Err(e) = slot_del(zelf, vm) {
let del_method = zelf.get_class_attr(identifier!(vm, __del__)).unwrap();
let msg = del_method
.repr(vm)
.ok()
Expand Down
48 changes: 31 additions & 17 deletions crates/wasm/src/socket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ mod _socket {
Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
builtins::{PyType, PyTypeRef},
common::lock::PyMutex,
function::{OptionalArg, OptionalOption},
function::{ArgIntoFloat, OptionalArg},
types::{Constructor, Initializer},
};
use std::net::{Ipv4Addr, Ipv6Addr};
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::atomic::{AtomicI32, AtomicU64, Ordering};

#[pyattr]
const AF_UNSPEC: i32 = 0;
Expand Down Expand Up @@ -102,7 +102,7 @@ mod _socket {
#[pyattr(name = "has_ipv6")]
const HAS_IPV6: bool = true;

static DEFAULT_TIMEOUT: AtomicI32 = AtomicI32::new(-1);
static DEFAULT_TIMEOUT: AtomicU64 = AtomicU64::new(f64::to_bits(-1.0));

fn unsupported(
vm: &VirtualMachine,
Expand Down Expand Up @@ -229,18 +229,24 @@ mod _socket {

#[pyfunction]
fn getdefaulttimeout() -> Option<f64> {
let timeout = DEFAULT_TIMEOUT.load(Ordering::Relaxed);
(timeout >= 0).then_some(f64::from(timeout))
let timeout = f64::from_bits(DEFAULT_TIMEOUT.load(Ordering::Relaxed));
(timeout >= 0.0).then_some(timeout)
}

#[pyfunction]
fn setdefaulttimeout(timeout: OptionalOption<f64>, vm: &VirtualMachine) -> PyResult<()> {
match timeout.flatten() {
None => DEFAULT_TIMEOUT.store(-1, Ordering::Relaxed),
Some(value) if value >= 0.0 => {
DEFAULT_TIMEOUT.store(value as i32, Ordering::Relaxed);
fn setdefaulttimeout(timeout: Option<ArgIntoFloat>, vm: &VirtualMachine) -> PyResult<()> {
match timeout {
None => DEFAULT_TIMEOUT.store((-1.0f64).to_bits(), Ordering::Relaxed),
Some(value) => {
let value = value.into_float();
if value.is_nan() {
return Err(vm.new_value_error("Invalid value NaN (not a number)"));
}
if value < 0.0 || !value.is_finite() {
return Err(vm.new_value_error("Timeout value out of range"));
}
DEFAULT_TIMEOUT.store(value.to_bits(), Ordering::Relaxed);
}
Some(_) => return Err(vm.new_value_error("Timeout value out of range")),
}
Ok(())
}
Expand Down Expand Up @@ -327,11 +333,19 @@ mod _socket {
}

#[pymethod]
fn settimeout(&self, timeout: OptionalOption<f64>, vm: &VirtualMachine) -> PyResult<()> {
*self.timeout.lock() = match timeout.flatten() {
fn settimeout(&self, timeout: Option<ArgIntoFloat>, vm: &VirtualMachine) -> PyResult<()> {
*self.timeout.lock() = match timeout {
None => None,
Some(value) if value >= 0.0 => Some(value),
Some(_) => return Err(vm.new_value_error("Timeout value out of range")),
Some(value) => {
let value = value.into_float();
if value.is_nan() {
return Err(vm.new_value_error("Invalid value NaN (not a number)"));
}
if value < 0.0 || !value.is_finite() {
return Err(vm.new_value_error("Timeout value out of range"));
}
Some(value)
}
};
Ok(())
}
Expand Down Expand Up @@ -449,8 +463,8 @@ mod _socket {
zelf.kind.store(kind, Ordering::Relaxed);
zelf.proto.store(proto, Ordering::Relaxed);
*zelf.closed.lock() = false;
let default = DEFAULT_TIMEOUT.load(Ordering::Relaxed);
*zelf.timeout.lock() = (default >= 0).then_some(f64::from(default));
let default = f64::from_bits(DEFAULT_TIMEOUT.load(Ordering::Relaxed));
*zelf.timeout.lock() = (default >= 0.0).then_some(default);
Ok(())
}
}
Expand Down
42 changes: 39 additions & 3 deletions crates/wasm/src/ssl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,15 @@ mod _ssl {
*context.maximum_version.read(),
*context.options.read(),
);
if *context.verify_mode.read() != CERT_NONE {
return Err(vm
.new_os_subtype_error(
PySSLCertVerificationError::class(&vm.ctx).to_owned(),
Some(SSL_ERROR_SSL),
"certificate verify failed: unable to get local issuer certificate",
)
.upcast());
}
let mut builder =
ClientConfig::builder_with_provider(Arc::new(CryptoExt::get_provider().clone()))
.with_protocol_versions(versions)
Expand All @@ -766,9 +775,14 @@ mod _ssl {
self.ensure_conn(vm)?;
let mut connection = self.connection.lock();
let conn = connection.as_mut().expect("connection created above");
let incoming = self.incoming.inner.lock().read(usize::MAX);
let (incoming, eof) = {
let mut bio = self.incoming.inner.lock();
(bio.read(usize::MAX), bio.eof())
};
if !incoming.is_empty() {
conn.feed_tls(&incoming).map_err(|err| map_tls(vm, err))?;
} else if eof {
conn.feed_tls(&[]).map_err(|err| map_tls(vm, err))?;
Comment on lines +784 to +785

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Drain buffered plaintext before reporting BIO EOF

When the incoming MemoryBIO has reached EOF, every call to pump() now feeds an empty slice, and TlsConnection::feed_tls immediately returns ZeroReturn or Eof. If the final TLS records produced more plaintext than the previous read(len) consumed—for example, the peer sent a response and closed while the caller reads it in small chunks—the next read() fails here before consulting rustls's buffered plaintext, making the remainder permanently inaccessible. The non-WASM SSL path explicitly checks pending_plaintext() and returns those bytes before propagating either clean or unexpected EOF; this path needs the same ordering.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Applied. pump now feeds BIO EOF only when rustls has no pending plaintext, so a later read() still returns the leftover application data before ZeroReturn/EOF.
— commented by Claude

}
conn.process_packets().map_err(|err| map_tls(vm, err))?;
let outgoing = conn.drain_tls().map_err(|err| map_tls(vm, err))?;
Expand Down Expand Up @@ -887,8 +901,30 @@ mod _ssl {
}

#[pymethod]
fn peer_certificate(&self, _binary: OptionalArg<bool>, vm: &VirtualMachine) -> PyObjectRef {
vm.ctx.none()
fn getpeercert(
&self,
binary_form: OptionalArg<bool>,
vm: &VirtualMachine,
) -> PyResult<Option<PyObjectRef>> {
let binary = binary_form.unwrap_or(false);
let der = {
let guard = self.connection.lock();
let Some(conn) = guard.as_ref() else {
return Err(vm.new_value_error("handshake not done yet"));
};
if conn.is_handshaking() {
return Err(vm.new_value_error("handshake not done yet"));
}
let Some(certs) = conn.peer_certificates() else {
return Ok(None);
};
certs.first().map(|cert| cert.as_ref().to_vec())
};
match der {
None => Ok(None),
Some(der) if binary => Ok(Some(vm.ctx.new_bytes(der).into())),
Some(_) => Ok(Some(vm.ctx.new_dict().into())),
}
}

#[pymethod]
Expand Down
Loading