mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-09-20 01:37:39 +03:00
Kernel: Add a systrace() syscall and implement /bin/strace using it.
Calling systrace(pid) gives you a file descriptor with a stream of the syscalls made by a peer process. The process must be owned by the same UID who calls systrace(). :^)
This commit is contained in:
parent
6693cfb26a
commit
5c68929aa1
Notes:
sideshowbarker
2024-07-19 14:37:27 +09:00
Author: https://github.com/awesomekling Commit: https://github.com/SerenityOS/serenity/commit/5c68929aa17
@ -12,6 +12,7 @@
|
|||||||
#include <Kernel/Devices/BlockDevice.h>
|
#include <Kernel/Devices/BlockDevice.h>
|
||||||
#include <Kernel/VM/MemoryManager.h>
|
#include <Kernel/VM/MemoryManager.h>
|
||||||
#include <Kernel/SharedMemory.h>
|
#include <Kernel/SharedMemory.h>
|
||||||
|
#include <Kernel/ProcessTracer.h>
|
||||||
|
|
||||||
Retained<FileDescriptor> FileDescriptor::create(RetainPtr<Inode>&& inode)
|
Retained<FileDescriptor> FileDescriptor::create(RetainPtr<Inode>&& inode)
|
||||||
{
|
{
|
||||||
@ -23,6 +24,11 @@ Retained<FileDescriptor> FileDescriptor::create(RetainPtr<Device>&& device)
|
|||||||
return adopt(*new FileDescriptor(move(device)));
|
return adopt(*new FileDescriptor(move(device)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Retained<FileDescriptor> FileDescriptor::create(RetainPtr<ProcessTracer>&& tracer)
|
||||||
|
{
|
||||||
|
return adopt(*new FileDescriptor(move(tracer)));
|
||||||
|
}
|
||||||
|
|
||||||
Retained<FileDescriptor> FileDescriptor::create(RetainPtr<SharedMemory>&& shared_memory)
|
Retained<FileDescriptor> FileDescriptor::create(RetainPtr<SharedMemory>&& shared_memory)
|
||||||
{
|
{
|
||||||
return adopt(*new FileDescriptor(move(shared_memory)));
|
return adopt(*new FileDescriptor(move(shared_memory)));
|
||||||
@ -53,6 +59,11 @@ FileDescriptor::FileDescriptor(RetainPtr<Device>&& device)
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileDescriptor::FileDescriptor(RetainPtr<ProcessTracer>&& tracer)
|
||||||
|
: m_tracer(move(tracer))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
FileDescriptor::FileDescriptor(RetainPtr<SharedMemory>&& shared_memory)
|
FileDescriptor::FileDescriptor(RetainPtr<SharedMemory>&& shared_memory)
|
||||||
: m_shared_memory(move(shared_memory))
|
: m_shared_memory(move(shared_memory))
|
||||||
{
|
{
|
||||||
@ -199,6 +210,8 @@ off_t FileDescriptor::seek(off_t offset, int whence)
|
|||||||
|
|
||||||
ssize_t FileDescriptor::read(Process& process, byte* buffer, ssize_t count)
|
ssize_t FileDescriptor::read(Process& process, byte* buffer, ssize_t count)
|
||||||
{
|
{
|
||||||
|
if (m_tracer)
|
||||||
|
return m_tracer->read(buffer, count);
|
||||||
if (is_fifo()) {
|
if (is_fifo()) {
|
||||||
ASSERT(fifo_direction() == FIFO::Reader);
|
ASSERT(fifo_direction() == FIFO::Reader);
|
||||||
return m_fifo->read(buffer, count);
|
return m_fifo->read(buffer, count);
|
||||||
@ -217,6 +230,10 @@ ssize_t FileDescriptor::read(Process& process, byte* buffer, ssize_t count)
|
|||||||
|
|
||||||
ssize_t FileDescriptor::write(Process& process, const byte* data, ssize_t size)
|
ssize_t FileDescriptor::write(Process& process, const byte* data, ssize_t size)
|
||||||
{
|
{
|
||||||
|
if (m_tracer) {
|
||||||
|
// FIXME: Figure out what the right error code would be.
|
||||||
|
return -EIO;
|
||||||
|
}
|
||||||
if (is_fifo()) {
|
if (is_fifo()) {
|
||||||
ASSERT(fifo_direction() == FIFO::Writer);
|
ASSERT(fifo_direction() == FIFO::Writer);
|
||||||
return m_fifo->write(data, size);
|
return m_fifo->write(data, size);
|
||||||
@ -235,6 +252,8 @@ ssize_t FileDescriptor::write(Process& process, const byte* data, ssize_t size)
|
|||||||
|
|
||||||
bool FileDescriptor::can_write(Process& process)
|
bool FileDescriptor::can_write(Process& process)
|
||||||
{
|
{
|
||||||
|
if (m_tracer)
|
||||||
|
return true;
|
||||||
if (is_fifo()) {
|
if (is_fifo()) {
|
||||||
ASSERT(fifo_direction() == FIFO::Writer);
|
ASSERT(fifo_direction() == FIFO::Writer);
|
||||||
return m_fifo->can_write();
|
return m_fifo->can_write();
|
||||||
@ -248,6 +267,8 @@ bool FileDescriptor::can_write(Process& process)
|
|||||||
|
|
||||||
bool FileDescriptor::can_read(Process& process)
|
bool FileDescriptor::can_read(Process& process)
|
||||||
{
|
{
|
||||||
|
if (m_tracer)
|
||||||
|
return m_tracer->can_read();
|
||||||
if (is_fifo()) {
|
if (is_fifo()) {
|
||||||
ASSERT(fifo_direction() == FIFO::Reader);
|
ASSERT(fifo_direction() == FIFO::Reader);
|
||||||
return m_fifo->can_read();
|
return m_fifo->can_read();
|
||||||
@ -376,6 +397,8 @@ bool FileDescriptor::is_file() const
|
|||||||
KResultOr<String> FileDescriptor::absolute_path()
|
KResultOr<String> FileDescriptor::absolute_path()
|
||||||
{
|
{
|
||||||
Stopwatch sw("absolute_path");
|
Stopwatch sw("absolute_path");
|
||||||
|
if (m_tracer)
|
||||||
|
return String::format("tracer:%d", m_tracer->pid());
|
||||||
if (is_tty())
|
if (is_tty())
|
||||||
return tty()->tty_name();
|
return tty()->tty_name();
|
||||||
if (is_fifo())
|
if (is_fifo())
|
||||||
@ -405,6 +428,8 @@ InodeMetadata FileDescriptor::metadata() const
|
|||||||
|
|
||||||
bool FileDescriptor::supports_mmap() const
|
bool FileDescriptor::supports_mmap() const
|
||||||
{
|
{
|
||||||
|
if (m_tracer)
|
||||||
|
return false;
|
||||||
if (m_inode)
|
if (m_inode)
|
||||||
return true;
|
return true;
|
||||||
if (m_shared_memory)
|
if (m_shared_memory)
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
class TTY;
|
class TTY;
|
||||||
class MasterPTY;
|
class MasterPTY;
|
||||||
class Process;
|
class Process;
|
||||||
|
class ProcessTracer;
|
||||||
class Region;
|
class Region;
|
||||||
class CharacterDevice;
|
class CharacterDevice;
|
||||||
|
|
||||||
@ -24,6 +25,7 @@ public:
|
|||||||
static Retained<FileDescriptor> create(RetainPtr<Inode>&&);
|
static Retained<FileDescriptor> create(RetainPtr<Inode>&&);
|
||||||
static Retained<FileDescriptor> create(RetainPtr<Device>&&);
|
static Retained<FileDescriptor> create(RetainPtr<Device>&&);
|
||||||
static Retained<FileDescriptor> create(RetainPtr<SharedMemory>&&);
|
static Retained<FileDescriptor> create(RetainPtr<SharedMemory>&&);
|
||||||
|
static Retained<FileDescriptor> create(RetainPtr<ProcessTracer>&&);
|
||||||
static Retained<FileDescriptor> create_pipe_writer(FIFO&);
|
static Retained<FileDescriptor> create_pipe_writer(FIFO&);
|
||||||
static Retained<FileDescriptor> create_pipe_reader(FIFO&);
|
static Retained<FileDescriptor> create_pipe_reader(FIFO&);
|
||||||
~FileDescriptor();
|
~FileDescriptor();
|
||||||
@ -101,11 +103,15 @@ public:
|
|||||||
|
|
||||||
KResult truncate(off_t);
|
KResult truncate(off_t);
|
||||||
|
|
||||||
|
ProcessTracer* tracer() { return m_tracer.ptr(); }
|
||||||
|
const ProcessTracer* tracer() const { return m_tracer.ptr(); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class VFS;
|
friend class VFS;
|
||||||
FileDescriptor(RetainPtr<Socket>&&, SocketRole);
|
FileDescriptor(RetainPtr<Socket>&&, SocketRole);
|
||||||
explicit FileDescriptor(RetainPtr<Inode>&&);
|
explicit FileDescriptor(RetainPtr<Inode>&&);
|
||||||
explicit FileDescriptor(RetainPtr<Device>&&);
|
explicit FileDescriptor(RetainPtr<Device>&&);
|
||||||
|
explicit FileDescriptor(RetainPtr<ProcessTracer>&&);
|
||||||
explicit FileDescriptor(RetainPtr<SharedMemory>&&);
|
explicit FileDescriptor(RetainPtr<SharedMemory>&&);
|
||||||
FileDescriptor(FIFO&, FIFO::Direction);
|
FileDescriptor(FIFO&, FIFO::Direction);
|
||||||
|
|
||||||
@ -126,6 +132,7 @@ private:
|
|||||||
FIFO::Direction m_fifo_direction { FIFO::Neither };
|
FIFO::Direction m_fifo_direction { FIFO::Neither };
|
||||||
|
|
||||||
RetainPtr<SharedMemory> m_shared_memory;
|
RetainPtr<SharedMemory> m_shared_memory;
|
||||||
|
RetainPtr<ProcessTracer> m_tracer;
|
||||||
|
|
||||||
bool m_closed { false };
|
bool m_closed { false };
|
||||||
};
|
};
|
||||||
|
@ -47,7 +47,8 @@ KERNEL_OBJS = \
|
|||||||
Net/E1000NetworkAdapter.o \
|
Net/E1000NetworkAdapter.o \
|
||||||
Net/LoopbackAdapter.o \
|
Net/LoopbackAdapter.o \
|
||||||
Net/Routing.o \
|
Net/Routing.o \
|
||||||
Net/NetworkTask.o
|
Net/NetworkTask.o \
|
||||||
|
ProcessTracer.o
|
||||||
|
|
||||||
VFS_OBJS = \
|
VFS_OBJS = \
|
||||||
FileSystem/ProcFS.o \
|
FileSystem/ProcFS.o \
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
#include <Kernel/ELF/exec_elf.h>
|
#include <Kernel/ELF/exec_elf.h>
|
||||||
#include <AK/StringBuilder.h>
|
#include <AK/StringBuilder.h>
|
||||||
#include <Kernel/SharedMemory.h>
|
#include <Kernel/SharedMemory.h>
|
||||||
|
#include <Kernel/ProcessTracer.h>
|
||||||
|
|
||||||
//#define DEBUG_IO
|
//#define DEBUG_IO
|
||||||
//#define TASK_DEBUG
|
//#define TASK_DEBUG
|
||||||
@ -1894,6 +1895,9 @@ void Process::finalize()
|
|||||||
|
|
||||||
void Process::die()
|
void Process::die()
|
||||||
{
|
{
|
||||||
|
if (m_tracer)
|
||||||
|
m_tracer->set_dead();
|
||||||
|
|
||||||
{
|
{
|
||||||
InterruptDisabler disabler;
|
InterruptDisabler disabler;
|
||||||
for_each_thread([] (Thread& thread) {
|
for_each_thread([] (Thread& thread) {
|
||||||
@ -2482,3 +2486,26 @@ int Process::sys$ftruncate(int fd, off_t length)
|
|||||||
return -EINVAL;
|
return -EINVAL;
|
||||||
return descriptor->truncate(length);
|
return descriptor->truncate(length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int Process::sys$systrace(pid_t pid)
|
||||||
|
{
|
||||||
|
InterruptDisabler disabler;
|
||||||
|
auto* peer = Process::from_pid(pid);
|
||||||
|
if (!peer)
|
||||||
|
return -ESRCH;
|
||||||
|
if (peer->uid() != m_euid)
|
||||||
|
return -EACCES;
|
||||||
|
int fd = alloc_fd();
|
||||||
|
if (fd < 0)
|
||||||
|
return fd;
|
||||||
|
auto descriptor = FileDescriptor::create(peer->ensure_tracer());
|
||||||
|
m_fds[fd].set(move(descriptor), 0);
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessTracer& Process::ensure_tracer()
|
||||||
|
{
|
||||||
|
if (!m_tracer)
|
||||||
|
m_tracer = ProcessTracer::create(m_pid);
|
||||||
|
return *m_tracer;
|
||||||
|
}
|
||||||
|
@ -18,6 +18,7 @@ class FileDescriptor;
|
|||||||
class PageDirectory;
|
class PageDirectory;
|
||||||
class Region;
|
class Region;
|
||||||
class VMObject;
|
class VMObject;
|
||||||
|
class ProcessTracer;
|
||||||
|
|
||||||
void kgettimeofday(timeval&);
|
void kgettimeofday(timeval&);
|
||||||
|
|
||||||
@ -175,6 +176,7 @@ public:
|
|||||||
int sys$restore_signal_mask(dword mask);
|
int sys$restore_signal_mask(dword mask);
|
||||||
int sys$create_thread(int(*)(void*), void*);
|
int sys$create_thread(int(*)(void*), void*);
|
||||||
int sys$rename(const char* oldpath, const char* newpath);
|
int sys$rename(const char* oldpath, const char* newpath);
|
||||||
|
int sys$systrace(pid_t);
|
||||||
|
|
||||||
int sys$create_shared_buffer(pid_t peer_pid, int, void** buffer);
|
int sys$create_shared_buffer(pid_t peer_pid, int, void** buffer);
|
||||||
void* sys$get_shared_buffer(int shared_buffer_id);
|
void* sys$get_shared_buffer(int shared_buffer_id);
|
||||||
@ -194,6 +196,9 @@ public:
|
|||||||
const Vector<Retained<Region>>& regions() const { return m_regions; }
|
const Vector<Retained<Region>>& regions() const { return m_regions; }
|
||||||
void dump_regions();
|
void dump_regions();
|
||||||
|
|
||||||
|
ProcessTracer* tracer() { return m_tracer.ptr(); }
|
||||||
|
ProcessTracer& ensure_tracer();
|
||||||
|
|
||||||
dword m_ticks_in_user { 0 };
|
dword m_ticks_in_user { 0 };
|
||||||
dword m_ticks_in_kernel { 0 };
|
dword m_ticks_in_kernel { 0 };
|
||||||
|
|
||||||
@ -318,6 +323,8 @@ private:
|
|||||||
|
|
||||||
unsigned m_syscall_count { 0 };
|
unsigned m_syscall_count { 0 };
|
||||||
|
|
||||||
|
RetainPtr<ProcessTracer> m_tracer;
|
||||||
|
|
||||||
Lock m_big_lock;
|
Lock m_big_lock;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
31
Kernel/ProcessTracer.cpp
Normal file
31
Kernel/ProcessTracer.cpp
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#include <Kernel/ProcessTracer.h>
|
||||||
|
#include <AK/kstdio.h>
|
||||||
|
|
||||||
|
ProcessTracer::ProcessTracer(pid_t pid)
|
||||||
|
: m_pid(pid)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessTracer::~ProcessTracer()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProcessTracer::did_syscall(dword function, dword arg1, dword arg2, dword arg3, dword result)
|
||||||
|
{
|
||||||
|
CallData data = { function, arg1, arg2, arg3, result };
|
||||||
|
m_calls.enqueue(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
int ProcessTracer::read(byte* buffer, int buffer_size)
|
||||||
|
{
|
||||||
|
if (!m_calls.is_empty()) {
|
||||||
|
auto data = m_calls.dequeue();
|
||||||
|
// FIXME: This should not be an assertion.
|
||||||
|
ASSERT(buffer_size == sizeof(data));
|
||||||
|
memcpy(buffer, &data, sizeof(data));
|
||||||
|
return sizeof(data);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
36
Kernel/ProcessTracer.h
Normal file
36
Kernel/ProcessTracer.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <AK/Retainable.h>
|
||||||
|
#include <AK/Retained.h>
|
||||||
|
#include <AK/CircularQueue.h>
|
||||||
|
#include <Kernel/UnixTypes.h>
|
||||||
|
|
||||||
|
class ProcessTracer : public Retainable<ProcessTracer> {
|
||||||
|
public:
|
||||||
|
static Retained<ProcessTracer> create(pid_t pid) { return adopt(*new ProcessTracer(pid)); }
|
||||||
|
~ProcessTracer();
|
||||||
|
|
||||||
|
bool is_dead() const { return m_dead; }
|
||||||
|
void set_dead() { m_dead = true; }
|
||||||
|
|
||||||
|
bool can_read() const { return !m_calls.is_empty() || m_dead; }
|
||||||
|
int read(byte*, int);
|
||||||
|
|
||||||
|
void did_syscall(dword function, dword arg1, dword arg2, dword arg3, dword result);
|
||||||
|
pid_t pid() const { return m_pid; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit ProcessTracer(pid_t);
|
||||||
|
|
||||||
|
struct CallData {
|
||||||
|
dword function;
|
||||||
|
dword arg1;
|
||||||
|
dword arg2;
|
||||||
|
dword arg3;
|
||||||
|
dword result;
|
||||||
|
};
|
||||||
|
|
||||||
|
pid_t m_pid;
|
||||||
|
bool m_dead { false };
|
||||||
|
CircularQueue<CallData, 200> m_calls;
|
||||||
|
};
|
@ -3,6 +3,7 @@
|
|||||||
#include "Syscall.h"
|
#include "Syscall.h"
|
||||||
#include "Console.h"
|
#include "Console.h"
|
||||||
#include "Scheduler.h"
|
#include "Scheduler.h"
|
||||||
|
#include <Kernel/ProcessTracer.h>
|
||||||
|
|
||||||
extern "C" void syscall_trap_entry(RegisterDump&);
|
extern "C" void syscall_trap_entry(RegisterDump&);
|
||||||
extern "C" void syscall_trap_handler();
|
extern "C" void syscall_trap_handler();
|
||||||
@ -251,6 +252,8 @@ static dword handle(RegisterDump& regs, dword function, dword arg1, dword arg2,
|
|||||||
return current->process().sys$shm_unlink((const char*)arg1);
|
return current->process().sys$shm_unlink((const char*)arg1);
|
||||||
case Syscall::SC_ftruncate:
|
case Syscall::SC_ftruncate:
|
||||||
return current->process().sys$ftruncate((int)arg1, (off_t)arg2);
|
return current->process().sys$ftruncate((int)arg1, (off_t)arg2);
|
||||||
|
case Syscall::SC_systrace:
|
||||||
|
return current->process().sys$systrace((pid_t)arg1);
|
||||||
default:
|
default:
|
||||||
kprintf("<%u> int0x82: Unknown function %u requested {%x, %x, %x}\n", current->process().pid(), function, arg1, arg2, arg3);
|
kprintf("<%u> int0x82: Unknown function %u requested {%x, %x, %x}\n", current->process().pid(), function, arg1, arg2, arg3);
|
||||||
break;
|
break;
|
||||||
@ -268,6 +271,8 @@ void syscall_trap_entry(RegisterDump& regs)
|
|||||||
dword arg2 = regs.ecx;
|
dword arg2 = regs.ecx;
|
||||||
dword arg3 = regs.ebx;
|
dword arg3 = regs.ebx;
|
||||||
regs.eax = Syscall::handle(regs, function, arg1, arg2, arg3);
|
regs.eax = Syscall::handle(regs, function, arg1, arg2, arg3);
|
||||||
|
if (auto* tracer = current->process().tracer())
|
||||||
|
tracer->did_syscall(function, arg1, arg2, arg3, regs.eax);
|
||||||
current->process().big_lock().unlock();
|
current->process().big_lock().unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,6 +99,7 @@
|
|||||||
__ENUMERATE_SYSCALL(shm_open) \
|
__ENUMERATE_SYSCALL(shm_open) \
|
||||||
__ENUMERATE_SYSCALL(shm_close) \
|
__ENUMERATE_SYSCALL(shm_close) \
|
||||||
__ENUMERATE_SYSCALL(ftruncate) \
|
__ENUMERATE_SYSCALL(ftruncate) \
|
||||||
|
__ENUMERATE_SYSCALL(systrace) \
|
||||||
|
|
||||||
|
|
||||||
namespace Syscall {
|
namespace Syscall {
|
||||||
|
@ -16,6 +16,12 @@
|
|||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
|
||||||
|
int systrace(pid_t pid)
|
||||||
|
{
|
||||||
|
int rc = syscall(SC_systrace, pid);
|
||||||
|
__RETURN_WITH_ERRNO(rc, rc, -1);
|
||||||
|
}
|
||||||
|
|
||||||
int chown(const char* pathname, uid_t uid, gid_t gid)
|
int chown(const char* pathname, uid_t uid, gid_t gid)
|
||||||
{
|
{
|
||||||
int rc = syscall(SC_chown, pathname, uid, gid);
|
int rc = syscall(SC_chown, pathname, uid, gid);
|
||||||
|
@ -14,6 +14,7 @@ __BEGIN_DECLS
|
|||||||
|
|
||||||
extern char** environ;
|
extern char** environ;
|
||||||
|
|
||||||
|
int systrace(pid_t);
|
||||||
int gettid();
|
int gettid();
|
||||||
int donate(int tid);
|
int donate(int tid);
|
||||||
int create_thread(int(*)(void*), void*);
|
int create_thread(int(*)(void*), void*);
|
||||||
|
40
Userland/strace.cpp
Normal file
40
Userland/strace.cpp
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <AK/Assertions.h>
|
||||||
|
#include <AK/Types.h>
|
||||||
|
#include <Kernel/Syscall.h>
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
if (argc < 2)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
int pid = atoi(argv[1]);
|
||||||
|
int fd = systrace(pid);
|
||||||
|
if (fd < 0) {
|
||||||
|
perror("systrace");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
dword call[5];
|
||||||
|
int nread = read(fd, &call, sizeof(call));
|
||||||
|
if (nread == 0)
|
||||||
|
break;
|
||||||
|
if (nread < 0) {
|
||||||
|
perror("read");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
ASSERT(nread == sizeof(call));
|
||||||
|
printf("%s(%#x, %#x, %#x) = %#x\n", Syscall::to_string((Syscall::Function)call[0]), call[1], call[2], call[3], call[4]);
|
||||||
|
}
|
||||||
|
|
||||||
|
int rc = close(fd);
|
||||||
|
if (rc < 0) {
|
||||||
|
perror("close");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user