Skip to content

workaround for broken io.popen and os.execute in luajit #1465

@rweichler

Description

@rweichler

On palera1n rootless, io.popen and os.execute are broken. I think a proper fix would require patching the source of luajit (replace the calls to system and popen with posix_spawn). But nobody's got time for that. So here's some FFI code. Examples at the bottom.

ffi = require 'ffi'
C = ffi.C

LazyCdef = function(s, lib)
    lib = lib or C
    local name = assert(s:match'^[^%s]+%s+([^(]+)%(')
    assert(not name:find'%s' and not name:find'%*', 'invalid cdef: '..s)
    local fn = function() return lib[name] end
    if not pcall(fn) then
        ffi.cdef(s)
    end
    return fn()
end

local pidPtr
Pspawn = function(args, callback)
    if type(args) == 'string' then
        args = args:find'%s'
            and {'sh', '-c', args}
            or {args}
    end
    if callback then
        assert(type(callback) == 'function', 'expected function')
        if not pcall(function() return ffi.typeof'struct pollfd' end) then
            ffi.cdef'struct pollfd { int fd; short events; short revents; }'
            ffi.typeof'struct pollfd'
        end
        LazyCdef'int poll(struct pollfd *, unsigned int, int);'
    end
    local bit_band = bit and bit.band or load'local a,b = ...; return a&b'
    -- pid_t is 32-bit on every platform that matters
    LazyCdef'int32_t waitpid(int32_t, int *, int);'
    LazyCdef'int posix_spawnp(int32_t *pid, const char *arg0, void *, void *, const char *argv[], const char *envp[]);'
    LazyCdef'int posix_spawn_file_actions_init(void **);'
    LazyCdef'int posix_spawn_file_actions_destroy(void **);'
    LazyCdef'int posix_spawn_file_actions_addclose(void **, int);'
    LazyCdef'int posix_spawn_file_actions_adddup2(void **, int, int);'
    LazyCdef'int pipe(int[2]);'
    LazyCdef'int close(int);'
    LazyCdef'ssize_t read(int, void *, size_t);'
    local STDIN_FILENO = 0
    local STDOUT_FILENO = 1
    local STDERR_FILENO = 2
    local EINTR = 4
    local POLLIN = 0x0001 -- in macOS arm64 and Linux x64
    local POLLERR = 0x0008 -- in macOS arm64 and Linux x64
    local POLLHUP = 0x0010 -- in macOS arm64 and Linux x64
    local POLLNVAL = 0x0020 -- in macOS arm64 (not in Linux x64)
    -- setup fa, outfd, errfd
    -- proxy for posix_spawn_file_actions_t;. made it huge just to be safe
    local fa = ffi.new'void *[8192]'
    local outfd = ffi.new('int[2]', -1, -1)
    local errfd = ffi.new('int[2]', -1, -1)
    local faInitted = false
    function rc(fn, ...)
        local rc = fn(...)
        if rc == 0 then return end
        if faInitted then C.posix_spawn_file_actions_destroy(fa) end
        if outfd[0] ~= -1 then C.close(outfd[0]) end
        if errfd[0] ~= -1 then C.close(errfd[0]) end
        if outfd[1] ~= -1 then C.close(outfd[1]) end
        if errfd[1] ~= -1 then C.close(errfd[1]) end
        error('rc is '..rc)
    end
    rc(C.posix_spawn_file_actions_init, fa)
    faInitted = true
    rc(C.pipe, outfd)
    rc(C.pipe, errfd)
    rc(C.posix_spawn_file_actions_adddup2, fa, outfd[1], STDOUT_FILENO)
    rc(C.posix_spawn_file_actions_adddup2, fa, errfd[1], STDERR_FILENO)
    rc(C.posix_spawn_file_actions_addclose, fa, outfd[0])
    rc(C.posix_spawn_file_actions_addclose, fa, errfd[0])
    rc(C.posix_spawn_file_actions_addclose, fa, outfd[1])
    rc(C.posix_spawn_file_actions_addclose, fa, errfd[1])
    -- setup other args for posix_spawn
    local argv = ffi.new('const char*[?]', #args + 1, args)
    argv[#args] = nil
    pidPtr = pidPtr or ffi.new('int32_t[1]', -1)
    local envp = ffi.cast('const char **', nil)
    -- critical function
    rc(C.posix_spawnp, pidPtr, argv[0], fa, nil, argv, envp)
    -- read from stdout / stderr
    C.posix_spawn_file_actions_destroy(fa)
    C.close(outfd[1])
    C.close(errfd[1])
    outfd[1] = -1
    errfd[1] = -1
    local buf = ffi.new'uint8_t[8192]'
    local function readfd(fd)
        local ans = {}
        while true do
            local n = C.read(fd, buf, ffi.sizeof(buf))
            if n > 0 then
                table.insert(ans, ffi.string(buf, n))
            elseif n == 0 or ffi.errno() ~= EINTR then
                -- eof or read error
                break
            end
        end
        C.close(fd)
        return table.concat(ans)
    end
    local stdout, stderr
    if callback then
        local pfds = ffi.new('struct pollfd[2]')
        pfds[0].fd = outfd[0]
        pfds[1].fd = errfd[0]
        pfds[0].events = POLLIN
        pfds[1].events = POLLIN
        while pfds[0].fd ~= -1 and pfds[1].fd ~= -1 do
            local n = C.poll(pfds, 2, -1)
            if n == 0 or (n < 0 and ffi.errno() == EINTR) then
                -- EAGAIN, i dont think this ever gets hit cuz theres no timeout
            elseif n < 0 then
                error'poll'
            else
                for i=0,1 do
                    if bit_band(pfds[i].revents, POLLERR+POLLHUP+POLLNVAL) ~= 0 then
                        -- eof probably
                        C.close(pfds[i].fd)
                        pfds[i].fd = -1
                    elseif bit_band(pfds[i].revents, POLLIN) ~= 0 then
                        local n = C.read(pfds[i].fd, buf, ffi.sizeof(buf))
                        local s = ffi.string(buf, n)
                        if i == 0 then
                            callback(s, nil)
                        else
                            callback(nil, s)
                        end
                    end
                end
            end
        end
    else
        stdout = readfd(outfd[0])
        stderr = readfd(errfd[0])
    end
    local statusPtr = ffi.new('int[1]')
    local w
    while not w or (w == -1 and ffi.errno() == EINTR) do
        w = C.waitpid(pidPtr[0], statusPtr, 0)
    end
    return bit_band(statusPtr[0], 0x7f) == 0 and 0 or statusPtr[0],
        stdout,
        stderr
end

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions