Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ language runtime. The main focus is on user-observable behavior of the engine.
* Add a new context option `python.UnicodeCharacterDatabaseNativeFallback` to control whether the ICU database may fall back to the native unicode character database from CPython for features and characters not supported by ICU. This requires native access to be enabled and is disabled by default for embeddings.
* Add a new context option `python.AllowSignalHandlers` to control whether Python code may install signal handlers. This is disabled by default for Java embedding and enabled in the standalone.
* Add an experimental `python.InitializationEntropySource` option to control the entropy source used for initialization-only randomness such as hash secret generation and `random.Random(None)` seeding. This means embeddings and tests can select deterministic or externally provided initialization entropy without affecting cryptographically relevant APIs like `os.urandom()` or `random.SystemRandom()`.
* Dispatch `sys.audit` events to hooks registered with `sys.addaudithook`, including audit events raised through the `PySys_Audit` C API.
* Foreign temporal objects (dates, times, and timezones) are now given a Python class corresponding to their interop traits, i.e., `date`, `time`, `datetime`, or `tzinfo`. This allows any foreign objects with these traits to be used in place of the native Python types and Python methods available on these types work on the foreign types.
* Make BouncyCastle an optional dependency for embedding use cases. BouncyCastle is only needed for legacy RSA, DSA, and EC privat keys versions 0 and 1. To support these from Python embeddings, BouncyCastle must now be explicitly enabled by adding the `org.graalvm.python:python-bouncycastle-support` Maven artifact.
* The GraalPy Native standalone on Linux now uses a lower-footprint Native Image garbage collection configuration. This reduces resident set size (RSS) for many workloads, but may increase startup time and warmup time, and can slow down some workloads.
Expand Down
28 changes: 26 additions & 2 deletions graalpython/com.oracle.graal.python.cext/src/sysmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,32 @@
#include "pycore_pystate.h" // _PyThreadState_GET()

int PySys_Audit(const char *event, const char *argFormat, ...) {
// ignore for now
return 0;
PyObject *args;
if (argFormat == NULL) {
args = PyTuple_New(0);
if (args == NULL) {
return -1;
}
} else {
va_list va;
va_start(va, argFormat);
args = Py_VaBuildValue(argFormat, va);
va_end(va);
if (args == NULL) {
return -1;
}
if (!PyTuple_Check(args)) {
PyObject *tmp = PyTuple_Pack(1, args);
Py_DECREF(args);
if (tmp == NULL) {
return -1;
}
args = tmp;
}
}
int result = GraalPyPrivate_Sys_Audit(event, args);
Py_DECREF(args);
return result;
}

static void
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved.
# Copyright (c) 2021, 2026, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
#
# The Universal Permissive License (UPL), Version 1.0
Expand Down Expand Up @@ -39,7 +39,12 @@

import sys

from . import CPyExtTestCase, CPyExtFunction, unhandled_error_compare
from . import (
CPyExtFunction,
CPyExtTestCase,
compile_module_from_string,
unhandled_error_compare,
)


def _reference_get_object(args):
Expand All @@ -49,7 +54,76 @@ def _reference_get_object(args):
raise SystemError # raised by PyBuildValue(..., NULL)

class TestPySys(CPyExtTestCase):

def test_PySys_Audit(self):
module = compile_module_from_string("""
#define PY_SSIZE_T_CLEAN
#include <Python.h>

static PyObject* audit(PyObject* self, PyObject* Py_UNUSED(args)) {
PyObject *value = PyUnicode_FromString("value");
if (value == NULL) {
return NULL;
}
int res = PySys_Audit("graalpy.test_capi_audit", "Oi", value, 23);
Py_DECREF(value);
if (res < 0) {
return NULL;
}
return PyLong_FromLong(res);
}

static PyObject* audit_error(PyObject* self, PyObject* Py_UNUSED(args)) {
PyObject *value = PyUnicode_FromString("value");
if (value == NULL) {
return NULL;
}
int res = PySys_Audit("graalpy.test_capi_audit_error", "O", value);
Py_DECREF(value);
if (res < 0) {
return NULL;
}
return PyLong_FromLong(res);
}

static PyMethodDef methods[] = {
{"audit", audit, METH_NOARGS, NULL},
{"audit_error", audit_error, METH_NOARGS, NULL},
{NULL, NULL, 0, NULL}
};

static struct PyModuleDef module = {
PyModuleDef_HEAD_INIT, "test_sys_audit", NULL, -1, methods
};

PyMODINIT_FUNC PyInit_test_sys_audit(void) {
return PyModule_Create(&module);
}
""", "test_sys_audit")

seen = []

def hook(event, args):
if event == "graalpy.test_capi_audit":
seen.append(args)

sys.addaudithook(hook)

self.assertEqual(module.audit(), 0)
self.assertEqual(seen, [("value", 23)])

class AuditError(Exception):
pass

def error_hook(event, args):
if event == "graalpy.test_capi_audit_error":
raise AuditError(args)

