Skip to content

gh-107545: Fix misleading setsockopt error message #107546

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

naweiss
Copy link

@naweiss naweiss commented Aug 1, 2023

According to Python's documentation, setsockopt can get either int, buffer or None as the third argument.
The way it is currently implemented is by trying to parse all of the three arguments as ints, if this fails no matter what the reason is we just try the next overload.
Since 2**31 causes PyExc_OverflowError we try the next overloads. Because the buffer overload is last we get type error for the third argument instead of OverflowError (e.g. TypeError: a bytes-like object is required, not 'int').

@bedevere-bot
Copy link

Most changes to Python require a NEWS entry.

Please add it using the blurb_it web app or the blurb command-line tool.

@ghost
Copy link

ghost commented Aug 1, 2023

All commit authors signed the Contributor License Agreement.
CLA signed

@naweiss
Copy link
Author

naweiss commented Aug 1, 2023

I choose to solve the issue using a white-list approach rather then a black-list approach since there might be more errors that PyErr_Clear will silently suppress. For example, currently, if the type of the first argument is incorrect you still need to keep checking all of the overloads of setsockopt they will all fail and you get the error message from the buffer overload. when you could report the error immediately after the first overload failure.

Error message for:

import socket with socket.socket() as s: s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 2 ** 31)

is now OverflowError: signed integer is greater than maximum

@CharlieZhao95
Copy link
Contributor

Looks good, how about adding some tests for new error messages.

@naweiss
Copy link
Author

naweiss commented Aug 6, 2023

I added the relevant tests. Any new review comments?

@CharlieZhao95
Copy link
Contributor

CharlieZhao95 commented Aug 7, 2023

I added the relevant tests. Any new review comments?

Considering this is your first PR in cpython, please be patient! The next step is waiting for the review by core devs.

Generally, core developers don't have enough time to review every PR, so you can see there are many PRs that can't be merged into the main. Before they come to review, we just need to keep our PR compliant and wait :)

@python-cla-bot
Copy link

python-cla-bot bot commented Apr 18, 2025

All commit authors signed the Contributor License Agreement.

CLA signed

@naweiss naweiss force-pushed the fix-issue-107545 branch from f712b65 to 6439014 Compare June 23, 2025 21:12
@naweiss naweiss force-pushed the fix-issue-107545 branch from 6439014 to 167d212 Compare June 23, 2025 21:22
@naweiss
Copy link
Author

naweiss commented Jun 25, 2025

Since this PR is old, I validated that the issue is still reproducible.
Than rebased the branch onto main, resolved any conflicts, and confirmed that all tests pass.
I also did some minor changes to make the PR files a little cleaner and follow conventions better.

Copy link
Member

@serhiy-storchaka serhiy-storchaka left a comment

Choose a reason for hiding this comment

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

LGTM, but I think that we can get rid of _testcapi.

The more user friendly approach is to parse arguments as "iiOO|O" and then use == Py_None, PyIndex_Check() and PyBuffer_Check() for the third argument. This will allow to generate better error messages like "... should be integer, bytes-like object or None".

@unittest.skipIf(_testcapi is None, "requires _testcapi")
def test_setsockopt_errors(self):
# See issue #107546.
from _testcapi import INT_MAX, INT_MIN
Copy link
Member

Choose a reason for hiding this comment

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

We could just use some large values like 2**1000 and -2**1000.

Copy link
Member

Choose a reason for hiding this comment

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

I'm scared by big numbers, 2**100 and -2**100 should be enough to trigger OverflowError on all platforms :-)

Copy link
Member

Choose a reason for hiding this comment

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

But not on platforms with 128-bit int. I often use 2**1000 as more future proof option.

Copy link
Member

Choose a reason for hiding this comment

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

Does Python support any platform with 128-bit integer? Does such platform exist?

@serhiy-storchaka serhiy-storchaka added the needs backport to 3.14 bugs and security fixes label Jul 7, 2025
@naweiss
Copy link
Author

naweiss commented Jul 7, 2025

Thanks for the feedback.
I would be happy for guidance regarding the AF_VSOCK family.
The old code supported only an integer option, is this the expected behavior?

@@ -3338,26 +3338,32 @@ sock_setsockopt(PyObject *self, PyObject *args)
Py_buffer optval;
int flag;
unsigned int optlen;
PyObject *none;
PyObject *type;
Copy link
Member

