Make file descriptors from stdlib non-inheritable by default (#13201)

* io: make file descriptors non-inheritable by default

This prevents file descriptors/handles leakage to child processes
that might cause issues like running out of file descriptors, or potential
security issues like leaking a file descriptor to a restricted file.

While this breaks backward compatibility, I'm rather certain that not
many programs (if any) actually make use of this implementation detail.
A new API `setInheritable` is provided for the few that actually want to
use this functionality.

* io: disable inheritance at file creation time for supported platforms

Some platforms provide extension to fopen-family of functions to allow
for disabling descriptor inheritance atomically during File creation.
This guards against possible leaks when a child process is spawned
before we managed to disable the file descriptor inheritance
(ie. in a multi-threaded program).

* net, nativesockets: make sockets non inheritable by default

With this commit, sockets will no longer leak to child processes when
you don't want it to. Should solves a lot of "address in use" that might
occur when your server has just restarted.

All APIs that create sockets in these modules now expose a `inheritable`
flag that allow users to toggle inheritance for the resulting sockets.
An implementation of `setInheritance()` is also provided for SocketHandle.

While atomically disabling inheritance at creation time is supported on
Windows, it's only implemented by native winsock2, which is too much for
now. This support can be implemented in a future patch.

* posix: add F_DUPFD_CLOEXEC

This command duplicates file descriptor with close-on-exec flag set.

Defined in POSIX.1-2008.

* ioselectors_kqueue: don't leak file descriptors

File descriptors internally used by ioselectors on BSD/OSX are now
shielded from leakage.

* posix: add O_CLOEXEC

This flag allows file descriptors to be open() with close-on-exec flag
set atomically.

This flag is specified in POSIX.1-2008

* tfdleak: test for selectors leakage

Also simplified the test by using handle-type agnostic APIs to test for
validity.

* ioselectors_epoll: mark all fd created close-on-exec

File descriptors from ioselectors should no longer leaks on Linux.

* tfdleak: don't check for selector leakage on Windows

The getFd proc for ioselectors_select returns a hardcoded -1

* io: add NoInheritFlag at compile time

* io: add support for ioctl-based close-on-exec

This allows for the flag to be set/unset in one syscall. While the
performance gains might be negliable, we have one less failure point
to deal with.

* tfdleak: add a test for setInheritable

* stdlib: add nimInheritHandles to restore old behaviors

* memfiles: make file handle not inheritable by default for posix

* io: setInheritable now operates on OS file handle

On Windows, the native handle is the only thing that's inheritable, thus
we can assume that users of this function will already have the handle
available to them. This also allows users to pass down file descriptors
from memfiles on Windows with ease, should that be desired.

With this, nativesockets.setInheritable can be made much simpler.

* changelog: clarify

* nativesockets: document setInheritable return value

* posix_utils: atomically disable fd inheritance for mkstemp
This commit is contained in:
alaviss
2020-04-20 15:09:59 +00:00
committed by GitHub
parent 6bd279c978
commit 1bdc30bdb1
17 changed files with 317 additions and 44 deletions

View File

@@ -10,6 +10,25 @@
- The default hash for `Ordinal` has changed to something more bit-scrambling.
`import hashes; proc hash(x: myInt): Hash = hashIdentity(x)` recovers the old
one in an instantiation context while `-d:nimIntHash1` recovers it globally.
- File handles created from high-level abstractions in the stdlib will no longer
be inherited by child processes. In particular, these modules are affected:
`system`, `nativesockets`, `net` and `selectors`.
For `net` and `nativesockets`, an `inheritable` flag has been added to all
`proc`s that create sockets, allowing the user to control whether the
resulting socket is inheritable. This flag is provided to ease the writing of
multi-process servers, where sockets inheritance is desired.
For a transistion period, define `nimInheritHandles` to enable file handle
inheritance by default. This flag does **not** affect the `selectors` module
due to the differing semantics between operating systems.
`system.setInheritable` and `nativesockets.setInheritable` is also introduced
for setting file handle or socket inheritance. Not all platform have these
`proc`s defined.
- The file descriptors created for internal bookkeeping by `ioselector_kqueue`
and `ioselector_epoll` will no longer be leaked to child processes.
## Language changes
- In newruntime it is now allowed to assign discriminator field without restrictions as long as case object doesn't have custom destructor. Discriminator value doesn't have to be a constant either. If you have custom destructor for case object and you do want to freely assign discriminator fields, it is recommended to refactor object into 2 objects like this:

View File

@@ -896,6 +896,10 @@ proc `==`*(x, y: SocketHandle): bool {.borrow.}
proc accept*(a1: SocketHandle, a2: ptr SockAddr, a3: ptr SockLen): SocketHandle {.
importc, header: "<sys/socket.h>", sideEffect.}
when defined(linux) or defined(bsd):
proc accept4*(a1: SocketHandle, a2: ptr SockAddr, a3: ptr SockLen,
flags: cint): SocketHandle {.importc, header: "<sys/socket.h>".}
proc bindSocket*(a1: SocketHandle, a2: ptr SockAddr, a3: SockLen): cint {.
importc: "bind", header: "<sys/socket.h>".}
## is Posix's ``bind``, because ``bind`` is a reserved word
@@ -1036,6 +1040,9 @@ proc mkstemp*(tmpl: cstring): cint {.importc, header: "<stdlib.h>", sideEffect.}
proc mkdtemp*(tmpl: cstring): pointer {.importc, header: "<stdlib.h>", sideEffect.}
when defined(linux) or defined(bsd):
proc mkostemp*(tmpl: cstring, oflags: cint): cint {.importc, header: "<stdlib.h>", sideEffect.}
proc utimes*(path: cstring, times: ptr array[2, Timeval]): int {.
importc: "utimes", header: "<sys/time.h>", sideEffect.}
## Sets file access and modification times.

View File

@@ -100,6 +100,7 @@ const EXDEV* = cint(18)
# <fcntl.h>
const F_DUPFD* = cint(0)
const F_DUPFD_CLOEXEC* = cint(1030)
const F_GETFD* = cint(1)
const F_SETFD* = cint(2)
const F_GETFL* = cint(3)
@@ -126,6 +127,7 @@ const O_ACCMODE* = cint(3)
const O_RDONLY* = cint(0)
const O_RDWR* = cint(2)
const O_WRONLY* = cint(1)
const O_CLOEXEC* = cint(524288)
const POSIX_FADV_NORMAL* = cint(0)
const POSIX_FADV_SEQUENTIAL* = cint(2)
const POSIX_FADV_RANDOM* = cint(1)
@@ -469,6 +471,7 @@ const SOCK_DGRAM* = cint(2)
const SOCK_RAW* = cint(3)
const SOCK_SEQPACKET* = cint(5)
const SOCK_STREAM* = cint(1)
const SOCK_CLOEXEC* = cint(524288)
const SOL_SOCKET* = cint(1)
const SOMAXCONN* = cint(128)
const SO_REUSEPORT* = cint(15)

View File

@@ -557,6 +557,9 @@ when defined(linux) or defined(nimdoc):
else:
var SO_REUSEPORT* {.importc, header: "<sys/socket.h>".}: cint
when defined(linux) or defined(bsd):
var SOCK_CLOEXEC* {.importc, header: "<sys/socket.h>".}: cint
when defined(macosx):
# We can't use the NOSIGNAL flag in the ``send`` function, it has no effect
# Instead we should use SO_NOSIGPIPE in setsockopt

View File

@@ -532,6 +532,8 @@ when defined(nimdoc):
else:
var SO_REUSEPORT* {.importc, header: "<sys/socket.h>".}: cint
var SOCK_CLOEXEC* {.importc, header: "<sys/socket.h>".}: cint
var MSG_NOSIGNAL* {.importc, header: "<sys/socket.h>".}: cint
when hasSpawnH:

View File

@@ -562,6 +562,9 @@ when defined(linux) or defined(nimdoc):
else:
var SO_REUSEPORT* {.importc, header: "<sys/socket.h>".}: cint
when defined(linux) or defined(bsd):
var SOCK_CLOEXEC* {.importc, header: "<sys/socket.h>".}: cint
when defined(macosx):
# We can't use the NOSIGNAL flag in the ``send`` function, it has no effect
# Instead we should use SO_NOSIGPIPE in setsockopt

View File

@@ -99,6 +99,7 @@ var EXDEV* {.importc: "EXDEV", header: "<errno.h>".}: cint
# <fcntl.h>
var F_DUPFD* {.importc: "F_DUPFD", header: "<fcntl.h>".}: cint
var F_DUPFD_CLOEXEC* {.importc: "F_DUPFD", header: "<fcntl.h>".}: cint
var F_GETFD* {.importc: "F_GETFD", header: "<fcntl.h>".}: cint
var F_SETFD* {.importc: "F_SETFD", header: "<fcntl.h>".}: cint
var F_GETFL* {.importc: "F_GETFL", header: "<fcntl.h>".}: cint
@@ -125,6 +126,7 @@ var O_ACCMODE* {.importc: "O_ACCMODE", header: "<fcntl.h>".}: cint
var O_RDONLY* {.importc: "O_RDONLY", header: "<fcntl.h>".}: cint
var O_RDWR* {.importc: "O_RDWR", header: "<fcntl.h>".}: cint
var O_WRONLY* {.importc: "O_WRONLY", header: "<fcntl.h>".}: cint
var O_CLOEXEC* {.importc: "O_CLOEXEC", header: "<fcntl.h>".}: cint
var POSIX_FADV_NORMAL* {.importc: "POSIX_FADV_NORMAL", header: "<fcntl.h>".}: cint
var POSIX_FADV_SEQUENTIAL* {.importc: "POSIX_FADV_SEQUENTIAL", header: "<fcntl.h>".}: cint
var POSIX_FADV_RANDOM* {.importc: "POSIX_FADV_RANDOM", header: "<fcntl.h>".}: cint

View File

@@ -83,7 +83,11 @@ proc mkstemp*(prefix: string): (string, File) =
## The file is created with perms 0600.
## Returns the filename and a file opened in r/w mode.
var tmpl = cstring(prefix & "XXXXXX")
let fd = mkstemp(tmpl)
let fd =
when declared(mkostemp):
mkostemp(tmpl, O_CLOEXEC)
else:
mkstemp(tmpl)
var f: File
if open(f, fd, fmReadWrite):
return ($tmpl, f)

View File

@@ -81,7 +81,7 @@ proc newSelector*[T](): Selector[T] =
# Start with a reasonable size, checkFd() will grow this on demand
const numFD = 1024
var epollFD = epoll_create(MAX_EPOLL_EVENTS)
var epollFD = epoll_create1(O_CLOEXEC)
if epollFD < 0:
raiseOSError(osLastError())
@@ -110,7 +110,7 @@ proc close*[T](s: Selector[T]) =
raiseIOSelectorsError(osLastError())
proc newSelectEvent*(): SelectEvent =
let fdci = eventfd(0, 0)
let fdci = eventfd(0, O_CLOEXEC)
if fdci == -1:
raiseIOSelectorsError(osLastError())
setNonBlocking(fdci)
@@ -269,7 +269,7 @@ proc registerTimer*[T](s: Selector[T], timeout: int, oneshot: bool,
var
newTs: Itimerspec
oldTs: Itimerspec
let fdi = timerfd_create(CLOCK_MONOTONIC, 0).int
let fdi = timerfd_create(CLOCK_MONOTONIC, O_CLOEXEC).int
if fdi == -1:
raiseIOSelectorsError(osLastError())
setNonBlocking(fdi.cint)
@@ -314,7 +314,7 @@ when not defined(android):
discard sigaddset(nmask, cint(signal))
blockSignals(nmask, omask)
let fdi = signalfd(-1, nmask, 0).int
let fdi = signalfd(-1, nmask, O_CLOEXEC).int
if fdi == -1:
raiseIOSelectorsError(osLastError())
setNonBlocking(fdi.cint)
@@ -341,7 +341,7 @@ when not defined(android):
discard sigaddset(nmask, posix.SIGCHLD)
blockSignals(nmask, omask)
let fdi = signalfd(-1, nmask, 0).int
let fdi = signalfd(-1, nmask, O_CLOEXEC).int
if fdi == -1:
raiseIOSelectorsError(osLastError())
setNonBlocking(fdi.cint)

View File

@@ -9,7 +9,7 @@
# This module implements BSD kqueue().
import posix, times, kqueue
import posix, times, kqueue, nativesockets
const
# Maximum number of events that can be returned.
@@ -76,7 +76,7 @@ type
proc getUnique[T](s: Selector[T]): int {.inline.} =
# we create duplicated handles to get unique indexes for our `fds` array.
result = posix.fcntl(s.sock, F_DUPFD, s.sock)
result = posix.fcntl(s.sock, F_DUPFD_CLOEXEC, s.sock)
if result == -1:
raiseIOSelectorsError(osLastError())
@@ -96,8 +96,8 @@ proc newSelector*[T](): owned(Selector[T]) =
# we allocating empty socket to duplicate it handle in future, to get unique
# indexes for `fds` array. This is needed to properly identify
# {Event.Timer, Event.Signal, Event.Process} events.
let usock = posix.socket(posix.AF_INET, posix.SOCK_STREAM,
posix.IPPROTO_TCP).cint
let usock = createNativeSocket(posix.AF_INET, posix.SOCK_STREAM,
posix.IPPROTO_TCP).cint
if usock == -1:
let err = osLastError()
discard posix.close(kqFD)

View File

@@ -228,7 +228,7 @@ proc open*(filename: string, mode: FileMode = fmRead,
if result.handle != -1: discard close(result.handle)
raiseOSError(errCode)
var flags = if readonly: O_RDONLY else: O_RDWR
var flags = (if readonly: O_RDONLY else: O_RDWR) or O_CLOEXEC
if newFileSize != -1:
flags = flags or O_CREAT or O_TRUNC

View File

@@ -185,19 +185,53 @@ proc toSockType*(protocol: Protocol): SockType =
of IPPROTO_IP, IPPROTO_IPV6, IPPROTO_RAW, IPPROTO_ICMP, IPPROTO_ICMPV6:
SOCK_RAW
proc createNativeSocket*(domain: Domain = AF_INET,
sockType: SockType = SOCK_STREAM,
protocol: Protocol = IPPROTO_TCP): SocketHandle =
## Creates a new socket; returns `osInvalidSocket` if an error occurs.
socket(toInt(domain), toInt(sockType), toInt(protocol))
proc close*(socket: SocketHandle) =
## closes a socket.
when useWinVersion:
discard winlean.closesocket(socket)
else:
discard posix.close(socket)
# TODO: These values should not be discarded. An OSError should be raised.
# http://stackoverflow.com/questions/12463473/what-happens-if-you-call-close-on-a-bsd-socket-multiple-times
proc createNativeSocket*(domain: cint, sockType: cint,
protocol: cint): SocketHandle =
when declared(setInheritable) or defined(nimdoc):
proc setInheritable*(s: SocketHandle, inheritable: bool): bool {.inline.} =
## Set whether a socket is inheritable by child processes. Returns `true`
## on success.
##
## This function is not implemented on all platform, test for availability
## with `declared() <system.html#declared,untyped>`.
setInheritable(FileHandle s, inheritable)
proc createNativeSocket*(domain: cint, sockType: cint, protocol: cint,
inheritable: bool = defined(nimInheritHandles)): SocketHandle =
## Creates a new socket; returns `osInvalidSocket` if an error occurs.
##
## `inheritable` decides if the resulting SocketHandle can be inherited
## by child processes.
##
## Use this overload if one of the enums specified above does
## not contain what you need.
socket(domain, sockType, protocol)
let sockType =
when (defined(linux) or defined(bsd)) and not defined(nimdoc):
if inheritable: sockType and not SOCK_CLOEXEC else: sockType or SOCK_CLOEXEC
else:
sockType
result = socket(domain, sockType, protocol)
when declared(setInheritable) and not (defined(linux) or defined(bsd)):
if not setInheritable(result, inheritable):
close result
return osInvalidSocket
proc createNativeSocket*(domain: Domain = AF_INET,
sockType: SockType = SOCK_STREAM,
protocol: Protocol = IPPROTO_TCP,
inheritable: bool = defined(nimInheritHandles)): SocketHandle =
## Creates a new socket; returns `osInvalidSocket` if an error occurs.
##
## `inheritable` decides if the resulting SocketHandle can be inherited
## by child processes.
createNativeSocket(toInt(domain), toInt(sockType), toInt(protocol))
proc newNativeSocket*(domain: Domain = AF_INET,
sockType: SockType = SOCK_STREAM,
@@ -215,15 +249,6 @@ proc newNativeSocket*(domain: cint, sockType: cint,
## not contain what you need.
createNativeSocket(domain, sockType, protocol)
proc close*(socket: SocketHandle) =
## closes a socket.
when useWinVersion:
discard winlean.closesocket(socket)
else:
discard posix.close(socket)
# TODO: These values should not be discarded. An OSError should be raised.
# http://stackoverflow.com/questions/12463473/what-happens-if-you-call-close-on-a-bsd-socket-multiple-times
proc bindAddr*(socket: SocketHandle, name: ptr SockAddr,
namelen: SockLen): cint =
result = bindSocket(socket, name, namelen)
@@ -652,14 +677,25 @@ proc selectWrite*(writefds: var seq[SocketHandle],
pruneSocketSet(writefds, (wr))
proc accept*(fd: SocketHandle): (SocketHandle, string) =
proc accept*(fd: SocketHandle, inheritable = defined(nimInheritHandles)): (SocketHandle, string) =
## Accepts a new client connection.
##
## `inheritable` decides if the resulting SocketHandle can be inherited by
## child processes.
##
## Returns (osInvalidSocket, "") if an error occurred.
var sockAddress: Sockaddr_in
var addrLen = sizeof(sockAddress).SockLen
var sock = accept(fd, cast[ptr SockAddr](addr(sockAddress)),
addr(addrLen))
var sock =
when (defined(linux) or defined(bsd)) and not defined(nimdoc):
accept4(fd, cast[ptr SockAddr](addr(sockAddress)), addr(addrLen),
if inheritable: 0 else: SOCK_CLOEXEC)
else:
accept(fd, cast[ptr SockAddr](addr(sockAddress)), addr(addrLen))
when declared(setInheritable) and not (defined(linux) or defined(bsd)):
if not setInheritable(sock, inheritable):
close sock
sock = osInvalidSocket
if sock == osInvalidSocket:
return (osInvalidSocket, "")
else:

View File

@@ -212,22 +212,32 @@ proc newSocket*(fd: SocketHandle, domain: Domain = AF_INET,
when defined(macosx) and not defined(nimdoc):
setSockOptInt(fd, SOL_SOCKET, SO_NOSIGPIPE, 1)
proc newSocket*(domain, sockType, protocol: cint, buffered = true): owned(Socket) =
proc newSocket*(domain, sockType, protocol: cint, buffered = true,
inheritable = defined(nimInheritHandles)): owned(Socket) =
## Creates a new socket.
##
## The SocketHandle associated with the resulting Socket will not be
## inheritable by child processes by default. This can be changed via
## the `inheritable` parameter.
##
## If an error occurs OSError will be raised.
let fd = createNativeSocket(domain, sockType, protocol)
let fd = createNativeSocket(domain, sockType, protocol, inheritable)
if fd == osInvalidSocket:
raiseOSError(osLastError())
result = newSocket(fd, domain.Domain, sockType.SockType, protocol.Protocol,
buffered)
proc newSocket*(domain: Domain = AF_INET, sockType: SockType = SOCK_STREAM,
protocol: Protocol = IPPROTO_TCP, buffered = true): owned(Socket) =
protocol: Protocol = IPPROTO_TCP, buffered = true,
inheritable = defined(nimInheritHandles)): owned(Socket) =
## Creates a new socket.
##
## The SocketHandle associated with the resulting Socket will not be
## inheritable by child processes by default. This can be changed via
## the `inheritable` parameter.
##
## If an error occurs OSError will be raised.
let fd = createNativeSocket(domain, sockType, protocol)
let fd = createNativeSocket(domain, sockType, protocol, inheritable)
if fd == osInvalidSocket:
raiseOSError(osLastError())
result = newSocket(fd, domain, sockType, protocol, buffered)
@@ -872,7 +882,8 @@ proc bindAddr*(socket: Socket, port = Port(0), address = "") {.
freeaddrinfo(aiList)
proc acceptAddr*(server: Socket, client: var owned(Socket), address: var string,
flags = {SocketFlag.SafeDisconn}) {.
flags = {SocketFlag.SafeDisconn},
inheritable = defined(nimInheritHandles)) {.
tags: [ReadIOEffect], gcsafe, locks: 0.} =
## Blocks until a connection is being made from a client. When a connection
## is made sets ``client`` to the client socket and ``address`` to the address
@@ -882,19 +893,23 @@ proc acceptAddr*(server: Socket, client: var owned(Socket), address: var string,
## The resulting client will inherit any properties of the server socket. For
## example: whether the socket is buffered or not.
##
## The SocketHandle associated with the resulting client will not be
## inheritable by child processes by default. This can be changed via
## the `inheritable` parameter.
##
## The ``accept`` call may result in an error if the connecting socket
## disconnects during the duration of the ``accept``. If the ``SafeDisconn``
## flag is specified then this error will not be raised and instead
## accept will be called again.
if client.isNil:
new(client)
let ret = accept(server.fd)
let ret = accept(server.fd, inheritable)
let sock = ret[0]
if sock == osInvalidSocket:
let err = osLastError()
if flags.isDisconnectionError(err):
acceptAddr(server, client, address, flags)
acceptAddr(server, client, address, flags, inheritable)
raiseOSError(err)
else:
address = ret[1]
@@ -964,10 +979,16 @@ when false: #defineSsl:
doHandshake()
proc accept*(server: Socket, client: var owned(Socket),
flags = {SocketFlag.SafeDisconn}) {.tags: [ReadIOEffect].} =
flags = {SocketFlag.SafeDisconn},
inheritable = defined(nimInheritHandles))
{.tags: [ReadIOEffect].} =
## Equivalent to ``acceptAddr`` but doesn't return the address, only the
## socket.
##
## The SocketHandle associated with the resulting client will not be
## inheritable by child processes by default. This can be changed via
## the `inheritable` parameter.
##
## The ``accept`` call may result in an error if the connecting socket
## disconnects during the duration of the ``accept``. If the ``SafeDisconn``
## flag is specified then this error will not be raised and instead

View File

@@ -259,6 +259,31 @@ else:
IOFBF {.importc: "_IOFBF", nodecl.}: cint
IONBF {.importc: "_IONBF", nodecl.}: cint
const SupportIoctlInheritCtl = (defined(linux) or defined(bsd)) and
not defined(nimscript)
when SupportIoctlInheritCtl:
var
FIOCLEX {.importc, header: "<sys/ioctl.h>".}: cint
FIONCLEX {.importc, header: "<sys/ioctl.h>".}: cint
proc c_ioctl(fd: cint, request: cint): cint {.
importc: "ioctl", header: "<sys/ioctl.h>", varargs.}
elif defined(posix) and not defined(nimscript):
var
F_GETFD {.importc, header: "<fcntl.h>".}: cint
F_SETFD {.importc, header: "<fcntl.h>".}: cint
FD_CLOEXEC {.importc, header: "<fcntl.h>".}: cint
proc c_fcntl(fd: cint, cmd: cint): cint {.
importc: "fcntl", header: "<fcntl.h>", varargs.}
elif defined(windows):
const HANDLE_FLAG_INHERIT = culong 0x1
proc getOsfhandle(fd: cint): FileHandle {.
importc: "_get_osfhandle", header: "<io.h>".}
proc setHandleInformation(handle: FileHandle, mask, flags: culong): cint {.
importc: "SetHandleInformation", header: "<handleapi.h>".}
const
BufSize = 4000
@@ -292,12 +317,29 @@ proc getOsFileHandle*(f: File): FileHandle =
## returns the OS file handle of the file ``f``. This is only useful for
## platform specific programming.
when defined(windows):
proc getOsfhandle(fd: cint): FileHandle {.
importc: "_get_osfhandle", header: "<io.h>".}
result = getOsfhandle(getFileHandle(f))
else:
result = c_fileno(f)
when defined(nimdoc) or (defined(posix) and not defined(nimscript)) or defined(windows):
proc setInheritable*(f: FileHandle, inheritable: bool): bool =
## control whether a file handle can be inherited by child processes. Returns
## ``true`` on success. This requires the OS file handle, which can be
## retrieved via `getOsFileHandle <#getOsFileHandle,File>`_.
##
## This procedure is not guaranteed to be available for all platforms. Test for
## availability with `declared() <system.html#declared,untyped>`.
when SupportIoctlInheritCtl:
result = c_ioctl(f, if inheritable: FIONCLEX else: FIOCLEX) != -1
elif defined(posix):
var flags = c_fcntl(f, F_GETFD)
if flags == -1:
return false
flags = if inheritable: flags and not FD_CLOEXEC else: flags or FD_CLOEXEC
result = c_fcntl(f, F_SETFD, flags) != -1
else:
result = setHandleInformation(f, HANDLE_FLAG_INHERIT, culong inheritable) != 0
proc readLine*(f: File, line: var TaintedString): bool {.tags: [ReadIOEffect],
benign.} =
## reads a line of text from the file `f` into `line`. May throw an IO
@@ -501,7 +543,21 @@ else:
importc: "freopen", nodecl.}
const
FormatOpen: array[FileMode, string] = ["rb", "wb", "w+b", "r+b", "ab"]
NoInheritFlag =
# Platform specific flag for creating a File without inheritance.
when not defined(nimInheritHandles):
when defined(windows):
"N"
elif defined(linux) or defined(bsd):
"e"
else:
""
else:
""
FormatOpen: array[FileMode, string] = [
"rb" & NoInheritFlag, "wb" & NoInheritFlag, "w+b" & NoInheritFlag,
"r+b" & NoInheritFlag, "ab" & NoInheritFlag
]
#"rt", "wt", "w+t", "r+t", "at"
# we always use binary here as for Nim the OS line ending
# should not be translated.
@@ -544,17 +600,25 @@ proc open*(f: var File, filename: string,
##
## Default mode is readonly. Returns true iff the file could be opened.
## This throws no exception if the file could not be opened.
##
## The file handle associated with the resulting ``File`` is not inheritable.
var p = fopen(filename, FormatOpen[mode])
if p != nil:
var f2 = cast[File](p)
when defined(posix) and not defined(nimscript):
# How `fopen` handles opening a directory is not specified in ISO C and
# POSIX. We do not want to handle directories as regular files that can
# be opened.
var f2 = cast[File](p)
var res: Stat
if c_fstat(getFileHandle(f2), res) >= 0'i32 and modeIsDir(res.st_mode):
close(f2)
return false
when not defined(nimInheritHandles) and declared(setInheritable) and
NoInheritFlag.len == 0:
if not setInheritable(getOsFileHandle(f2), false):
close(f2)
return false
result = true
f = cast[File](p)
if bufSize > 0 and bufSize <= high(cint).int:
@@ -569,13 +633,27 @@ proc reopen*(f: File, filename: string, mode: FileMode = fmRead): bool {.
## file variables.
##
## Default mode is readonly. Returns true iff the file could be reopened.
result = freopen(filename, FormatOpen[mode], f) != nil
##
## The file handle associated with `f` won't be inheritable.
if freopen(filename, FormatOpen[mode], f) != nil:
when not defined(nimInheritHandles) and declared(setInheritable) and
NoInheritFlag.len == 0:
if not setInheritable(getOsFileHandle(f), false):
close(f)
return false
result = true
proc open*(f: var File, filehandle: FileHandle,
mode: FileMode = fmRead): bool {.tags: [], raises: [], benign.} =
## Creates a ``File`` from a `filehandle` with given `mode`.
##
## Default mode is readonly. Returns true iff the file could be opened.
##
## The passed file handle will no longer be inheritable.
when not defined(nimInheritHandles) and declared(setInheritable):
let oshandle = when defined(windows): getOsfhandle(filehandle) else: filehandle
if not setInheritable(oshandle, false):
return false
f = c_fdopen(filehandle, FormatOpen[mode])
result = f != nil
@@ -585,6 +663,8 @@ proc open*(filename: string,
##
## Default mode is readonly. Raises an ``IOError`` if the file
## could not be opened.
##
## The file handle associated with the resulting ``File`` is not inheritable.
if not open(result, filename, mode, bufSize):
sysFatal(IOError, "cannot open: " & filename)

View File

@@ -126,6 +126,8 @@ const
CREATE_NO_WINDOW* = 0x08000000'i32
HANDLE_FLAG_INHERIT* = 0x00000001'i32
proc getVersionExW*(lpVersionInfo: ptr OSVERSIONINFO): WINBOOL {.
stdcall, dynlib: "kernel32", importc: "GetVersionExW", sideEffect.}
proc getVersionExA*(lpVersionInfo: ptr OSVERSIONINFO): WINBOOL {.
@@ -708,6 +710,9 @@ proc duplicateHandle*(hSourceProcessHandle: Handle, hSourceHandle: Handle,
dwOptions: DWORD): WINBOOL{.stdcall, dynlib: "kernel32",
importc: "DuplicateHandle".}
proc getHandleInformation*(hObject: Handle, lpdwFlags: ptr DWORD): WINBOOL {.
stdcall, dynlib: "kernel32", importc: "GetHandleInformation".}
proc setHandleInformation*(hObject: Handle, dwMask: DWORD,
dwFlags: DWORD): WINBOOL {.stdcall,
dynlib: "kernel32", importc: "SetHandleInformation".}

85
tests/stdlib/tfdleak.nim Normal file
View File

@@ -0,0 +1,85 @@
discard """
exitcode: 0
output: ""
matrix: "; -d:nimInheritHandles"
"""
import os, osproc, strutils, nativesockets, net, selectors, memfiles
when defined(windows):
import winlean
else:
import posix
proc leakCheck(f: int | FileHandle | SocketHandle, msg: string, expectLeak = defined(nimInheritHandles)) =
discard startProcess(
getAppFilename(),
args = @[$f.int, msg, $expectLeak],
options = {poParentStreams}
).waitForExit -1
proc isValidHandle(f: int): bool =
## Check if a handle is valid. Requires OS-native handles.
when defined(windows):
var flags: DWORD
result = getHandleInformation(f.Handle, addr flags) != 0
else:
result = fcntl(f.cint, F_GETFD) != -1
proc main() =
if paramCount() == 0:
# Parent process
let f = system.open("__test_fdleak", fmReadWrite)
defer: close f
leakCheck(f.getOsFileHandle, "system.open()")
doAssert f.reopen("__test_fdleak2", fmReadWrite), "reopen failed"
leakCheck(f.getOsFileHandle, "reopen")
let sock = createNativeSocket()
defer: close sock
leakCheck(sock, "createNativeSocket()")
if sock.setInheritable(true):
leakCheck(sock, "createNativeSocket()", true)
else:
raiseOSError osLastError()
let server = newSocket()
defer: close server
server.bindAddr()
server.listen()
let (_, port) = server.getLocalAddr
leakCheck(server.getFd, "newSocket()")
let client = newSocket()
defer: close client
client.connect("127.0.0.1", port)
var input: Socket
server.accept(input)
leakCheck(input.getFd, "accept()")
# ioselectors_select doesn't support returning a handle.
when not defined(windows):
let selector = newSelector[int]()
leakCheck(selector.getFd, "selector()", false)
var mf = memfiles.open("__test_fdleak3", fmReadWrite, newFileSize = 1)
defer: close mf
when defined(windows):
leakCheck(mf.fHandle, "memfiles.open().fHandle", false)
leakCheck(mf.mapHandle, "memfiles.open().mapHandle", false)
else:
leakCheck(mf.handle, "memfiles.open().handle", false)
else:
let
fd = parseInt(paramStr 1)
expectLeak = parseBool(paramStr 3)
msg = (if expectLeak: "not " else: "") & "leaked " & paramStr 2
if expectLeak xor fd.isValidHandle:
echo msg
when isMainModule: main()

View File

@@ -228,6 +228,7 @@ v("EXDEV")
header("<fcntl.h>")
v("F_DUPFD")
v("F_DUPFD_CLOEXEC")
v("F_GETFD")
v("F_SETFD")
v("F_GETFL")
@@ -254,6 +255,7 @@ v("O_ACCMODE")
v("O_RDONLY")
v("O_RDWR")
v("O_WRONLY")
v("O_CLOEXEC")
v("POSIX_FADV_NORMAL")
v("POSIX_FADV_SEQUENTIAL")
v("POSIX_FADV_RANDOM")
@@ -621,6 +623,7 @@ v("SOCK_DGRAM")
v("SOCK_RAW")
v("SOCK_SEQPACKET")
v("SOCK_STREAM")
v("SOCK_CLOEXEC", no_other = true)
v("SOL_SOCKET")
v("SOMAXCONN")
v("SO_REUSEPORT", no_other = true)