sys.addaudithook(error_hook)

with self.assertRaises(AuditError) as cm:
module.audit_error()
self.assertEqual(cm.exception.args[0], ("value",))

test_PySys_GetObject = CPyExtFunction(
_reference_get_object,
lambda: (
Expand Down
129 changes: 129 additions & 0 deletions graalpython/com.oracle.graal.python.test/src/tests/test_audit_hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
#
# The Universal Permissive License (UPL), Version 1.0
#
# Subject to the condition set forth below, permission is hereby granted to any
# person obtaining a copy of this software, associated documentation and/or
# data (collectively the "Software"), free of charge and under any and all
# copyright rights in the Software, and any and all patent rights owned or
# freely licensable by each licensor hereunder covering either (i) the
# unmodified Software as contributed to or provided by such licensor, or (ii)
# the Larger Works (as defined below), to deal in both
#
# (a) the Software, and
#
# (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
# one is included with the Software each a "Larger Work" to which the Software
# is contributed by such licensors),
#
# without restriction, including without limitation the rights to copy, create
# derivative works of, display, perform, and distribute the Software and make,
# use, sell, offer for sale, import, export, have made, and have sold the
# Software and the Larger Work(s), and to sublicense the foregoing rights on
# either these or other terms.
#
# This license is subject to the following condition:
#
# The above copyright notice and either this complete permission notice or at a
# minimum a reference to the UPL must be included in all copies or substantial
# portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import sys
import tempfile
import unittest


class AuditHookTests(unittest.TestCase):
def test_sys_audit_calls_registered_hook(self):
seen = []

def hook(event, args):
if event == "graalpy.test_sys_audit":
seen.append((event, args))

sys.addaudithook(hook)
sys.audit("graalpy.test_sys_audit", 1, "two")

self.assertEqual(seen, [("graalpy.test_sys_audit", (1, "two"))])

def test_sys_audit_propagates_hook_exception(self):
class AuditError(Exception):
pass

def hook(event, args):
if event == "graalpy.test_sys_audit_error":
raise AuditError(args)

sys.addaudithook(hook)

with self.assertRaises(AuditError):
sys.audit("graalpy.test_sys_audit_error", 42)

def test_java_audit_site_calls_registered_hook(self):
seen = []

def hook(event, args):
if event == "open":
seen.append(args)

sys.addaudithook(hook)
with tempfile.TemporaryFile("w"):
pass

self.assertTrue(seen)

def test_addaudithook_exception_blocks_new_hook(self):
seen = []
block_add = True

def blocking_hook(event, args):
nonlocal block_add
if event == "sys.addaudithook":
if not block_add:
return
block_add = False
raise RuntimeError("blocked")
if event == "graalpy.test_blocked_hook":
seen.append("blocking")

def blocked_hook(event, args):
if event == "graalpy.test_blocked_hook":
seen.append("blocked")

sys.addaudithook(blocking_hook)
sys.addaudithook(blocked_hook)
sys.audit("graalpy.test_blocked_hook")

self.assertEqual(seen, ["blocking"])

def test_addaudithook_propagates_baseexception(self):
class AuditBaseException(BaseException):
pass

block_add = True

def hook(event, args):
nonlocal block_add
if event == "sys.addaudithook":
if not block_add:
return
block_add = False
raise AuditBaseException

sys.addaudithook(hook)

with self.assertRaises(AuditBaseException):
sys.addaudithook(lambda event, args: None)


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
test.test_audit.AuditTest.test_basic @ linux-x86_64
test.test_audit.AuditTest.test_block_add_hook @ linux-x86_64
test.test_audit.AuditTest.test_block_add_hook_baseexception @ linux-x86_64
test.test_audit.AuditTest.test_mmap @ linux-x86_64
test.test_audit.AuditTest.test_socket @ linux-x86_64
test.test_audit.AuditTest.test_sqlite3 @ linux-x86_64
Original file line number Diff line number Diff line change
Expand Up @@ -2427,6 +2427,7 @@ static Object doGeneric(VirtualFrame frame, Object arg,
@Builtin(name = "input", minNumOfPositionalArgs = 0, parameterNames = {"prompt"})
@GenerateNodeFactory
abstract static class InputNode extends PythonUnaryBuiltinNode {
private static final TruffleString T_BUILTINS_INPUT = tsLiteral("builtins.input");

@Specialization
static Object input(VirtualFrame frame, Object prompt,
Expand Down Expand Up @@ -2459,7 +2460,7 @@ private static Object doInput(Object prompt, Node node, PythonContext context) {
throw PRaiseNode.raiseStatic(node, RuntimeError, ErrorMessages.INPUT_LOST_SYS_S, T_STDERR);
}

SysModuleBuiltins.AuditNode.auditUncached("builtins.input", prompt != NO_VALUE ? prompt : NONE);
SysModuleBuiltins.AuditNode.auditUncached(T_BUILTINS_INPUT, prompt != NO_VALUE ? prompt : NONE);

try {
PyObjectCallMethodObjArgs.executeUncached(stderr, T_FLUSH);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import static com.oracle.graal.python.runtime.PosixConstants.LOCK_NB;
import static com.oracle.graal.python.runtime.PosixConstants.LOCK_SH;
import static com.oracle.graal.python.runtime.PosixConstants.LOCK_UN;
import static com.oracle.graal.python.util.PythonUtils.tsLiteral;

import java.util.List;

Expand Down Expand Up @@ -94,6 +95,9 @@

@CoreFunctions(defineModule = "fcntl")
public final class FcntlModuleBuiltins extends PythonBuiltins {
private static final TruffleString T_FCNTL_FLOCK = tsLiteral("fcntl.flock");
private static final TruffleString T_FCNTL_LOCKF = tsLiteral("fcntl.lockf");
private static final TruffleString T_FCNT_IOCTL = tsLiteral("fcnt.ioctl");

@Override
protected List<? extends NodeFactory<? extends PythonBuiltinBaseNode>> getNodeFactories() {
Expand Down Expand Up @@ -131,7 +135,7 @@ synchronized PNone flock(VirtualFrame frame, int fd, int operation,
@Cached SysModuleBuiltins.AuditNode auditNode,
@CachedLibrary("getPosixSupport()") PosixSupportLibrary posix,
@Cached PConstructAndRaiseNode.Lazy constructAndRaiseNode) {
auditNode.audit(inliningTarget, "fcntl.flock", fd, operation);
auditNode.audit(frame, inliningTarget, T_FCNTL_FLOCK, fd, operation);
try {
posix.flock(getPosixSupport(), fd, operation);
} catch (PosixException e) {
Expand All @@ -155,7 +159,7 @@ PNone lockf(VirtualFrame frame, int fd, int code, Object lenObj, Object startObj
@Cached PyLongAsLongNode asLongNode,
@Cached PRaiseNode raiseNode,
@Cached PConstructAndRaiseNode.Lazy constructAndRaiseNode) {
auditNode.audit(inliningTarget, "fcntl.lockf", fd, code, lenObj != PNone.NO_VALUE ? lenObj : PNone.NONE, startObj != PNone.NO_VALUE ? startObj : PNone.NONE, whence);
auditNode.audit(frame, inliningTarget, T_FCNTL_LOCKF, fd, code, lenObj != PNone.NO_VALUE ? lenObj : PNone.NONE, startObj != PNone.NO_VALUE ? startObj : PNone.NONE, whence);
int lockType;
if (code == LOCK_UN.value) {
lockType = F_UNLCK.getValueIfDefined();
Expand Down Expand Up @@ -212,7 +216,7 @@ Object ioctl(VirtualFrame frame, int fd, long request, Object arg, boolean mutat
@Cached PRaiseNode raiseNode,
@Cached PConstructAndRaiseNode.Lazy constructAndRaiseNode,
@Cached SysModuleBuiltins.AuditNode auditNode) {
auditNode.audit(inliningTarget, "fcnt.ioctl", fd, request, arg);
auditNode.audit(frame, inliningTarget, T_FCNT_IOCTL, fd, request, arg);

int intArg = 0;
if (arg != PNone.NO_VALUE) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* The Universal Permissive License (UPL), Version 1.0
Expand Down Expand Up @@ -40,6 +40,8 @@
*/
package com.oracle.graal.python.builtins.modules;

import static com.oracle.graal.python.util.PythonUtils.tsLiteral;

import java.util.List;

import com.oracle.graal.python.annotations.ArgumentClinic;
Expand All @@ -62,13 +64,15 @@
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.library.CachedLibrary;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.strings.TruffleString;

@CoreFunctions(defineModule = "msvcrt", os = PythonOS.PLATFORM_WIN32)
public final class MsvcrtModuleBuiltins extends PythonBuiltins {

public static final int LK_LOCK = 1;
public static final int LK_NBLCK = 2;
public static final int LK_UNLOCK = 3;
private static final TruffleString T_MSVCRT_LOCKING = tsLiteral("msvcrt.locking");

@Override
protected List<? extends NodeFactory<? extends PythonBuiltinBaseNode>> getNodeFactories() {
Expand All @@ -95,7 +99,7 @@ Object locking(VirtualFrame frame, int fd, int mode, long nbytes,
@Cached SysModuleBuiltins.AuditNode auditNode,
@CachedLibrary("getPosixSupport()") PosixSupportLibrary posixLib,
@Cached PConstructAndRaiseNode.Lazy constructAndRaiseNode) {
auditNode.audit(inliningTarget, "msvcrt.locking", fd, mode, nbytes);
auditNode.audit(frame, inliningTarget, T_MSVCRT_LOCKING, fd, mode, nbytes);
try {
posixLib.fcntlLock(getPosixSupport(), fd, mode == LK_NBLCK, mode == LK_LOCK ? 1 : 0, 0, 0, nbytes);
} catch (PosixSupportLibrary.PosixException e) {
Expand Down
Loading
Loading