Skip to content

Commit 3d8987b

Browse files
authored
Fix fileno parameter handling for socket class (#1638)
* Fix fileno handling and add tests Tests are backported from py3.11 * Expect failure for non socket fd on posix
1 parent 955e7a6 commit 3d8987b

3 files changed

Lines changed: 142 additions & 3 deletions

File tree

Src/IronPython.Modules/_socket.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,19 @@ public void __init__(CodeContext/*!*/ context, int family = DefaultAddressFamily
143143
// we now own the lifetime of the socket
144144
GC.SuppressFinalize(sock);
145145
} else if (fileno != null) {
146-
if (Converter.TryConvertToInt64(fileno, out long l)) {
147-
socket = HandleToSocket(l);
146+
if (!PythonOps.TryToIndex(fileno, out object? handleObj)) {
147+
throw PythonOps.TypeErrorForUnIndexableObject(fileno);
148148
}
149+
long handle = Converter.ConvertToInt64(handleObj);
150+
// Windows reserves only INVALID_SOCKET (~0) as an invalid handle
151+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? handle == -1 : handle < 0) {
152+
throw PythonOps.ValueError("negative file descriptor");
153+
}
154+
socket = HandleToSocket(handle);
149155
if (socket is null) {
150-
throw PythonOps.OSError("Bad file descriptor");
156+
throw PythonOps.OSError(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
157+
? PythonErrorNumber.WSAENOTSOCK : PythonErrorNumber.EBADF,
158+
"Bad file descriptor");
151159
}
152160
} else {
153161
try {

Src/StdLib/Lib/test/test_socket.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,45 @@
3030
except ImportError:
3131
fcntl = None
3232

33+
# ironpython: backported shims for older python
34+
try:
35+
from test.support import os_helper
36+
except ImportError:
37+
class os_helper:
38+
make_bad_fd = support.make_bad_fd
39+
unlink = support.unlink
40+
41+
try:
42+
from test.support import socket_helper
43+
except ImportError:
44+
class socket_helper:
45+
HOST = "localhost"
46+
HOSTv6 = "::1"
47+
48+
@staticmethod
49+
def create_unix_domain_name():
50+
"""
51+
Create a UNIX domain name: socket.bind() argument of a AF_UNIX socket.
52+
Return a path relative to the current directory to get a short path
53+
(around 27 ASCII characters).
54+
"""
55+
return tempfile.mktemp(prefix="test_python_", suffix='.sock',
56+
dir=os.path.curdir)
57+
58+
IPV6_ENABLED = False
59+
60+
if socket.has_ipv6:
61+
sock = None
62+
try:
63+
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
64+
sock.bind((socket_helper.HOSTv6, 0))
65+
socket_helper.IPV6_ENABLED = True
66+
except OSError:
67+
pass
68+
finally:
69+
if sock:
70+
sock.close()
71+
3372
HOST = support.HOST
3473
MSG = 'Michael Gilfix was here\u1234\r\n'.encode('utf-8') ## test unicode string and carriage return
3574
MAIN_TIMEOUT = 60.0
@@ -1531,6 +1570,94 @@ def fileno(self):
15311570
with self.assertRaises(TypeError):
15321571
sock._sendfile_use_sendfile(File(None))
15331572

1573+
# ironpython: test_socket_fileno tests are backported from py3.11
1574+
def _test_socket_fileno(self, s, family, stype):
1575+
self.assertEqual(s.family, family)
1576+
self.assertEqual(s.type, stype)
1577+
1578+
fd = s.fileno()
1579+
s2 = socket.socket(fileno=fd)
1580+
self.addCleanup(s2.close)
1581+
# detach old fd to avoid double close
1582+
s.detach()
1583+
self.assertEqual(s2.family, family)
1584+
self.assertEqual(s2.type, stype)
1585+
self.assertEqual(s2.fileno(), fd)
1586+
1587+
def test_socket_fileno(self):
1588+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1589+
self.addCleanup(s.close)
1590+
s.bind((socket_helper.HOST, 0))
1591+
self._test_socket_fileno(s, socket.AF_INET, socket.SOCK_STREAM)
1592+
1593+
if hasattr(socket, "SOCK_DGRAM"):
1594+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1595+
self.addCleanup(s.close)
1596+
s.bind((socket_helper.HOST, 0))
1597+
self._test_socket_fileno(s, socket.AF_INET, socket.SOCK_DGRAM)
1598+
1599+
if socket_helper.IPV6_ENABLED:
1600+
s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
1601+
self.addCleanup(s.close)
1602+
s.bind((socket_helper.HOSTv6, 0, 0, 0))
1603+
self._test_socket_fileno(s, socket.AF_INET6, socket.SOCK_STREAM)
1604+
1605+
if hasattr(socket, "AF_UNIX"):
1606+
unix_name = socket_helper.create_unix_domain_name()
1607+
self.addCleanup(os_helper.unlink, unix_name)
1608+
1609+
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
1610+
with s:
1611+
try:
1612+
s.bind(unix_name)
1613+
except PermissionError:
1614+
pass
1615+
else:
1616+
self._test_socket_fileno(s, socket.AF_UNIX,
1617+
socket.SOCK_STREAM)
1618+
1619+
def test_socket_fileno_rejects_float(self):
1620+
with self.assertRaises(TypeError):
1621+
socket.socket(socket.AF_INET, socket.SOCK_STREAM, fileno=42.5)
1622+
1623+
def test_socket_fileno_rejects_other_types(self):
1624+
with self.assertRaises(TypeError):
1625+
socket.socket(socket.AF_INET, socket.SOCK_STREAM, fileno="foo")
1626+
1627+
def test_socket_fileno_rejects_invalid_socket(self):
1628+
with self.assertRaisesRegex(ValueError, "negative file descriptor"):
1629+
socket.socket(socket.AF_INET, socket.SOCK_STREAM, fileno=-1)
1630+
1631+
@unittest.skipIf(os.name == "nt", "Windows disallows -1 only")
1632+
def test_socket_fileno_rejects_negative(self):
1633+
with self.assertRaisesRegex(ValueError, "negative file descriptor"):
1634+
socket.socket(socket.AF_INET, socket.SOCK_STREAM, fileno=-42)
1635+
1636+
def test_socket_fileno_requires_valid_fd(self):
1637+
WSAENOTSOCK = 10038
1638+
with self.assertRaises(OSError) as cm:
1639+
socket.socket(fileno=os_helper.make_bad_fd())
1640+
self.assertIn(cm.exception.errno, (errno.EBADF, WSAENOTSOCK))
1641+
1642+
with self.assertRaises(OSError) as cm:
1643+
socket.socket(
1644+
socket.AF_INET,
1645+
socket.SOCK_STREAM,
1646+
fileno=os_helper.make_bad_fd())
1647+
self.assertIn(cm.exception.errno, (errno.EBADF, WSAENOTSOCK))
1648+
1649+
def test_socket_fileno_requires_socket_fd(self):
1650+
with tempfile.NamedTemporaryFile() as afile:
1651+
with self.assertRaises(OSError):
1652+
socket.socket(fileno=afile.fileno())
1653+
1654+
with self.assertRaises(OSError) as cm:
1655+
socket.socket(
1656+
socket.AF_INET,
1657+
socket.SOCK_STREAM,
1658+
fileno=afile.fileno())
1659+
self.assertEqual(cm.exception.errno, errno.ENOTSOCK)
1660+
15341661

15351662
@unittest.skipUnless(HAVE_SOCKET_CAN, 'SocketCan required for this test.')
15361663
class BasicCANTest(unittest.TestCase):

Tests/test_socket_stdlib.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ def load_tests(loader, standard_tests, pattern):
4949
failing_tests += [
5050
test.test_socket.GeneralModuleTests('test_idna'), # TODO: figure out
5151
]
52+
if is_posix: # https://github.com/IronLanguages/ironpython3/pull/1638#discussion_r1059535867
53+
failing_tests += [
54+
test.test_socket.GeneralModuleTests('test_socket_fileno_requires_socket_fd'),
55+
]
5256

5357
skip_tests = [
5458
test.test_socket.UnbufferedFileObjectClassTestCase('testWriteNonBlocking') # fails intermittently during CI

0 commit comments

Comments
 (0)