summaryrefslogtreecommitdiff
path: root/kernel/poll.cc
blob: 36744911ec09cf2c22a21ca2c5c57136d580feed (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
/* SPDX-License-Identifier: GPL-2.0-or-later */
/* Copyright (C) 2026 Richard Qin */

/*
 * sys_ppoll — POSIX ppoll() implementation.
 *
 * Linux fs/select.c:do_sys_poll() pattern, simplified for our needs:
 *   - Two-phase loop: "register-then-check".  First pass calls every
 *     f_op->poll() with a real poll_table so drivers can register their
 *     wait queues; if any fd is already ready we return immediately.
 *     Otherwise schedule() until a wq fires (or timeout/signal), then
 *     re-check with poll_table=nullptr (probe-only).
 *
 * Limitations vs. Linux:
 *   - Only a single wait queue per ppoll call is tracked (the first one
 *     a driver hands to poll_wait).  Multi-fd polls work, but only the
 *     first wq's wake actually re-runs the loop; other fds becoming
 *     ready won't be noticed until the timer fires or another wake.
 *     BusyBox interactive shell polls one fd (stdin), so this matches.
 *
 * Reference: Linux fs/select.c
 */

#include <lume/poll.h>
#include <lume/file.h>
#include <lume/fdtable.h>
#include <lume/task.h>
#include <lume/sched.h>
#include <lume/timer.h>
#include <lume/waitqueue.h>
#include <lume/signal.h>
#include <lume/signal_types.h>
#include <lume/uaccess.h>
#include <lume/errno.h>
#include <lume/klog.h>
#include <lume/new.h>

/* ================================================================== */
/*  poll_wait — called by file_operations->poll() implementations     */
/* ================================================================== */

void poll_wait(file* /*fp*/, WaitQueue* wq, poll_table* pt)
{
    /* Probe-only pass: pt is null, drivers skip registration. */
    if (!pt || !wq) return;
    /* Single-wq design: keep the first one we see.  A more complete
     * implementation would chain entries onto a list. */
    if (!pt->wq) pt->wq = wq;
}

/* ================================================================== */
/*  Linux-compatible timespec                                         */
/* ================================================================== */

struct linux_timespec_pp {
    int64 tv_sec;
    int64 tv_nsec;
};

/*
 * Convert a timespec into a deadline in jiffies (g_jiffies clock).
 * Returns 0 if the input is null (= infinite wait), or a deadline
 * jiffy value otherwise.  Caller treats 0 deadline as "no timeout".
 */
static uint64 timespec_to_deadline(const linux_timespec_pp* ts)
{
    if (!ts) return 0;
    /* g_jiffies advances at 100 Hz (HZ=100) — 1 jiffy = 10 ms */
    uint64 ticks = static_cast<uint64>(ts->tv_sec) * 100ULL;
    ticks += ts->tv_nsec / 10'000'000;          /* 10 ms granularity */
    /* Round up sub-jiffy nsec so a tiny timeout doesn't truncate to 0. */
    if (ts->tv_nsec % 10'000'000 != 0) ticks++;
    return g_jiffies + ticks;
}

/* ================================================================== */
/*  do_poll — one scan over all fds                                   */
/* ================================================================== */

/*
 * For each fd: look up the file, call its poll() callback.  If `pt` is
 * non-null, drivers register their wait queues onto it.  Returns the
 * number of fds that have any non-zero revents.
 */
static int do_poll_scan(pollfd* fds, uint32 nfds, poll_table* pt)
{
    TaskControlBlock* c = current_task();
    files_struct* files = c ? c->files : nullptr;
    if (!files) return 0;
    int ready = 0;

    for (uint32 i = 0; i < nfds; i++) {
        if (fds[i].fd < 0) {
            fds[i].revents = 0;
            continue;
        }
        file* fp = fcheck(files, fds[i].fd);
        if (!fp) {
            fds[i].revents = POLLNVAL;
            ready++;
            continue;
        }
        uint32 mask = 0;
        if (fp->f_op && fp->f_op->poll) {
            mask = fp->f_op->poll(fp, pt);
        } else {
            /* No poll handler — treat as always readable/writable so
             * a missing implementation doesn't permanently block sh. */
            mask = POLLIN | POLLOUT;
        }
        /* Filter: revents always include error/hup; userland event bits
         * only set if requested in events. */
        uint32 always = POLLERR | POLLHUP | POLLNVAL;
        uint32 want = static_cast<uint32>(fds[i].events) | always;
        fds[i].revents = static_cast<int16>(mask & want);
        if (fds[i].revents) ready++;
    }
    return ready;
}

/* ================================================================== */
/*  sys_ppoll                                                         */
/* ================================================================== */

/*
 * a0 = struct pollfd __user *ufds
 * a1 = nfds_t nfds
 * a2 = const struct __kernel_timespec __user *tsp  (nullable = infinite)
 * a3 = const sigset_t __user *sigmask              (nullable = no override)
 * a4 = size_t sigsetsize
 */
int64 sys_ppoll(uint64 a0, uint64 a1, uint64 a2, uint64 a3,
                uint64 a4, uint64 /*a5*/)
{
    uint64 ufds   = a0;
    uint32 nfds   = static_cast<uint32>(a1);
    uint64 utsp   = a2;
    uint64 usigmask = a3;
    uint64 sigsetsize = a4;

    if (nfds > 1024) return -EINVAL;        /* Linux RLIMIT_NOFILE-ish cap */
    if (usigmask && sigsetsize != sizeof(sigset_t)) return -EINVAL;

    /* Copy timespec from user.  Null pointer means "wait forever". */
    bool have_timeout = (utsp != 0);
    linux_timespec_pp ts{};
    if (have_timeout) {
        if (copy_from_user(&ts, utsp, sizeof(ts)) < 0) return -EFAULT;
        if (ts.tv_sec < 0 || ts.tv_nsec < 0 || ts.tv_nsec >= 1'000'000'000)
            return -EINVAL;
    }
    uint64 deadline = have_timeout ? timespec_to_deadline(&ts) : 0;

    /* Apply temporary sigmask if requested.  Linux semantics: the new
     * mask is in effect for the duration of the call; on return we
     * restore the old mask.  If a signal interrupts us mid-wait, the
     * original mask is restored before returning -EINTR. */
    TaskControlBlock* curr = current_task();
    sigset_t saved_mask{};
    bool mask_swapped = false;
    if (usigmask && curr && curr->sighand) {
        sigset_t newmask{};
        if (copy_from_user(&newmask, usigmask, sizeof(newmask)) < 0)
            return -EFAULT;
        newmask.del(SIGKILL);
        newmask.del(SIGSTOP);
        LockGuard guard(curr->sighand->siglock);
        saved_mask = curr->blocked;
        curr->blocked = newmask;
        mask_swapped = true;
    }

    /* Copy pollfd array from user.  Allocate a kernel buffer; for the
     * common nfds<=8 case this is one slab object. */
    pollfd* kfds = nullptr;
    uint64 fds_bytes = static_cast<uint64>(nfds) * sizeof(pollfd);
    int64 ret = 0;

    if (nfds > 0) {
        kfds = static_cast<pollfd*>(operator new[](fds_bytes));
        if (!kfds) { ret = -ENOMEM; goto out_restore_mask; }
        if (copy_from_user(kfds, ufds, fds_bytes) < 0) {
            ret = -EFAULT; goto out_free;
        }
    }

    /* The main poll loop. */
    {
        poll_table pt{};
        pt.wq = nullptr;
        int n_ready = do_poll_scan(kfds, nfds, &pt);

        while (n_ready == 0) {
            /* Timeout==0 means non-blocking probe. */
            if (have_timeout && deadline <= g_jiffies) break;

            /* No registered wait queue?  Nothing to sleep on.  This
             * happens when no fd has a poll handler that calls
             * poll_wait — fall back to a short reschedule loop so we
             * don't burn the CPU. */
            if (!pt.wq) {
                schedule();
                if (signal_pending(curr)) { ret = -EINTR; goto out_free; }
                pt.wq = nullptr;
                n_ready = do_poll_scan(kfds, nfds, &pt);
                continue;
            }

            /* Standard prepare-check-schedule pattern. */
            pt.wq->prepare_to_wait(curr, TaskState::Sleeping);

            /* Re-probe with pt=nullptr (no extra registration) so we
             * see any state change that arrived between the previous
             * scan and prepare_to_wait. */
            n_ready = do_poll_scan(kfds, nfds, nullptr);
            if (n_ready > 0) {
                pt.wq->finish_wait(curr);
                break;
            }
            if (signal_pending(curr)) {
                pt.wq->finish_wait(curr);
                ret = -EINTR; goto out_free;
            }

            schedule();
            pt.wq->finish_wait(curr);

            /* Reset the registration and rescan — drivers may register
             * fresh wait queues this pass. */
            pt.wq = nullptr;
            n_ready = do_poll_scan(kfds, nfds, &pt);
        }
        ret = n_ready;
    }

    /* Copy revents back to user (events bits stay as user wrote them). */
    if (nfds > 0) {
        if (copy_to_user(ufds, kfds, fds_bytes) < 0) ret = -EFAULT;
    }

out_free:
    if (kfds) operator delete[](kfds);

out_restore_mask:
    if (mask_swapped && curr && curr->sighand) {
        LockGuard guard(curr->sighand->siglock);
        curr->blocked = saved_mask;
    }
    return ret;
}