Choose a reason for hiding this comment

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

Semantically, this is not type, but value.

Copy link
Author

@naweiss naweiss Jul 8, 2025

Choose a reason for hiding this comment

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

I choose the name optval for all cases which access the value of the option.

PyObject *none;
PyObject *type;

if (!PyArg_ParseTuple(args, "iiO|I:setsockopt",
Copy link
Member

Choose a reason for hiding this comment

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

The fourth argument is only accepted if the third argument is None. One way to verify this is to use the "O" converter with a default value of NULL and check that it is still NULL in all cases except when the third argument is None.

Copy link
Author

Choose a reason for hiding this comment

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

I ended up using a different approach, since I didn't wanted the code to break in the future if someone will add another overload and forget to check that the value is NULL.
So, I first check that the code receives either 3 or 4 arguments with the appropriate types while allowing a 4th argument only if the 3rd is None.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, this would work too. The minor downside of this approach is that you needs to explicitly raise an error message for wrong number of arguments.

You can also use a single PyArg_ParseTuple() with format "iiO|I:setsockopt" and then check the number of arguments:

if (arglen == 3 && optval == Py_None) { // error } if (arglen == 4 && optval != Py_None) { // error }

Bot note that only integers are currently supported for AF_VSOCK (I think we will change this, but in a separate issue).

with self.assertRaises(OverflowError):
sock.setsockopt(2 ** 100, socket.SO_REUSEADDR, 1)

with self.assertRaises(TypeError):
Copy link
Member

Choose a reason for hiding this comment

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

You can test the error message as well.

Copy link
Author

Choose a reason for hiding this comment

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

Good Idea, I also created some more type checks and added their exact message to the tests code.

@serhiy-storchaka
Copy link
Member

Thank you for your update, @naweiss. Here is the next portion of comments.

@serhiy-storchaka
Copy link
Member

The old code supported only an integer option, is this the expected behavior?

I noticed this too. We need to look at the history of the code. Anyway, this is a different issue.

if (!PyArg_ParseTuple(args, "iiO:setsockopt",
&level, &optname, &optval)) {
return NULL;
}
Copy link
Member

Choose a reason for hiding this comment

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

Can you please move this test for None and 4 arguments here?

if (optval == Py_None) { PyErr_Format(...) }

}
break;
case 4:
if (!PyArg_ParseTuple(args, "iiO!I:setsockopt",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (!PyArg_ParseTuple(args, "iiO!I:setsockopt",
if (!PyArg_ParseTuple(args, "iiOI:setsockopt",
@@ -3374,36 +3397,45 @@ sock_setsockopt(PyObject *self, PyObject *args)
goto done;
}

PyErr_Clear();
/* setsockopt(level, opt, None, flag) */
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/* setsockopt(level, opt, None, flag) */
/* setsockopt(level, opt, None, optlen) */
Comment on lines +3427 to +3428
res = setsockopt(get_sock_fd(s), level, optname,
buffer.buf, (int)buffer.len);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
res = setsockopt(get_sock_fd(s), level, optname,
buffer.buf, (int)buffer.len);
res = setsockopt(get_sock_fd(s), level, optname,
buffer.buf, (int)buffer.len);
&level, &optname, Py_TYPE(Py_None), &optval, &optlen)) {
return NULL;
}
break;
Copy link
Member

Choose a reason for hiding this comment

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

We need if (optval != Py_None) {error...} here.

sock.setsockopt(2 ** 100, socket.SO_REUSEADDR, 1)

msg = "socket option should be should be integer, bytes-like object or None"
with self.assertRaises(TypeError, msg=msg):
Copy link
Member

Choose a reason for hiding this comment

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

It should be assertRaisesRegex. And the expected error message is a regular expression, so you need to escape parentheses.

PyObject *none;
PyObject *type;

if (!PyArg_ParseTuple(args, "iiO|I:setsockopt",
Copy link
Member

Choose a reason for hiding this comment

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

Yes, this would work too. The minor downside of this approach is that you needs to explicitly raise an error message for wrong number of arguments.

You can also use a single PyArg_ParseTuple() with format "iiO|I:setsockopt" and then check the number of arguments:

if (arglen == 3 && optval == Py_None) { // error } if (arglen == 4 && optval != Py_None) { // error }

Bot note that only integers are currently supported for AF_VSOCK (I think we will change this, but in a separate issue).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
5 participants