diff --git a/.gitignore b/.gitignore index 9c4e613030..e992815de5 100644 --- a/.gitignore +++ b/.gitignore @@ -62,4 +62,4 @@ third_party/ os/StarryOS-REF .Ds_Store starry-*.log -loongarch64-axvisor-guest-tests.md +loongarch64-axvisor-guest-tests.md \ No newline at end of file diff --git a/apps/starry/mysql/README.md b/apps/starry/mysql/README.md new file mode 100644 index 0000000000..1754f65eda --- /dev/null +++ b/apps/starry/mysql/README.md @@ -0,0 +1,84 @@ +# Starry MySQL App + +This app prepares an x86_64 Debian rootfs with Oracle MySQL 8.4.6 generic glibc binaries, then runs a StarryOS guest-side SQL workload. + +```bash +cargo xtask starry app qemu -t mysql --arch x86_64 +``` + +Only x86_64 Debian/glibc rootfs images are supported. The Oracle generic package is not suitable for aarch64 or Alpine/musl rootfs images. + +## Rootfs Preparation + +`prebuild.sh` runs on the host/container before QEMU starts: + +1. Prepares `tmp/axbuild/rootfs/rootfs-x86_64-debian.img.tar.xz` with `wget --no-check-certificate`. +2. Extracts the Debian rootfs archive into a dedicated MySQL rootfs image. +3. Expands the dedicated image to `5G`. +4. Downloads MySQL 8.4.6 with `wget --no-check-certificate`, unless `MYSQL_TARBALL` or `mysql.tar.xz` is already available. +5. Installs MySQL into `/opt/mysql`. +6. Unpacks runtime dependencies: `libaio`, `libnuma`, and `libncurses`. +7. Writes `/root/mysql-env.sh` with `PATH` and `LD_LIBRARY_PATH`. +8. Adds `/usr/bin/mysql-test.sh` through the app overlay. + +The QEMU config uses the generated rootfs: + +```text +tmp/axbuild/rootfs/rootfs-x86_64-mysql.img +``` + +## Interactive Mode + +To enter the MySQL client and run SQL manually, use the interactive QEMU config: + +```bash +cargo xtask starry app qemu -t mysql --arch x86_64 \ + --qemu-config qemu-x86_64-interactive.toml +``` + +The guest automatically runs: + +```sh +/usr/bin/mysql-interactive.sh +``` + +The script initializes `/opt/mysql/data` if needed, starts `mysqld` in the background, waits until the Unix socket is usable, then enters the MySQL interactive client. Use `exit` to leave the MySQL client and `Ctrl-a x` to exit QEMU. + +## Guest Test Flow + +`mysql-test.sh` runs automatically inside StarryOS: + +1. Initializes `/opt/mysql/data` in the background. +2. Sleeps 30 seconds, then checks `/tmp/mysql-init.log` until `Bootstrapping complete` appears. +3. Stops the initialization process with plain `kill`, then sleeps 3 seconds. +4. Starts `mysqld` in the background with socket `/tmp/mysql.sock`. +5. Sleeps 30 seconds, then waits for `/tmp/mysql.sock` and `/opt/mysql/data/mysqld.pid`. +6. Runs 15 SQL stages with colored `MYSQL_STAGE_PASSED` output. +7. Restarts `mysqld` with a non-graceful exit before the final persistence stage to avoid the known shutdown hang path. + +The test intentionally avoids `mysqladmin shutdown`, which currently can hang the guest during MySQL graceful shutdown. + +## Coverage + +The 15 SQL stages cover: + +- version and server metadata +- database and schema creation +- InnoDB tables with constraints +- multi-row inserts and ordered queries +- updates +- secondary indexes and `EXPLAIN` +- joins +- aggregations +- transactions with `COMMIT` and `ROLLBACK` +- temporary tables +- views and information_schema queries +- restart persistence checks + +## Configuration + +- Guest memory: `2G` +- StarryOS physical memory: `0x8000_0000` +- Rootfs target size: `5G` +- MySQL package cache: `target/mysql` +- Success marker: `MYSQL_TEST_PASSED` diff --git a/apps/starry/mysql/README_CN.md b/apps/starry/mysql/README_CN.md new file mode 100644 index 0000000000..b297538694 --- /dev/null +++ b/apps/starry/mysql/README_CN.md @@ -0,0 +1,84 @@ +# Starry MySQL 应用 + +这个应用会先准备一个带 Oracle MySQL 8.4.6 generic glibc 二进制包的 x86_64 Debian rootfs,然后在 StarryOS guest 中运行 SQL 测试。 + +```bash +cargo xtask starry app qemu -t mysql --arch x86_64 +``` + +当前只支持 x86_64 Debian/glibc rootfs。Oracle generic 包不适合 aarch64,也不适合 Alpine/musl rootfs。 + +## Rootfs 准备 + +`prebuild.sh` 在宿主机或容器中、QEMU 启动前执行: + +1. 使用 `wget --no-check-certificate` 准备 `tmp/axbuild/rootfs/rootfs-x86_64-debian.img.tar.xz`。 +2. 从 Debian rootfs 压缩包解出 MySQL 专用 rootfs。 +3. 将专用镜像扩容到 `5G`。 +4. 使用 `wget --no-check-certificate` 下载 MySQL 8.4.6;如果已有 `MYSQL_TARBALL` 或 `mysql.tar.xz`,则直接复用。 +5. 将 MySQL 安装到 `/opt/mysql`。 +6. 解包运行时依赖:`libaio`、`libnuma`、`libncurses`。 +7. 写入 `/root/mysql-env.sh`,配置 `PATH` 和 `LD_LIBRARY_PATH`。 +8. 通过 app overlay 加入 `/usr/bin/mysql-test.sh`。 + +QEMU 使用生成后的 rootfs: + +```text +tmp/axbuild/rootfs/rootfs-x86_64-mysql.img +``` + +## 交互模式 + +如果需要进入 MySQL 客户端手动执行 SQL,可以使用交互配置: + +```bash +cargo xtask starry app qemu -t mysql --arch x86_64 \ + --qemu-config qemu-x86_64-interactive.toml +``` + +进入 guest 后会自动运行: + +```sh +/usr/bin/mysql-interactive.sh +``` + +这个脚本会在 `/opt/mysql/data` 尚未初始化时执行初始化,随后后台启动 `mysqld`,等待 socket 可连接,最后进入 MySQL 交互客户端。退出 MySQL 客户端使用 `exit`,退出 QEMU 使用 `Ctrl-a x`。 + +## Guest 测试流程 + +`mysql-test.sh` 会在 StarryOS 内自动运行: + +1. 后台初始化 `/opt/mysql/data`。 +2. 先睡眠 30 秒,然后检查 `/tmp/mysql-init.log`,直到出现 `Bootstrapping complete`。 +3. 使用普通 `kill` 停掉初始化进程,然后睡眠 3 秒。 +4. 后台启动 `mysqld`,socket 使用 `/tmp/mysql.sock`。 +5. 先睡眠 30 秒,然后等待 `/tmp/mysql.sock` 和 `/opt/mysql/data/mysqld.pid` 出现。 +6. 执行 15 个 SQL 阶段,并输出带颜色的 `MYSQL_STAGE_PASSED`。 +7. 最后一组持久化测试前使用非优雅退出重启 `mysqld`,避免触发当前已知的 shutdown 卡死路径。 + +测试刻意避开 `mysqladmin shutdown`,因为当前 MySQL 优雅关闭路径可能导致 guest 卡死。 + +## 覆盖范围 + +15 个 SQL 阶段覆盖: + +- 版本与服务器元数据 +- 数据库和 schema 创建 +- 带约束的 InnoDB 表 +- 多行插入和排序查询 +- 更新操作 +- 二级索引和 `EXPLAIN` +- 表连接 +- 聚合查询 +- `COMMIT` 和 `ROLLBACK` 事务 +- 临时表 +- 视图和 information_schema 查询 +- 重启后的持久化检查 + +## 配置 + +- Guest 内存:`2G` +- StarryOS 物理内存:`0x8000_0000` +- Rootfs 目标大小:`5G` +- MySQL 包缓存:`target/mysql` +- 成功标记:`MYSQL_TEST_PASSED` diff --git a/apps/starry/mysql/build-x86_64-unknown-none.toml b/apps/starry/mysql/build-x86_64-unknown-none.toml new file mode 100644 index 0000000000..0644f49c45 --- /dev/null +++ b/apps/starry/mysql/build-x86_64-unknown-none.toml @@ -0,0 +1,15 @@ +target = "x86_64-unknown-none" +env = { AX_IP = "10.0.2.15", AX_GW = "10.0.2.2" } +axconfig_overrides = ["plat.phys-memory-size=0x8000_0000"] +log = "Warn" +features = [ + "ax-hal/x86-pc", + "qemu", + "ax-driver/plat-static", + "ax-driver/virtio-blk", + "ax-driver/virtio-net", + "ax-driver/virtio-gpu", + "ax-driver/virtio-input", + "ax-driver/virtio-socket", +] +plat_dyn = false diff --git a/apps/starry/mysql/mysql-interactive.sh b/apps/starry/mysql/mysql-interactive.sh new file mode 100644 index 0000000000..6ec5d69d9b --- /dev/null +++ b/apps/starry/mysql/mysql-interactive.sh @@ -0,0 +1,102 @@ +#!/bin/sh +set -e + +. /root/mysql-env.sh + +mysql_exec() { + /opt/mysql/bin/mysql --no-defaults -uroot --socket=/tmp/mysql.sock "$@" +} + +wait_for_init() { + i=0 + sleep 30 + while [ "$i" -lt 60 ]; do + tail -n 80 /tmp/mysql-init.log || true + if grep -q "Bootstrapping complete" /tmp/mysql-init.log; then + return + fi + if ! kill -0 "$init_pid" 2>/dev/null; then + echo "mysql initialize exited before Bootstrapping complete" >&2 + exit 1 + fi + i=$((i + 1)) + sleep 10 + done + echo "mysql initialize timed out" >&2 + exit 1 +} + +initialize_if_needed() { + if [ -d /opt/mysql/data/mysql ]; then + return + fi + + echo "MYSQL_INTERACTIVE_PREP initialize" + rm -rf /opt/mysql/data + mkdir -p /opt/mysql/data /tmp /run/mysqld + /opt/mysql/bin/mysqld \ + --initialize-insecure \ + --user=root \ + --basedir=/opt/mysql \ + --datadir=/opt/mysql/data \ + --console \ + --log-error-verbosity=3 \ + > /tmp/mysql-init.log 2>&1 & + init_pid=$! + + wait_for_init + kill "$init_pid" 2>/dev/null || true + sleep 3 +} + +wait_for_server() { + i=0 + sleep 30 + while [ "$i" -lt 180 ]; do + if [ -S /tmp/mysql.sock ] && [ -f /opt/mysql/data/mysqld.pid ] \ + && mysql_exec -e "SELECT VERSION() AS mysql_version;" >/tmp/mysql-interactive-ready.out 2>&1; then + ls -l /tmp/mysql.sock /opt/mysql/data/mysqld.pid 2>/dev/null || true + cat /tmp/mysql-interactive-ready.out || true + return + fi + if ! kill -0 "$server_pid" 2>/dev/null; then + echo "mysqld exited before readiness" >&2 + tail -n 120 /tmp/mysqld.log >&2 || true + exit 1 + fi + i=$((i + 1)) + sleep 10 + done + echo "mysqld readiness timed out" >&2 + tail -n 120 /tmp/mysqld.log >&2 || true + exit 1 +} + +start_server_if_needed() { + if mysql_exec -e "SELECT VERSION() AS mysql_version;" >/tmp/mysql-interactive-ready.out 2>&1; then + cat /tmp/mysql-interactive-ready.out || true + return + fi + + echo "MYSQL_INTERACTIVE_PREP start" + rm -f /tmp/mysql.sock /opt/mysql/data/mysqld.pid /tmp/mysqld.log + /opt/mysql/bin/mysqld \ + --no-defaults \ + --user=root \ + --basedir=/opt/mysql \ + --datadir=/opt/mysql/data \ + --socket=/tmp/mysql.sock \ + --pid-file=/opt/mysql/data/mysqld.pid \ + --console \ + --log-error-verbosity=3 \ + > /tmp/mysqld.log 2>&1 & + server_pid=$! + + wait_for_server +} + +initialize_if_needed +start_server_if_needed + +echo "MYSQL_INTERACTIVE_READY" +exec /opt/mysql/bin/mysql --no-defaults -uroot --socket=/tmp/mysql.sock diff --git a/apps/starry/mysql/mysql-test.sh b/apps/starry/mysql/mysql-test.sh new file mode 100755 index 0000000000..5186e8b2f6 --- /dev/null +++ b/apps/starry/mysql/mysql-test.sh @@ -0,0 +1,448 @@ +#!/bin/sh +set -e + +fail() { + if [ -n "$current_stage" ]; then + printf "%sMYSQL_STAGE_FAILED %s/%s %s%s\n" "$red" "$stage_no" "$total_stages" "$current_stage" "$reset" + stage_output="/tmp/mysql-${current_stage}.out" + if [ -f "$stage_output" ]; then + echo "MYSQL_STAGE_OUTPUT $current_stage" >&2 + cat "$stage_output" >&2 || true + fi + fi + echo "MYSQL_TEST_FAILED: $*" >&2 + echo "MYSQL_TEST_FAILED" + [ -f /tmp/mysql-init.log ] && tail -n 80 /tmp/mysql-init.log >&2 || true + [ -f /tmp/mysqld.log ] && tail -n 120 /tmp/mysqld.log >&2 || true + exit 1 +} + +passed=0 +server_pid="" +total_stages=15 +stage_no=0 +current_stage="" +green="$(printf '\033[32m')" +red="$(printf '\033[31m')" +bold="$(printf '\033[1m')" +reset="$(printf '\033[0m')" + +cleanup() { + if [ "$passed" -ne 1 ] && [ -n "$server_pid" ] && kill -0 "$server_pid" 2>/dev/null; then + kill -9 "$server_pid" 2>/dev/null || true + fi +} +trap cleanup EXIT + +. /root/mysql-env.sh + +mysql_exec() { + /opt/mysql/bin/mysql --no-defaults -uroot --socket=/tmp/mysql.sock "$@" +} + +capture_stream() { + label="$1" + shift + + output="/tmp/mysql-${label}.out" + rm -f "$output" + "$@" >"$output" 2>&1 +} + +prep_step() { + printf "%sMYSQL_PREP %s%s\n" "$bold" "$1" "$reset" +} + +wait_mysql_ready() { + local i min_ready_count + + min_ready_count="$1" + sleep 30 + i=0 + while [ "$i" -lt 180 ]; do + if mysql_ready_probe "$min_ready_count"; then + return + fi + + if ! kill -0 "$server_pid" 2>/dev/null; then + fail "mysqld exited before readiness" + fi + + i=$((i + 1)) + sleep 10 + done + + fail "mysqld did not become ready" +} + +ready_log_count() { + if [ -f /tmp/mysqld.log ]; then + grep -c "ready for connections" /tmp/mysqld.log 2>/dev/null || true + else + echo 0 + fi +} + +mysql_ready_probe() { + local ready_count min_ready_count + + min_ready_count="$1" + ready_count="$(ready_log_count)" + [ "$ready_count" -gt "$min_ready_count" ] || return 1 + + [ -S /tmp/mysql.sock ] || return 1 + [ -f /opt/mysql/data/mysqld.pid ] || return 1 + capture_stream ready mysql_exec -e "SELECT VERSION() AS mysql_version;" || return 1 + + ls -l /tmp/mysql.sock /opt/mysql/data/mysqld.pid 2>/dev/null || return 1 + cat /tmp/mysql-ready.out || true +} + +start_mysqld() { + rm -f /tmp/mysql.sock /opt/mysql/data/mysqld.pid /tmp/mysqld.log + + /opt/mysql/bin/mysqld \ + --no-defaults \ + --user=root \ + --basedir=/opt/mysql \ + --datadir=/opt/mysql/data \ + --socket=/tmp/mysql.sock \ + --pid-file=/opt/mysql/data/mysqld.pid \ + --console \ + --log-error-verbosity=3 \ + > /tmp/mysqld.log 2>&1 & + server_pid=$! + + wait_mysql_ready 0 +} + +restart_mysqld() { + local old_pid + old_pid="$server_pid" + + prep_step restart + + if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then + kill -9 "$old_pid" 2>/dev/null || true + wait "$old_pid" >/dev/null 2>&1 || true + fi + + rm -f /tmp/mysql.sock /opt/mysql/data/mysqld.pid + start_mysqld +} + +run_stage() { + local name="$1" + shift + stage_no=$((stage_no + 1)) + current_stage="$name" + echo "MYSQL_STAGE_NO $stage_no/$total_stages" + echo "MYSQL_STAGE $current_stage" + capture_stream "$name" mysql_exec "$@" || fail "stage failed: $name" + printf "%sMYSQL_STAGE_PASSED %s/%s %s%s\n" "$green" "$stage_no" "$total_stages" "$current_stage" "$reset" +} + +[ -x /opt/mysql/bin/mysqld ] || fail "missing /opt/mysql/bin/mysqld" +[ -x /opt/mysql/bin/mysql ] || fail "missing /opt/mysql/bin/mysql" + +rm -rf /opt/mysql/data +mkdir -p /opt/mysql/data /tmp /run/mysqld + +prep_step initialize +/opt/mysql/bin/mysqld \ + --initialize-insecure \ + --user=root \ + --basedir=/opt/mysql \ + --datadir=/opt/mysql/data \ + --console \ + --log-error-verbosity=3 \ + > /tmp/mysql-init.log 2>&1 & +init_pid=$! + +sleep 30 +init_ready=0 +i=0 +while [ "$i" -lt 60 ]; do + tail -n 120 /tmp/mysql-init.log || true + if grep -q "Bootstrapping complete" /tmp/mysql-init.log; then + init_ready=1 + break + fi + if ! kill -0 "$init_pid" 2>/dev/null; then + fail "initialize process exited before bootstrapping completed" + fi + i=$((i + 1)) + sleep 10 +done + +[ "$init_ready" -eq 1 ] || fail "initialize did not reach Bootstrapping complete" + +kill "$init_pid" 2>/dev/null || true +sleep 3 + +prep_step start +start_mysqld + +run_stage "01-create-database" <<'SQL' +DROP DATABASE IF EXISTS starry_mysql_test; +CREATE DATABASE IF NOT EXISTS starry_mysql_test + CHARACTER SET utf8mb4 + COLLATE utf8mb4_0900_ai_ci; +SHOW DATABASES LIKE 'starry_mysql_test'; +SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME + FROM information_schema.SCHEMATA + WHERE SCHEMA_NAME = 'starry_mysql_test'; +SQL + +run_stage "02-create-users-table" <<'SQL' +USE starry_mysql_test; +DROP TABLE IF EXISTS audit_log; +DROP TABLE IF EXISTS payments; +DROP TABLE IF EXISTS orders; +DROP TABLE IF EXISTS users; +CREATE TABLE users ( + id INT PRIMARY KEY, + name VARCHAR(64) NOT NULL, + email VARCHAR(128) NOT NULL UNIQUE, + age INT NOT NULL, + city VARCHAR(64) NOT NULL, + status ENUM('active', 'inactive', 'blocked') NOT NULL DEFAULT 'active', + score DECIMAL(8,2) NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NULL DEFAULT NULL, + CHECK (age >= 0) +) ENGINE=InnoDB; +SHOW CREATE TABLE users; +SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = 'starry_mysql_test' AND TABLE_NAME = 'users' + ORDER BY ORDINAL_POSITION; +SQL + +run_stage "03-insert-users" <<'SQL' +USE starry_mysql_test; +INSERT INTO users(id, name, email, age, city, status, score) VALUES + (1, 'alice', 'alice@example.test', 20, 'beijing', 'active', 88.50), + (2, 'bob', 'bob@example.test', 21, 'shanghai', 'active', 91.25), + (3, 'carol', 'carol@example.test', 22, 'guangzhou', 'inactive', 79.00), + (4, 'dave', 'dave@example.test', 23, 'shenzhen', 'active', 95.75), + (5, 'eve', 'eve@example.test', 24, 'hangzhou', 'blocked', 66.00), + (6, 'frank', 'frank@example.test', 25, 'chengdu', 'active', 83.40), + (7, 'grace', 'grace@example.test', 26, 'beijing', 'active', 98.10); +SELECT COUNT(*) AS users_count FROM users; +SELECT status, COUNT(*) AS cnt, MIN(age) AS min_age, MAX(score) AS max_score + FROM users + GROUP BY status + ORDER BY status; +SQL + +run_stage "04-query-filter-order" <<'SQL' +USE starry_mysql_test; +SELECT id, name, city, age, score + FROM users + WHERE age BETWEEN 21 AND 26 AND status IN ('active', 'inactive') + ORDER BY score DESC, id ASC + LIMIT 5; +SELECT city, GROUP_CONCAT(name ORDER BY name SEPARATOR ',') AS names + FROM users + GROUP BY city + HAVING COUNT(*) >= 1 + ORDER BY city; +SQL + +run_stage "05-update" <<'SQL' +USE starry_mysql_test; +UPDATE users + SET age = age + 1, + score = score + 2.50, + updated_at = CURRENT_TIMESTAMP + WHERE name = 'alice'; +UPDATE users + SET status = 'inactive' + WHERE score < 70; +SELECT id, name, age, status, score, updated_at FROM users WHERE name IN ('alice', 'eve'); +SELECT COUNT(*) AS inactive_count FROM users WHERE status = 'inactive'; +SQL + +run_stage "06-index" <<'SQL' +USE starry_mysql_test; +CREATE INDEX idx_users_age_city ON users(age, city); +CREATE INDEX idx_users_status_score ON users(status, score); +SHOW INDEX FROM users; +EXPLAIN SELECT * FROM users WHERE age >= 22 AND city = 'beijing'; +EXPLAIN SELECT * FROM users WHERE status = 'active' ORDER BY score DESC; +SQL + +run_stage "07-create-orders" <<'SQL' +USE starry_mysql_test; +DROP TABLE IF EXISTS orders; +CREATE TABLE orders ( + id INT PRIMARY KEY, + user_id INT NOT NULL, + product VARCHAR(64) NOT NULL, + amount DECIMAL(10,2) NOT NULL, + status VARCHAR(16) NOT NULL, + created_at DATETIME NOT NULL, + note VARCHAR(128), + INDEX idx_orders_user_status(user_id, status), + INDEX idx_orders_created(created_at), + CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id) +) ENGINE=InnoDB; +SHOW CREATE TABLE orders; +SELECT TABLE_NAME, ENGINE, TABLE_ROWS + FROM information_schema.TABLES + WHERE TABLE_SCHEMA = 'starry_mysql_test' AND TABLE_NAME = 'orders'; +SQL + +run_stage "08-insert-orders-join" <<'SQL' +USE starry_mysql_test; +INSERT INTO orders(id, user_id, product, amount, status, created_at, note) VALUES + (1, 1, 'book', 99.50, 'paid', '2026-06-01 10:00:00', 'first order'), + (2, 2, 'keyboard', 188.00, 'paid', '2026-06-01 10:05:00', 'mechanical'), + (3, 3, 'mouse', 42.25, 'pending', '2026-06-01 10:10:00', NULL), + (4, 4, 'monitor', 899.99, 'paid', '2026-06-02 09:30:00', 'large item'), + (5, 1, 'usb-c cable', 19.90, 'paid', '2026-06-02 09:45:00', 'accessory'), + (6, 6, 'ssd', 499.00, 'pending', '2026-06-03 11:20:00', 'storage'), + (7, 7, 'router', 299.00, 'paid', '2026-06-03 12:00:00', 'network'); +SELECT u.name, u.city, o.product, o.amount, o.status + FROM users u JOIN orders o ON u.id = o.user_id + WHERE o.amount >= 40 + ORDER BY o.amount DESC, o.id ASC; +SELECT u.name, COALESCE(SUM(o.amount), 0) AS total_amount + FROM users u LEFT JOIN orders o ON u.id = o.user_id + GROUP BY u.id, u.name + ORDER BY total_amount DESC, u.id ASC; +SQL + +run_stage "09-aggregation" <<'SQL' +USE starry_mysql_test; +SELECT status, COUNT(*) AS cnt, SUM(amount) AS total_amount, AVG(amount) AS avg_amount + FROM orders + GROUP BY status + ORDER BY total_amount DESC; +SELECT DATE(created_at) AS order_day, COUNT(*) AS cnt, SUM(amount) AS day_total + FROM orders + GROUP BY DATE(created_at) + ORDER BY order_day; +SELECT city, COUNT(*) AS user_count, ROUND(AVG(score), 2) AS avg_score + FROM users + GROUP BY city + ORDER BY avg_score DESC; +SQL + +run_stage "10-transaction-commit" <<'SQL' +USE starry_mysql_test; +START TRANSACTION; +INSERT INTO users(id, name, email, age, city, status, score) + VALUES (8, 'heidi', 'heidi@example.test', 27, 'nanjing', 'active', 87.70); +INSERT INTO orders(id, user_id, product, amount, status, created_at, note) + VALUES (8, 8, 'dock', 268.80, 'paid', '2026-06-04 08:00:00', 'committed order'); +INSERT INTO orders(id, user_id, product, amount, status, created_at, note) + VALUES (9, 8, 'adapter', 38.60, 'paid', '2026-06-04 08:05:00', 'second committed order'); +COMMIT; +SELECT u.id, u.name, COUNT(o.id) AS order_count, SUM(o.amount) AS total_amount + FROM users u JOIN orders o ON u.id = o.user_id + WHERE u.id = 8 + GROUP BY u.id, u.name; +SQL + +run_stage "11-transaction-rollback-delete" <<'SQL' +USE starry_mysql_test; +START TRANSACTION; +INSERT INTO users(id, name, email, age, city, status, score) + VALUES (9, 'rollback_user', 'rollback@example.test', 99, 'nowhere', 'active', 1.00); +DELETE FROM orders WHERE status = 'pending'; +UPDATE users SET score = 0 WHERE status = 'blocked'; +ROLLBACK; +SELECT COUNT(*) AS rollback_user_count FROM users WHERE id = 9; +SELECT COUNT(*) AS pending_orders_after_rollback FROM orders WHERE status = 'pending'; +SELECT id, name, score FROM users WHERE status = 'blocked'; +SQL + +run_stage "12-temporary-table" <<'SQL' +USE starry_mysql_test; +CREATE TEMPORARY TABLE tmp_order_summary AS + SELECT user_id, COUNT(*) AS order_count, SUM(amount) AS total_amount + FROM orders + GROUP BY user_id; +SELECT s.user_id, u.name, s.order_count, s.total_amount + FROM tmp_order_summary s JOIN users u ON s.user_id = u.id + ORDER BY s.total_amount DESC; +CREATE TEMPORARY TABLE tmp_numbers (n INT NOT NULL, label VARCHAR(16)); +INSERT INTO tmp_numbers VALUES (1, 'one'), (2, 'two'), (3, 'three'), (4, 'four'), (5, 'five'); +SELECT SUM(n) AS total, GROUP_CONCAT(label ORDER BY n SEPARATOR '|') AS labels FROM tmp_numbers; +SQL + +run_stage "13-view-and-schema" <<'SQL' +USE starry_mysql_test; +CREATE OR REPLACE VIEW paid_orders_view AS + SELECT u.name, u.city, o.product, o.amount, o.created_at + FROM users u JOIN orders o ON u.id = o.user_id + WHERE o.status = 'paid'; +SELECT COUNT(*) AS users_count FROM users; +SELECT COUNT(*) AS orders_count FROM orders; +SELECT name, product, amount FROM paid_orders_view ORDER BY amount DESC, name; +SELECT COUNT(*) AS paid_view_count FROM paid_orders_view; +SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES + WHERE TABLE_SCHEMA = 'starry_mysql_test' + ORDER BY TABLE_NAME; +SELECT COLUMN_NAME, COLUMN_TYPE + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = 'starry_mysql_test' AND TABLE_NAME = 'orders' + ORDER BY ORDINAL_POSITION; +SQL + +run_stage "14-consistency-report" <<'SQL' +USE starry_mysql_test; +CREATE TABLE IF NOT EXISTS audit_log ( + id INT PRIMARY KEY AUTO_INCREMENT, + category VARCHAR(32) NOT NULL, + detail VARCHAR(128) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX idx_audit_category_created(category, created_at) +) ENGINE=InnoDB; +INSERT INTO audit_log(category, detail) +SELECT 'user_status', CONCAT(status, ':', COUNT(*)) + FROM users + GROUP BY status; +INSERT INTO audit_log(category, detail) +SELECT 'order_status', CONCAT(status, ':', COUNT(*), ':', COALESCE(SUM(amount), 0)) + FROM orders + GROUP BY status; +SELECT category, COUNT(*) AS entries, GROUP_CONCAT(detail ORDER BY detail SEPARATOR ';') AS details + FROM audit_log + GROUP BY category + ORDER BY category; +SELECT u.city, + COUNT(DISTINCT u.id) AS user_count, + COUNT(o.id) AS order_count, + COALESCE(SUM(CASE WHEN o.status = 'paid' THEN o.amount ELSE 0 END), 0) AS paid_amount + FROM users u + LEFT JOIN orders o ON o.user_id = u.id + GROUP BY u.city + ORDER BY paid_amount DESC, u.city; +SELECT s.TABLE_NAME, s.INDEX_NAME, COUNT(*) AS column_count + FROM information_schema.STATISTICS s + WHERE s.TABLE_SCHEMA = 'starry_mysql_test' + GROUP BY s.TABLE_NAME, s.INDEX_NAME + ORDER BY s.TABLE_NAME, s.INDEX_NAME; +SQL + +restart_mysqld + +run_stage "15-restart-persistence" <<'SQL' +USE starry_mysql_test; +SELECT COUNT(*) AS users_after_restart FROM users; +SELECT COUNT(*) AS orders_after_restart FROM orders; +SELECT SUM(amount) AS paid_total_after_restart FROM orders WHERE status = 'paid'; +SELECT name, product, amount FROM paid_orders_view ORDER BY amount DESC, name; +SELECT COUNT(*) AS rollback_user_after_restart FROM users WHERE id = 9; +SELECT COUNT(*) AS pending_orders_after_restart FROM orders WHERE status = 'pending'; +SELECT u.name, COUNT(o.id) AS order_count + FROM users u LEFT JOIN orders o ON u.id = o.user_id + GROUP BY u.id, u.name + ORDER BY u.id; +SQL + +passed=1 +echo "MYSQL_TEST_PASSED" diff --git a/apps/starry/mysql/prebuild.sh b/apps/starry/mysql/prebuild.sh new file mode 100755 index 0000000000..5a17ded68e --- /dev/null +++ b/apps/starry/mysql/prebuild.sh @@ -0,0 +1,349 @@ +#!/usr/bin/env bash +set -eo pipefail + +app_dir="${STARRY_APP_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" +workspace="${STARRY_WORKSPACE:-$(cd "$app_dir/../../.." && pwd)}" +rootfs="${STARRY_ROOTFS:-}" +overlay_dir="${STARRY_OVERLAY_DIR:-}" +arch="${STARRY_ARCH:-}" + +mysql_version="${MYSQL_VERSION:-8.4.6}" +mysql_package_dir="mysql-${mysql_version}-linux-glibc2.28-x86_64" +mysql_tarball_name="${mysql_package_dir}.tar.xz" +mysql_tarball_url="${MYSQL_TARBALL_URL:-https://dev.mysql.com/get/Downloads/MySQL-8.4/$mysql_tarball_name}" +mysql_cache_dir="${MYSQL_CACHE_DIR:-$workspace/target/mysql}" +mysql_rootfs_size="${MYSQL_ROOTFS_SIZE:-5G}" +rootfs_release="${MYSQL_ROOTFS_RELEASE:-v0.0.5}" +base_rootfs_archive_name="rootfs-${arch}-debian.img.tar.xz" +base_rootfs_archive="${MYSQL_BASE_ROOTFS_ARCHIVE:-$workspace/tmp/axbuild/rootfs/$base_rootfs_archive_name}" +base_rootfs_archive_url="${MYSQL_BASE_ROOTFS_ARCHIVE_URL:-https://github.com/rcore-os/tgosimages/releases/download/$rootfs_release/$base_rootfs_archive_name}" +base_rootfs="${MYSQL_BASE_ROOTFS:-}" +apt_lists_ready=0 + +require_env() { + local name="$1" + local value="$2" + if [[ -z "$value" ]]; then + echo "error: $name is required" >&2 + exit 1 + fi +} + +run_e2fsck() { + local status + + set +e + e2fsck -f -y "$1" >/dev/null + status=$? + set -e + + if (( (status & 4) != 0 || status >= 8 )); then + echo "error: e2fsck failed for $1 with status $status" >&2 + exit 1 + fi +} + +ensure_host_tools() { + local missing=() + + for tool in apt-get chmod cp dirname dpkg-deb e2fsck find head id install ln losetup mkdir mktemp mount mountpoint mv numfmt resize2fs rm rmdir sort stat sync tar truncate umount wget; do + command -v "$tool" >/dev/null 2>&1 || missing+=("$tool") + done + + if [[ ${#missing[@]} -ne 0 ]]; then + echo "error: missing required host tools: ${missing[*]}" >&2 + exit 1 + fi + + if [[ "$(id -u)" -ne 0 ]]; then + if ! command -v sudo >/dev/null 2>&1; then + echo "error: mysql prebuild requires root or sudo for loop mount" >&2 + exit 1 + fi + if ! sudo -n true 2>/dev/null; then + echo "error: mysql prebuild requires passwordless sudo for loop mount" >&2 + exit 1 + fi + fi +} + +run_root() { + if [[ "$(id -u)" -eq 0 ]]; then + "$@" + else + sudo "$@" + fi +} + +download_mysql_tarball() { + mkdir -p "$mysql_cache_dir" + + if [[ -n "${MYSQL_TARBALL:-}" ]]; then + if [[ ! -f "$MYSQL_TARBALL" ]]; then + echo "error: MYSQL_TARBALL not found: $MYSQL_TARBALL" >&2 + exit 1 + fi + printf '%s\n' "$MYSQL_TARBALL" + return + fi + + local workspace_tarball="$workspace/mysql.tar.xz" + if [[ -s "$workspace_tarball" ]]; then + printf '%s\n' "$workspace_tarball" + return + fi + + local tarball="$mysql_cache_dir/$mysql_tarball_name" + if [[ -s "$tarball" ]]; then + printf '%s\n' "$tarball" + return + fi + + local tmp="$tarball.tmp" + rm -f "$tmp" + wget --no-check-certificate -O "$tmp" "$mysql_tarball_url" + mv "$tmp" "$tarball" + printf '%s\n' "$tarball" +} + +extract_mysql_tarball() { + local tarball="$1" + local extract_root="$mysql_cache_dir/package" + local package_path="$extract_root/$mysql_package_dir" + + if [[ -x "$package_path/bin/mysqld" && -x "$package_path/bin/mysql" ]]; then + printf '%s\n' "$package_path" + return + fi + + rm -rf "$extract_root" + mkdir -p "$extract_root" + tar -xf "$tarball" -C "$extract_root" + + if [[ ! -x "$package_path/bin/mysqld" || ! -x "$package_path/bin/mysql" ]]; then + echo "error: unexpected MySQL package layout under $extract_root" >&2 + exit 1 + fi + + printf '%s\n' "$package_path" +} + +download_deb() { + local deb_cache="$mysql_cache_dir/debs" + local package + + mkdir -p "$deb_cache" + for package in "$@"; do + local existing=() + while IFS= read -r path; do + existing+=("$path") + done < <(find "$deb_cache" -maxdepth 1 -type f -name "${package}_*.deb" | sort) + + if [[ ${#existing[@]} -ne 0 ]]; then + printf '%s\n' "${existing[0]}" + return + fi + + if (cd "$deb_cache" && apt-get download "$package" >/dev/null); then + local downloaded + downloaded="$(find "$deb_cache" -maxdepth 1 -type f -name "${package}_*.deb" | sort | head -n 1)" + if [[ -n "$downloaded" ]]; then + printf '%s\n' "$downloaded" + return + fi + fi + + if [[ "$apt_lists_ready" -eq 0 ]]; then + run_root apt-get update >/dev/null + apt_lists_ready=1 + fi + + if (cd "$deb_cache" && apt-get download "$package" >/dev/null); then + local downloaded + downloaded="$(find "$deb_cache" -maxdepth 1 -type f -name "${package}_*.deb" | sort | head -n 1)" + if [[ -n "$downloaded" ]]; then + printf '%s\n' "$downloaded" + return + fi + fi + done + + echo "error: failed to download any of: $*" >&2 + exit 1 +} + +prepare_debian_rootfs_archive() { + if [[ -n "${MYSQL_BASE_ROOTFS_ARCHIVE:-}" ]]; then + return + fi + + mkdir -p "$(dirname "$base_rootfs_archive")" + wget --no-check-certificate -c -O "$base_rootfs_archive" "$base_rootfs_archive_url" +} + +prepare_mysql_rootfs_image() { + local extract_dir extracted fallback + + if [[ -n "$base_rootfs" && "$rootfs" == "$base_rootfs" ]]; then + return + fi + + mkdir -p "$(dirname "$rootfs")" + rm -f "$rootfs" + + if [[ -n "$base_rootfs" && -f "$base_rootfs" ]]; then + cp --reflink=auto --sparse=always "$base_rootfs" "$rootfs" 2>/dev/null \ + || cp "$base_rootfs" "$rootfs" + chmod 0644 "$rootfs" + return + fi + + prepare_debian_rootfs_archive + + if [[ -f "$base_rootfs_archive" ]]; then + extract_dir="$(mktemp -d "${TMPDIR:-/tmp}/starry-mysql-base.XXXXXX")" + tar -xf "$base_rootfs_archive" -C "$extract_dir" + + extracted="$extract_dir/rootfs-${arch}-debian.img" + if [[ ! -f "$extracted" ]]; then + extracted="$(find "$extract_dir" -maxdepth 1 -type f -name '*.img' | sort | head -n 1)" + fi + + if [[ -z "$extracted" || ! -f "$extracted" ]]; then + rm -rf "$extract_dir" + echo "error: no rootfs image found in $base_rootfs_archive" >&2 + exit 1 + fi + + mv "$extracted" "$rootfs" + rm -rf "$extract_dir" + chmod 0644 "$rootfs" + return + fi + + fallback="$workspace/tmp/axbuild/rootfs/rootfs-${arch}-debian.img.tar.xz" + echo "error: Debian base rootfs archive not found: $base_rootfs_archive" >&2 + echo "error: expected archive path: $fallback" >&2 + echo "error: download URL: $base_rootfs_archive_url" >&2 + exit 1 +} + +resize_rootfs_if_needed() { + local current_size target_size + current_size="$(stat -c '%s' "$rootfs")" + target_size="$(numfmt --from=iec "$mysql_rootfs_size")" + + if (( current_size >= target_size )); then + return + fi + + truncate -s "$target_size" "$rootfs" + run_e2fsck "$rootfs" + resize2fs "$rootfs" >/dev/null +} + +repair_rootfs_image() { + run_e2fsck "$rootfs" +} + +populate_rootfs() { + local mnt="$1" + local mysql_package="$2" + local libaio_deb="$3" + local libnuma_deb="$4" + local libncurses_deb="$5" + local libdir="$mnt/usr/lib/x86_64-linux-gnu" + + if [[ ! -f "$mnt/etc/debian_version" ]]; then + echo "error: target rootfs is not Debian: $rootfs" >&2 + exit 1 + fi + if [[ ! -e "$mnt/lib64/ld-linux-x86-64.so.2" && ! -e "$mnt/usr/lib64/ld-linux-x86-64.so.2" ]]; then + echo "error: Debian rootfs is missing ld-linux-x86-64.so.2" >&2 + exit 1 + fi + + run_root rm -rf "$mnt/opt/mysql" + run_root mkdir -p "$mnt/opt" + run_root cp -a "$mysql_package" "$mnt/opt/mysql" + + run_root dpkg-deb -x "$libaio_deb" "$mnt" + run_root dpkg-deb -x "$libnuma_deb" "$mnt" + run_root dpkg-deb -x "$libncurses_deb" "$mnt" + + if [[ -e "$libdir/libaio.so.1t64" ]]; then + run_root ln -sf libaio.so.1t64 "$libdir/libaio.so.1" + fi + if [[ -e "$libdir/libnuma.so.1.0.0" ]]; then + run_root ln -sf libnuma.so.1.0.0 "$libdir/libnuma.so.1" + fi + +cat >"$mysql_cache_dir/mysql-env.sh" <<'EOF' +export MYSQL_HOME=/opt/mysql +export PATH=/opt/mysql/bin:$PATH +export LD_LIBRARY_PATH=/opt/mysql/lib:/opt/mysql/lib/private:/usr/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH:-} +EOF + run_root install -m 0644 "$mysql_cache_dir/mysql-env.sh" "$mnt/root/mysql-env.sh" +} + +populate_overlay() { + mkdir -p "$overlay_dir/usr/bin" "$overlay_dir/root/mysql" + install -m 0755 "$app_dir/mysql-test.sh" "$overlay_dir/usr/bin/mysql-test.sh" + install -m 0755 "$app_dir/mysql-interactive.sh" "$overlay_dir/usr/bin/mysql-interactive.sh" + cat >"$overlay_dir/root/mysql/prebuild-info.txt" <&2 + exit 1 +fi + +ensure_host_tools +mysql_tarball="$(download_mysql_tarball)" +mysql_package="$(extract_mysql_tarball "$mysql_tarball")" +libaio_deb="$(download_deb libaio1t64 libaio1)" +libnuma_deb="$(download_deb libnuma1)" +libncurses_deb="$(download_deb libncurses6)" + +prepare_mysql_rootfs_image + +if [[ ! -f "$rootfs" ]]; then + echo "error: rootfs image not found: $rootfs" >&2 + exit 1 +fi + +repair_rootfs_image +resize_rootfs_if_needed + +mnt="$(mktemp -d "${TMPDIR:-/tmp}/starry-mysql-rootfs.XXXXXX")" +loop="$(run_root losetup -f --show "$rootfs")" + +cleanup() { + if mountpoint -q "$mnt"; then + run_root sync + run_root umount "$mnt" + fi + if [[ -n "${loop:-}" ]]; then + run_root losetup -d "$loop" 2>/dev/null || true + fi + rmdir "$mnt" 2>/dev/null || true +} +trap cleanup EXIT + +run_root mount "$loop" "$mnt" +populate_rootfs "$mnt" "$mysql_package" "$libaio_deb" "$libnuma_deb" "$libncurses_deb" +run_root sync +run_root umount "$mnt" +run_root losetup -d "$loop" +loop="" + +populate_overlay diff --git a/apps/starry/mysql/qemu-x86_64-interactive.toml b/apps/starry/mysql/qemu-x86_64-interactive.toml new file mode 100644 index 0000000000..e682169b86 --- /dev/null +++ b/apps/starry/mysql/qemu-x86_64-interactive.toml @@ -0,0 +1,27 @@ +timeout = 0 +shell_prefix = "root@starry:" +uefi = false +to_bin = false +success_regex = [] +fail_regex = [] + +args = [ + "-machine", + "q35", + "-nographic", + "-m", + "2G", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-mysql.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", + "-append", + "root=/dev/sda", + "-snapshot", +] + +shell_init_cmd = "/usr/bin/mysql-interactive.sh" diff --git a/apps/starry/mysql/qemu-x86_64.toml b/apps/starry/mysql/qemu-x86_64.toml new file mode 100644 index 0000000000..4079e3e48c --- /dev/null +++ b/apps/starry/mysql/qemu-x86_64.toml @@ -0,0 +1,30 @@ +args = [ + "-machine", + "q35", + "-nographic", + "-m", + "2G", + "-device", + "virtio-blk-pci,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-mysql.img", + "-device", + "virtio-net-pci,netdev=net0", + "-netdev", + "user,id=net0", + "-append", + "root=/dev/sda", + "-snapshot", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/mysql-test.sh" +success_regex = ["(?m)^MYSQL_TEST_PASSED\\s*$"] +fail_regex = [ + "(?i)\\bpanic(?:ked)?\\b", + "(?i)page fault", + "(?i)segmentation fault", + "(?m)^MYSQL_TEST_FAILED(?:[: ].*)?$", +] +timeout = 2400 diff --git a/os/StarryOS/kernel/src/file/fs.rs b/os/StarryOS/kernel/src/file/fs.rs index 214a1aa8b0..6459923ae3 100644 --- a/os/StarryOS/kernel/src/file/fs.rs +++ b/os/StarryOS/kernel/src/file/fs.rs @@ -14,6 +14,7 @@ use ax_task::future::{block_on, poll_io}; use axfs_ng_vfs::{Location, Metadata, NodeFlags}; use axpoll::{IoEvents, Pollable}; use linux_raw_sys::general::{AT_EMPTY_PATH, AT_FDCWD, AT_SYMLINK_NOFOLLOW, O_APPEND, O_EXCL}; +use starry_vm::VmPtr; use super::{FileLike, Kstat, get_file_like}; use crate::{ @@ -21,6 +22,9 @@ use crate::{ pseudofs::Device, }; +// FusionIO/directFS atomic-write toggle used by MySQL. +const DFS_IOCTL_ATOMIC_WRITE_SET: u32 = 0x4004_9502; + pub fn with_fs(dirfd: c_int, f: impl FnOnce(&mut FsContext) -> AxResult) -> AxResult { let mut fs = FS_CONTEXT.lock(); if dirfd == AT_FDCWD { @@ -196,7 +200,14 @@ impl FileLike for File { } fn ioctl(&self, cmd: u32, arg: usize) -> AxResult { - self.inner().backend()?.location().ioctl(cmd, arg) + let loc = self.inner().backend()?.location(); + match cmd { + DFS_IOCTL_ATOMIC_WRITE_SET => { + let _enabled: u32 = (arg as *const u32).vm_read()?; + Ok(0) + } + _ => loc.ioctl(cmd, arg), + } } fn file_mmap(&self) -> AxResult<(FileBackend, FileFlags)> { diff --git a/os/StarryOS/kernel/src/syscall/fs/aio.rs b/os/StarryOS/kernel/src/syscall/fs/aio.rs index 7ab6149db4..d6a2c66946 100644 --- a/os/StarryOS/kernel/src/syscall/fs/aio.rs +++ b/os/StarryOS/kernel/src/syscall/fs/aio.rs @@ -1,19 +1,65 @@ -use alloc::collections::{BTreeMap, VecDeque}; +use alloc::{ + collections::{BTreeMap, VecDeque}, + string::String, + sync::Arc, + vec, + vec::Vec, +}; use core::{ ffi::c_int, - sync::atomic::{AtomicUsize, Ordering}, + future::poll_fn, + mem::{MaybeUninit, offset_of, size_of}, + slice, + sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}, }; use ax_errno::{AxError, AxResult, LinuxError}; -use linux_raw_sys::general::{__kernel_off_t, timespec}; +use ax_fs::FileFlags; +use ax_memory_addr::{MemoryAddr, VirtAddr, VirtAddrRange, align_up_4k}; +use ax_runtime::hal::{ + paging::{MappingFlags, PageSize}, + time::wall_time, +}; +use ax_sync::Mutex; +use ax_task::{ + WaitQueue, + future::{block_on, interruptible, timeout_at}, +}; +use axpoll::{IoEvents, PollSet}; +use linux_raw_sys::general::timespec; use spin::RwLock; use starry_process::Pid; +use starry_signal::SignalSet; use starry_vm::{VmMutPtr, VmPtr}; -use super::io::{sys_fdatasync, sys_fsync, sys_pread64, sys_preadv2, sys_pwrite64, sys_pwritev2}; -use crate::{file::get_file_like, mm::IoVec, task::AsThread}; +use crate::{ + file::{Directory, File, FileLike, get_file_like, memfd::Memfd}, + mm::{AddrSpace, Backend, IoVec}, + syscall::signal::check_sigset_size, + task::{AsThread, with_blocked_signals}, + time::TimeValueLike, +}; type AioContextId = usize; +type AioRequestId = u64; + +// Linux AIO ring structure. Userspace libaio/MySQL reads this mapping directly. +#[repr(C)] +#[derive(Clone, Copy)] +struct AioRing { + id: u32, + nr: u32, + head: u32, + tail: u32, + magic: u32, + compat_features: u32, + incompat_features: u32, + header_length: u32, +} + +const AIO_RING_MAGIC: u32 = 0xa10a10a1; +const AIO_RING_COMPAT_FEATURES: u32 = 1; +const AIO_RING_INCOMPAT_FEATURES: u32 = 0; const IOCB_CMD_PREAD: u16 = 0; const IOCB_CMD_PWRITE: u16 = 1; @@ -25,7 +71,8 @@ const IOCB_CMD_PREADV: u16 = 7; const IOCB_CMD_PWRITEV: u16 = 8; const IOCB_FLAG_RESFD: u32 = 1; -const MAX_PREALLOC_EVENTS: usize = 1024; +const IOCB_FLAG_IOPRIO: u32 = 1 << 1; +const AIO_MAX_WORKERS: usize = 4; #[repr(C)] #[derive(Clone, Copy, Default)] @@ -56,62 +103,303 @@ pub struct Iocb { resfd: u32, } +#[repr(C)] +#[derive(Clone, Copy)] +struct AioSigSet { + sigmask: *const SignalSet, + sigsetsize: usize, +} + +#[derive(Clone)] +struct UserSegment { + start: VirtAddr, + len: usize, +} + +struct UserBuffer { + segments: Vec, + len: usize, +} + +enum AioWriteTarget { + File(Arc), + Memfd(Arc), +} + +enum AioSyncTarget { + File(Arc), + Directory(Arc), + Memfd(Arc), +} + +enum AioOperation { + Read { + file: Arc, + offset: u64, + dst: UserBuffer, + }, + Write { + target: AioWriteTarget, + offset: u64, + data: Vec, + }, + Fsync { + target: AioSyncTarget, + data_only: bool, + }, + Poll { + file: Arc, + events: IoEvents, + }, + Noop, +} + +struct AioRequest { + id: AioRequestId, + cb_ptr: usize, + data: u64, + op: AioOperation, + resfd: Option>, +} + +struct PendingRequest { + cb_ptr: usize, + data: u64, + running: bool, +} + +struct AioContextInner { + inflight: usize, + queue: VecDeque>, + pending: BTreeMap, + worker_count: usize, +} + struct AioContext { + id: AioContextId, owner: Pid, - events: VecDeque, + aspace: Arc>, + ring_vaddr: VirtAddr, + ring_size: usize, + ring_events: u32, + ring_tail: AtomicUsize, + ring_lock: Mutex<()>, + ready_count: AtomicUsize, + queued_count: AtomicUsize, + destroying: AtomicBool, + work_wq: WaitQueue, + inflight_wq: WaitQueue, + completion_wakers: PollSet, + inner: Mutex, } impl AioContext { - // Create an AIO context owned by one process. - fn new(owner: Pid, nr_events: u32) -> Self { + // Build a process-owned AIO context around a mapped user ring. + fn new( + id: AioContextId, + owner: Pid, + aspace: Arc>, + ring_vaddr: VirtAddr, + ring_size: usize, + ring_events: u32, + ) -> Self { Self { + id, owner, - events: VecDeque::with_capacity((nr_events as usize).min(MAX_PREALLOC_EVENTS)), + aspace, + ring_vaddr, + ring_size, + ring_events, + ring_tail: AtomicUsize::new(0), + ring_lock: Mutex::new(()), + ready_count: AtomicUsize::new(0), + queued_count: AtomicUsize::new(0), + destroying: AtomicBool::new(false), + work_wq: WaitQueue::new(), + inflight_wq: WaitQueue::new(), + completion_wakers: PollSet::new(), + inner: Mutex::new(AioContextInner { + inflight: 0, + queue: VecDeque::new(), + pending: BTreeMap::new(), + worker_count: 0, + }), } } + + // Return usable completion slots, leaving one slot empty to distinguish full. + fn capacity(&self) -> usize { + self.ring_events.saturating_sub(1) as usize + } } static NEXT_AIO_CONTEXT_ID: AtomicUsize = AtomicUsize::new(1); -static AIO_CONTEXTS: RwLock> = RwLock::new(BTreeMap::new()); +static NEXT_AIO_REQUEST_ID: AtomicU64 = AtomicU64::new(1); +static AIO_CONTEXTS: RwLock>> = RwLock::new(BTreeMap::new()); -// Return the current process id used to scope AIO contexts. +// Return the process id that owns newly created or looked-up contexts. fn current_pid() -> Pid { ax_task::current().as_thread().proc_data.proc.pid() } -// Build the Linux-compatible error for an invalid AIO context. +// Use Linux EINVAL for all invalid AIO context handles. fn invalid_context() -> AxError { AxError::from(LinuxError::EINVAL) } -// Check whether a context belongs to the expected process. -fn context_belongs_to(context: &AioContext, owner: Pid) -> bool { - context.owner == owner +// Return the byte size of the userspace AIO ring header. +fn aio_ring_header_size() -> usize { + size_of::() +} + +// Return the byte size of one userspace completion event. +fn aio_event_size() -> usize { + size_of::() +} + +// Compute a page-aligned ring layout for the requested event count. +fn aio_ring_layout(nr_events: u32) -> AxResult<(usize, u32)> { + let requested = usize::try_from(nr_events).map_err(|_| AxError::InvalidInput)?; + let wanted_events = requested + .checked_mul(2) + .and_then(|events| events.checked_add(2)) + .ok_or(AxError::InvalidInput)?; + let min_size = aio_ring_header_size() + .checked_add( + wanted_events + .checked_mul(aio_event_size()) + .ok_or(AxError::InvalidInput)?, + ) + .ok_or(AxError::InvalidInput)?; + let ring_size = align_up_4k(min_size); + let ring_events = (ring_size - aio_ring_header_size()) / aio_event_size(); + let ring_events = u32::try_from(ring_events).map_err(|_| AxError::InvalidInput)?; + Ok((ring_size, ring_events)) } -// Validate that the AIO context exists for the current process. -fn check_context(ctx_id: AioContextId) -> AxResult<()> { +// Reserve and map the userspace ring buffer in the process address space. +fn allocate_aio_ring(aspace: &mut AddrSpace, ring_size: usize) -> AxResult { + let ring_vaddr = aspace + .find_free_area( + aspace.base(), + ring_size, + VirtAddrRange::new(aspace.base(), aspace.end()), + PageSize::Size4K as usize, + ) + .ok_or(AxError::NoMemory)?; + + let backend = Backend::new_alloc(ring_vaddr, PageSize::Size4K, "aio_ring"); + let flags = MappingFlags::READ | MappingFlags::WRITE | MappingFlags::USER; + aspace.map(ring_vaddr, ring_size, flags, true, backend)?; + Ok(ring_vaddr) +} + +// Create the initial Linux-compatible ring header. +fn initial_ring(ctx_id: AioContextId, ring_events: u32) -> AxResult { + Ok(AioRing { + id: u32::try_from(ctx_id).map_err(|_| AxError::NoMemory)?, + nr: ring_events, + head: 0, + tail: 0, + magic: AIO_RING_MAGIC, + compat_features: AIO_RING_COMPAT_FEATURES, + incompat_features: AIO_RING_INCOMPAT_FEATURES, + header_length: u32::try_from(aio_ring_header_size()).map_err(|_| AxError::NoMemory)?, + }) +} + +// Interpret the public context id as the userspace ring address. +fn ring_ptr(ctx: AioContextId) -> *mut AioRing { + ctx as *mut AioRing +} + +// Compute the address of a completion event inside the mapped ring. +fn ring_event_addr(context: &AioContext, index: u32) -> VirtAddr { + context.ring_vaddr + aio_ring_header_size() + index as usize * aio_event_size() +} + +// View a plain value as bytes for address-space writes. +fn typed_as_bytes(value: &T) -> &[u8] { + unsafe { slice::from_raw_parts((value as *const T).cast::(), size_of::()) } +} + +// View uninitialized storage as bytes for address-space reads. +fn typed_as_bytes_mut(value: &mut MaybeUninit) -> &mut [u8] { + unsafe { slice::from_raw_parts_mut(value.as_mut_ptr().cast::(), size_of::()) } +} + +// Read the ring header through the caller's user pointer. +fn read_ring_user(ctx: AioContextId) -> AxResult { + let ring = ring_ptr(ctx) + .cast_const() + .vm_read_uninit() + .map_err(|_| invalid_context())?; + Ok(unsafe { ring.assume_init() }) +} + +// Read the ring header from its owning address space. +fn read_ring_context(context: &AioContext) -> AxResult { + let mut ring = MaybeUninit::::uninit(); + context + .aspace + .lock() + .read(context.ring_vaddr, typed_as_bytes_mut(&mut ring))?; + Ok(unsafe { ring.assume_init() }) +} + +// Store a new ring head after userspace events are drained. +fn write_ring_head_context(context: &AioContext, head: u32) -> AxResult<()> { + context.aspace.lock().write( + context.ring_vaddr + offset_of!(AioRing, head), + typed_as_bytes(&head), + ) +} + +// Store a new ring tail after the kernel enqueues a completion. +fn write_ring_tail_context(context: &AioContext, tail: u32) -> AxResult<()> { + context.aspace.lock().write( + context.ring_vaddr + offset_of!(AioRing, tail), + typed_as_bytes(&tail), + ) +} + +// Read one completion event from the ring. +fn read_event_context(context: &AioContext, index: u32) -> AxResult { + let mut event = MaybeUninit::::uninit(); + context.aspace.lock().read( + ring_event_addr(context, index), + typed_as_bytes_mut(&mut event), + )?; + Ok(unsafe { event.assume_init() }) +} + +// Write one completion event into the ring. +fn write_event_context(context: &AioContext, index: u32, event: &IoEvent) -> AxResult<()> { + context + .aspace + .lock() + .write(ring_event_addr(context, index), typed_as_bytes(event)) +} + +// Validate a userspace context handle and return its kernel object. +fn lookup_context(ctx: AioContextId) -> AxResult> { let owner = current_pid(); + let ring = read_ring_user(ctx)?; let contexts = AIO_CONTEXTS.read(); + let ctx_id = ring.id as usize; match contexts.get(&ctx_id) { - Some(context) if context_belongs_to(context, owner) => Ok(()), + Some(context) + if context.owner == owner + && context.ring_vaddr.as_usize() == ctx + && ring.magic == AIO_RING_MAGIC => + { + Ok(context.clone()) + } _ => Err(invalid_context()), } } -// Queue one completed event into an AIO context. -fn enqueue_event(ctx_id: AioContextId, event: IoEvent) -> AxResult<()> { - let owner = current_pid(); - let mut contexts = AIO_CONTEXTS.write(); - let context = contexts - .get_mut(&ctx_id) - .filter(|context| context_belongs_to(context, owner)) - .ok_or_else(invalid_context)?; - context.events.push_back(event); - Ok(()) -} - -// Convert an operation result into the signed Linux AIO event result. +// Convert a syscall-style result into an io_event result field. fn result_to_event_res(result: AxResult) -> i64 { match result { Ok(n) => n as i64, @@ -119,107 +407,806 @@ fn result_to_event_res(result: AxResult) -> i64 { } } -// Convert a userspace length field into a native usize. +// Convert a user u64 length to this kernel's pointer-sized length. fn u64_to_usize(value: u64) -> AxResult { usize::try_from(value).map_err(|_| AxError::InvalidInput) } -// Extract the high offset word for 32-bit preadv2/pwritev2 ABI helpers. -fn offset_hi(offset: i64) -> usize { - #[cfg(target_pointer_width = "32")] - { - ((offset as u64) >> 32) as usize +// Convert an iocb offset into a non-negative file offset. +fn u64_to_offset(value: i64) -> AxResult { + if value < 0 { + Err(AxError::InvalidInput) + } else { + Ok(value as u64) } +} - #[cfg(target_pointer_width = "64")] - { - let _ = offset; - 0 +// Fault in and validate a user memory range before worker access. +fn prepare_user_region( + aspace: &Arc>, + start: VirtAddr, + len: usize, + flags: MappingFlags, +) -> AxResult<()> { + if len == 0 { + return Ok(()); } + let end = start + .as_usize() + .checked_add(len) + .ok_or(AxError::BadAddress)?; + let page_start = start.align_down_4k(); + let page_end = VirtAddr::from(end).align_up_4k(); + let mut guard = aspace.lock(); + if !guard.can_access_range(start, len, flags) { + return Err(AxError::BadAddress); + } + guard.populate_area(page_start, page_end - page_start, flags) } -// Notify an eventfd completion target when IOCB_FLAG_RESFD is set. -fn notify_resfd(resfd: u32) -> AxResult<()> { - let file = get_file_like(resfd as c_int)?; - let data = 1u64.to_ne_bytes(); - file.write(&mut data.as_slice())?; +// Copy a linear user buffer into owned kernel memory. +fn read_user_region( + aspace: &Arc>, + start: VirtAddr, + len: usize, +) -> AxResult> { + prepare_user_region(aspace, start, len, MappingFlags::READ)?; + let mut data = vec![0; len]; + if len != 0 { + let guard = aspace.lock(); + if !guard.can_access_range(start, len, MappingFlags::READ) { + return Err(AxError::BadAddress); + } + guard.read(start, &mut data)?; + } + Ok(data) +} + +// Build a one-segment user buffer descriptor. +fn user_buffer_from_linear( + aspace: &Arc>, + ptr: u64, + len: usize, + flags: MappingFlags, +) -> AxResult { + let start = VirtAddr::from(usize::try_from(ptr).map_err(|_| AxError::BadAddress)?); + prepare_user_region(aspace, start, len, flags)?; + Ok(UserBuffer { + segments: if len == 0 { + Vec::new() + } else { + vec![UserSegment { start, len }] + }, + len, + }) +} + +// Read an iovec array and normalize zero-length entries away. +fn read_iov(iov: *const IoVec, iovcnt: usize) -> AxResult> { + if iovcnt > 1024 { + return Err(AxError::InvalidInput); + } + let mut segments = Vec::with_capacity(iovcnt); + for i in 0..iovcnt { + let iov = iov.wrapping_add(i).vm_read()?; + if iov.iov_len < 0 { + return Err(AxError::InvalidInput); + } + let len = iov.iov_len as usize; + if len != 0 { + segments.push(UserSegment { + start: VirtAddr::from(iov.iov_base as usize), + len, + }); + } + } + Ok(segments) +} + +// Build a multi-segment user buffer from an iovec array. +fn user_buffer_from_iov( + aspace: &Arc>, + iov: *const IoVec, + iovcnt: usize, + flags: MappingFlags, +) -> AxResult { + let segments = read_iov(iov, iovcnt)?; + let mut total = 0usize; + for segment in &segments { + prepare_user_region(aspace, segment.start, segment.len, flags)?; + total = total + .checked_add(segment.len) + .filter(|len| *len <= isize::MAX as usize) + .ok_or(AxError::InvalidInput)?; + } + Ok(UserBuffer { + segments, + len: total, + }) +} + +// Copy all user segments into a contiguous kernel buffer. +fn read_user_segments(aspace: &Arc>, buf: &UserBuffer) -> AxResult> { + let mut data = vec![0; buf.len]; + let mut offset = 0usize; + let guard = aspace.lock(); + for segment in &buf.segments { + if !guard.can_access_range(segment.start, segment.len, MappingFlags::READ) { + return Err(AxError::BadAddress); + } + guard.read(segment.start, &mut data[offset..offset + segment.len])?; + offset += segment.len; + } + Ok(data) +} + +// Copy a kernel buffer back into user segments. +fn write_user_segments( + aspace: &Arc>, + buf: &UserBuffer, + data: &[u8], +) -> AxResult<()> { + let mut offset = 0usize; + let guard = aspace.lock(); + for segment in &buf.segments { + if offset >= data.len() { + break; + } + let len = segment.len.min(data.len() - offset); + if !guard.can_access_range(segment.start, len, MappingFlags::WRITE) { + return Err(AxError::BadAddress); + } + guard.write(segment.start, &data[offset..offset + len])?; + offset += len; + } Ok(()) } -// Execute one iocb synchronously through existing file syscalls. -fn execute_iocb(cb: &Iocb) -> AxResult { - if (cb.flags & !IOCB_FLAG_RESFD) != 0 { +// Resolve an fd that can be used by asynchronous writes. +fn write_target_from_fd(fd: c_int) -> AxResult { + if let Ok(memfd) = Memfd::from_fd(fd) { + Ok(AioWriteTarget::Memfd(memfd)) + } else { + let file = File::from_fd(fd).map_err(|e| { + if e == AxError::IsADirectory { + AxError::BadFileDescriptor + } else if e == AxError::BadFileDescriptor { + e + } else { + AxError::from(LinuxError::ESPIPE) + } + })?; + let _ = file.inner().access(FileFlags::WRITE)?; + Ok(AioWriteTarget::File(file)) + } +} + +// Resolve an fd that can be used by asynchronous reads. +fn read_file_from_fd(fd: c_int) -> AxResult> { + File::from_fd(fd).map_err(|e| { + if e == AxError::BadFileDescriptor || e == AxError::IsADirectory { + e + } else { + AxError::from(LinuxError::ESPIPE) + } + }) +} + +// Resolve an fd that can handle fsync or fdatasync. +fn sync_target_from_fd(fd: c_int) -> AxResult { + let file = get_file_like(fd)?; + if let Ok(memfd) = file.clone().downcast_arc::() { + Ok(AioSyncTarget::Memfd(memfd)) + } else if let Ok(file) = file.clone().downcast_arc::() { + Ok(AioSyncTarget::File(file)) + } else if let Ok(dir) = file.downcast_arc::() { + Ok(AioSyncTarget::Directory(dir)) + } else { + Err(AxError::from(LinuxError::EINVAL)) + } +} + +// Resolve the optional eventfd notification target from an iocb. +fn resolve_resfd(cb: &Iocb) -> AxResult>> { + if (cb.flags & IOCB_FLAG_RESFD) == 0 { + Ok(None) + } else { + get_file_like(cb.resfd as c_int).map(Some) + } +} + +// Validate iocb fields shared by all supported operations. +fn validate_iocb_common(cb: &Iocb) -> AxResult<()> { + if cb.reserved2 != 0 { + return Err(AxError::InvalidInput); + } + if (cb.flags & !(IOCB_FLAG_RESFD | IOCB_FLAG_IOPRIO)) != 0 { return Err(AxError::InvalidInput); } + Ok(()) +} - // Dispatch only the common file operations needed by the compatibility layer. - match cb.lio_opcode { +// Translate a userspace iocb into an owned request for worker execution. +fn prepare_request( + context: &Arc, + cb: &Iocb, + cb_ptr: *const Iocb, +) -> AxResult> { + validate_iocb_common(cb)?; + let resfd = resolve_resfd(cb)?; + let fd = cb.fildes as c_int; + // Snapshot or pin all user data before handing the request to a worker. + let op = match cb.lio_opcode { IOCB_CMD_PREAD => { if cb.rw_flags != 0 { return Err(AxError::OperationNotSupported); } - sys_pread64( - cb.fildes as c_int, - cb.buf as *mut u8, - u64_to_usize(cb.nbytes)?, - cb.offset as __kernel_off_t, - ) + AioOperation::Read { + file: read_file_from_fd(fd)?, + offset: u64_to_offset(cb.offset)?, + dst: user_buffer_from_linear( + &context.aspace, + cb.buf, + u64_to_usize(cb.nbytes)?, + MappingFlags::WRITE, + )?, + } } IOCB_CMD_PWRITE => { if cb.rw_flags != 0 { return Err(AxError::OperationNotSupported); } - sys_pwrite64( - cb.fildes as c_int, - cb.buf as *const u8, + let start = VirtAddr::from(usize::try_from(cb.buf).map_err(|_| AxError::BadAddress)?); + let len = u64_to_usize(cb.nbytes)?; + let data = read_user_region(&context.aspace, start, len)?; + AioOperation::Write { + target: write_target_from_fd(fd)?, + offset: u64_to_offset(cb.offset)?, + data, + } + } + IOCB_CMD_FSYNC => AioOperation::Fsync { + target: sync_target_from_fd(fd)?, + data_only: false, + }, + IOCB_CMD_FDSYNC => AioOperation::Fsync { + target: sync_target_from_fd(fd)?, + data_only: true, + }, + IOCB_CMD_POLL => { + if cb.rw_flags != 0 { + return Err(AxError::OperationNotSupported); + } + let events = IoEvents::from_bits(cb.buf as u32).ok_or(AxError::InvalidInput)?; + AioOperation::Poll { + file: get_file_like(fd)?, + events: events | IoEvents::ALWAYS_POLL, + } + } + IOCB_CMD_NOOP => AioOperation::Noop, + IOCB_CMD_PREADV => { + if cb.rw_flags != 0 { + return Err(AxError::OperationNotSupported); + } + AioOperation::Read { + file: read_file_from_fd(fd)?, + offset: u64_to_offset(cb.offset)?, + dst: user_buffer_from_iov( + &context.aspace, + cb.buf as *const IoVec, + u64_to_usize(cb.nbytes)?, + MappingFlags::WRITE, + )?, + } + } + IOCB_CMD_PWRITEV => { + if cb.rw_flags != 0 { + return Err(AxError::OperationNotSupported); + } + let src = user_buffer_from_iov( + &context.aspace, + cb.buf as *const IoVec, u64_to_usize(cb.nbytes)?, - cb.offset as __kernel_off_t, - ) - } - IOCB_CMD_FSYNC => sys_fsync(cb.fildes as c_int), - IOCB_CMD_FDSYNC => sys_fdatasync(cb.fildes as c_int), - IOCB_CMD_NOOP => Ok(0), - IOCB_CMD_PREADV => sys_preadv2( - cb.fildes as c_int, - cb.buf as *const IoVec, - u64_to_usize(cb.nbytes)?, - cb.offset as __kernel_off_t, - offset_hi(cb.offset), - cb.rw_flags, - ), - IOCB_CMD_PWRITEV => sys_pwritev2( - cb.fildes as c_int, - cb.buf as *const IoVec, - u64_to_usize(cb.nbytes)?, - cb.offset as __kernel_off_t, - offset_hi(cb.offset), - cb.rw_flags, - ), - IOCB_CMD_POLL => Err(AxError::OperationNotSupported), - _ => Err(AxError::InvalidInput), - } -} - -// Build the completion event for one submitted iocb. -fn complete_iocb(cb: &Iocb, cb_ptr: *const Iocb) -> IoEvent { - let result = execute_iocb(cb); - if result.is_ok() && (cb.flags & IOCB_FLAG_RESFD) != 0 { - // Best-effort eventfd notification; the AIO result stays in the queue. - let _ = notify_resfd(cb.resfd); + MappingFlags::READ, + )?; + let data = read_user_segments(&context.aspace, &src)?; + AioOperation::Write { + target: write_target_from_fd(fd)?, + offset: u64_to_offset(cb.offset)?, + data, + } + } + _ => return Err(AxError::InvalidInput), + }; + + let id = NEXT_AIO_REQUEST_ID.fetch_add(1, Ordering::Relaxed); + if id == 0 { + return Err(AxError::NoMemory); + } + Ok(Arc::new(AioRequest { + id, + cb_ptr: cb_ptr as usize, + data: cb.data, + op, + resfd, + })) +} + +// Signal an eventfd completion counter when IOCB_FLAG_RESFD is set. +fn notify_resfd(resfd: &Arc) -> AxResult<()> { + let data = 1u64.to_ne_bytes(); + resfd.write(&mut data.as_slice())?; + Ok(()) +} + +// Execute a positioned read and copy the bytes into the original user buffer. +fn execute_read( + context: &AioContext, + file: &Arc, + offset: u64, + dst: &UserBuffer, +) -> AxResult { + let mut data = vec![0; dst.len]; + let read = file.inner().read_at(&mut data[..], offset)?; + write_user_segments(&context.aspace, dst, &data[..read])?; + Ok(read as isize) +} + +// Execute a positioned write to a regular file or memfd. +fn execute_write(target: &AioWriteTarget, offset: u64, data: &[u8]) -> AxResult { + match target { + AioWriteTarget::File(file) => { + let file = file.inner().access(FileFlags::WRITE)?; + file.write_at(data, offset).map(|n| n as isize) + } + AioWriteTarget::Memfd(memfd) => memfd.write_at(data, offset).map(|n| n as isize), + } +} + +// Execute fsync or fdatasync against a supported target. +fn execute_fsync(target: &AioSyncTarget, data_only: bool) -> AxResult { + match target { + AioSyncTarget::File(file) => file.inner().sync(data_only)?, + AioSyncTarget::Directory(dir) => dir.inner().sync(data_only)?, + AioSyncTarget::Memfd(memfd) => memfd.inner().inner().sync(data_only)?, + } + Ok(0) +} + +// Return ready poll events, including Linux always-reported error bits. +fn ready_poll_events(file: &Arc, interested: IoEvents) -> Option { + let mut ready = file.poll(); + if ready.contains(IoEvents::IN) { + ready |= IoEvents::RDNORM; } + if ready.contains(IoEvents::OUT) { + ready |= IoEvents::WRNORM; + } + let always = ready & (IoEvents::ERR | IoEvents::HUP | IoEvents::RDHUP | IoEvents::NVAL); + ready &= interested; + ready |= always; + (!ready.is_empty()).then_some(ready.bits() as isize) +} + +// Wait until a poll request becomes ready or the context is destroyed. +fn poll_result( + context: &AioContext, + file: &Arc, + interested: IoEvents, +) -> AxResult { + block_on(interruptible(poll_fn(|cx| { + // Check before registration so already-ready fds complete immediately. + if context.destroying.load(Ordering::Acquire) { + return core::task::Poll::Ready(Err(AxError::Interrupted)); + } + if let Some(ready) = ready_poll_events(file, interested) { + return core::task::Poll::Ready(Ok(ready)); + } + file.register(cx, interested); + context.completion_wakers.register(cx.waker()); + // Re-check after registration to avoid losing a destroy or readiness wake. + if context.destroying.load(Ordering::Acquire) { + return core::task::Poll::Ready(Err(AxError::Interrupted)); + } + if let Some(ready) = ready_poll_events(file, interested) { + return core::task::Poll::Ready(Ok(ready)); + } + core::task::Poll::Pending + }))) + .map_err(AxError::from)? +} +// Dispatch one prepared request to the matching operation implementation. +fn execute_request(context: &AioContext, request: &AioRequest) -> AxResult { + debug!( + "execute_request: request_id={}, cb_ptr={:#x}", + request.id, request.cb_ptr + ); + let result = match &request.op { + AioOperation::Read { file, offset, dst } => { + debug!("execute_request: READ offset={}, len={}", offset, dst.len); + execute_read(context, file, *offset, dst) + } + AioOperation::Write { + target, + offset, + data, + } => { + debug!( + "execute_request: WRITE offset={}, len={}", + offset, + data.len() + ); + execute_write(target, *offset, data) + } + AioOperation::Fsync { target, data_only } => { + debug!("execute_request: FSYNC data_only={}", data_only); + execute_fsync(target, *data_only) + } + AioOperation::Poll { file, events } => { + debug!("execute_request: POLL events={:?}", events); + poll_result(context, file, *events) + } + AioOperation::Noop => { + debug!("execute_request: NOOP"); + Ok(0) + } + }; + debug!( + "execute_request: request_id={} completed, result={:?}", + request.id, result + ); + result +} + +// Build the userspace completion event for a finished request. +fn completion_event(request: &AioRequest, result: AxResult) -> IoEvent { + if let Some(resfd) = &request.resfd { + let _ = notify_resfd(resfd); + } IoEvent { - data: cb.data, - obj: cb_ptr as u64, + data: request.data, + obj: request.cb_ptr as u64, res: result_to_event_res(result), res2: 0, } } -// Create a process-local AIO context for compatibility. +// Count events currently visible in the circular ring. +fn ring_ready_count(ring_events: u32, head: u32, tail: u32) -> usize { + if ring_events == 0 { + return 0; + } + if tail >= head { + (tail - head) as usize + } else { + (ring_events - head + tail) as usize + } +} + +// Read and validate the user-visible ring head. +fn checked_ring_head(context: &AioContext) -> AxResult { + let ring = read_ring_context(context)?; + if ring.magic != AIO_RING_MAGIC || ring.nr != context.ring_events || ring.nr < 2 { + return Err(invalid_context()); + } + Ok(ring.head % context.ring_events) +} + +// Recompute the cached ready count from ring head and tail. +fn refresh_ready_count(context: &AioContext) -> AxResult { + let _ring = context.ring_lock.lock(); + let head = checked_ring_head(context)?; + let tail = context.ring_tail.load(Ordering::Acquire) as u32 % context.ring_events; + let ready = ring_ready_count(context.ring_events, head, tail); + context.ready_count.store(ready, Ordering::Release); + Ok(ready) +} + +// Append one completion into the ring and wake waiters. +fn enqueue_completion(context: &AioContext, event: IoEvent) -> AxResult<()> { + let _ring = context.ring_lock.lock(); + let head = checked_ring_head(context)?; + let tail = context.ring_tail.load(Ordering::Acquire) as u32 % context.ring_events; + let next_tail = if tail + 1 >= context.ring_events { + 0 + } else { + tail + 1 + }; + if next_tail == head { + return Err(AxError::WouldBlock); + } + + // Event data must be visible before publishing the new tail. + write_event_context(context, tail, &event)?; + write_ring_tail_context(context, next_tail)?; + context + .ring_tail + .store(next_tail as usize, Ordering::Release); + context.ready_count.store( + ring_ready_count(context.ring_events, head, next_tail), + Ordering::Release, + ); + context.completion_wakers.wake(); + Ok(()) +} + +// Remove a finished request from accounting and notify blocked paths. +fn finish_request(context: &AioContext, request: &AioRequest, event: IoEvent) { + debug!( + "finish_request: request_id={}, res={}", + request.id, event.res + ); + match enqueue_completion(context, event) { + Ok(()) => { + debug!("finish_request: enqueued completion successfully"); + } + Err(e) => { + warn!( + "finish_request: enqueue_completion failed: {:?}, ring may be full", + e + ); + } + } + { + let mut inner = context.inner.lock(); + inner.pending.remove(&request.id); + inner.inflight = inner.inflight.saturating_sub(1); + debug!( + "finish_request: inflight={}, pending={}", + inner.inflight, + inner.pending.len() + ); + } + context.inflight_wq.notify_all(true); + context.completion_wakers.wake(); +} + +// Pop the next queued request and mark it as running. +fn next_work(context: &Arc) -> Option> { + let mut inner = context.inner.lock(); + let request = inner.queue.pop_front()?; + context.queued_count.fetch_sub(1, Ordering::AcqRel); + if let Some(pending) = inner.pending.get_mut(&request.id) { + pending.running = true; + Some(request) + } else { + None + } +} + +// Worker loop that executes queued requests for one AIO context. +fn aio_worker(context: Arc) { + debug!("aio_worker: started"); + loop { + let request = loop { + // Prefer existing queued work; otherwise block until work or destroy. + if let Some(request) = next_work(&context) { + debug!("aio_worker: got work, request_id={}", request.id); + break request; + } + if context.destroying.load(Ordering::Acquire) { + debug!("aio_worker: destroying flag set, exiting"); + return; + } + debug!("aio_worker: waiting for work"); + context.work_wq.wait_until(|| { + context.queued_count.load(Ordering::Acquire) != 0 + || context.destroying.load(Ordering::Acquire) + }); + }; + + // All completions pass through finish_request for accounting and wakeups. + debug!("aio_worker: executing request_id={}", request.id); + let result = execute_request(&context, &request); + let event = completion_event(&request, result); + finish_request(&context, &request, event); + debug!("aio_worker: finished request_id={}", request.id); + } +} + +// Bound worker fan-out by both ring capacity and a fixed kernel limit. +fn max_worker_count(context: &AioContext) -> usize { + context.capacity().clamp(1, AIO_MAX_WORKERS) +} + +// Queue a request and start a worker if this context can use another one. +fn enqueue_request(context: &Arc, request: Arc) -> AxResult<()> { + refresh_ready_count(context)?; + let spawn_worker = { + let mut inner = context.inner.lock(); + if context.destroying.load(Ordering::Acquire) { + return Err(invalid_context()); + } + // Ring capacity covers both ready completions and in-flight work. + let used = inner + .inflight + .checked_add(context.ready_count.load(Ordering::Acquire)) + .ok_or(AxError::InvalidInput)?; + if used >= context.capacity() { + return Err(AxError::WouldBlock); + } + inner.inflight += 1; + inner.pending.insert( + request.id, + PendingRequest { + cb_ptr: request.cb_ptr, + data: request.data, + running: false, + }, + ); + inner.queue.push_back(request); + context.queued_count.fetch_add(1, Ordering::AcqRel); + if inner.worker_count < max_worker_count(context) { + inner.worker_count += 1; + true + } else { + false + } + }; + + if spawn_worker { + let worker_context = context.clone(); + ax_task::spawn_with_name( + move || aio_worker(worker_context), + String::from("aio-worker"), + ); + } + context.work_wq.notify_one(true); + Ok(()) +} + +// Wait for at least one completion or for the optional deadline to expire. +fn wait_for_completion( + context: &AioContext, + deadline: Option, +) -> AxResult { + let wait = poll_fn(|cx| { + // Register then re-check to avoid a completion wake racing this waiter. + if context.ready_count.load(Ordering::Acquire) != 0 + || context.destroying.load(Ordering::Acquire) + { + core::task::Poll::Ready(()) + } else { + context.completion_wakers.register(cx.waker()); + if context.ready_count.load(Ordering::Acquire) != 0 + || context.destroying.load(Ordering::Acquire) + { + core::task::Poll::Ready(()) + } else { + core::task::Poll::Pending + } + } + }); + + match block_on(interruptible(timeout_at(deadline, wait))) { + Ok(Ok(())) => Ok(true), + Ok(Err(_)) => Ok(false), + Err(_) => Err(AxError::Interrupted), + } +} + +// Wait until io_destroy sees every running request finish. +fn wait_for_inflight_drain(context: &AioContext) { + block_on(poll_fn(|cx| { + let inflight = context.inner.lock().inflight; + if inflight == 0 { + return core::task::Poll::Ready(()); + } + + debug!("sys_io_destroy: still waiting, inflight={}", inflight); + context.completion_wakers.register(cx.waker()); + // Re-check after registration so the final completion cannot be missed. + if context.inner.lock().inflight == 0 { + core::task::Poll::Ready(()) + } else { + core::task::Poll::Pending + } + })) +} + +// Read an optional relative timeout from userspace. +fn read_timeout(timeout: *const timespec) -> AxResult> { + if timeout.is_null() { + return Ok(None); + } + let timeout = unsafe { timeout.vm_read_uninit()?.assume_init() }.try_into_time_value()?; + Ok(Some(timeout)) +} + +// Drain completion events from the ring into the userspace output array. +fn copy_completed_events( + context: &AioContext, + max: usize, + events: *mut IoEvent, + completed_offset: usize, +) -> AxResult { + let mut copied = 0usize; + let _ring = context.ring_lock.lock(); + let ring_events = context.ring_events; + let mut head = checked_ring_head(context)?; + let tail = context.ring_tail.load(Ordering::Acquire) as u32 % ring_events; + + while copied < max && head != tail { + let event = read_event_context(context, head)?; + // If a later copy fails, keep the events already delivered visible. + if let Err(err) = events + .wrapping_add(completed_offset + copied) + .vm_write(event) + { + if copied > 0 { + write_ring_head_context(context, head)?; + context + .ready_count + .store(ring_ready_count(ring_events, head, tail), Ordering::Release); + return Ok(copied); + } + context + .ready_count + .store(ring_ready_count(ring_events, head, tail), Ordering::Release); + return Err(err.into()); + } + copied += 1; + head = if head + 1 >= ring_events { 0 } else { head + 1 }; + } + + // Publish the new head only after successful user copies. + if copied > 0 { + write_ring_head_context(context, head)?; + } + context + .ready_count + .store(ring_ready_count(ring_events, head, tail), Ordering::Release); + Ok(copied) +} + +// Shared implementation for io_getevents and io_pgetevents. +fn do_io_getevents( + context: Arc, + min_nr: isize, + nr: isize, + events: *mut IoEvent, + timeout: *const timespec, +) -> AxResult { + if min_nr < 0 || nr < 0 || min_nr > nr { + return Err(AxError::InvalidInput); + } + if nr == 0 { + return Ok(0); + } + + let min_nr = min_nr as usize; + let nr = nr as usize; + let deadline = read_timeout(timeout)?.and_then(|duration| wall_time().checked_add(duration)); + let mut completed = 0usize; + + loop { + // First drain everything already ready before sleeping. + let copied = copy_completed_events(&context, nr - completed, events, completed)?; + completed += copied; + if completed >= min_nr || completed == nr || min_nr == 0 { + return Ok(completed as isize); + } + if context.destroying.load(Ordering::Acquire) { + return Ok(completed as isize); + } + + // Sleep only when min_nr still requires more events. + match wait_for_completion(&context, deadline) { + Ok(true) => {} + Ok(false) => return Ok(completed as isize), + Err(_) if completed > 0 => return Ok(completed as isize), + Err(err) => return Err(err), + } + } +} + +// Create an AIO context and expose its ring address to userspace. pub fn sys_io_setup(nr_events: u32, ctxp: *mut AioContextId) -> AxResult { - debug!("sys_io_setup <= nr_events: {nr_events}, ctxp: {ctxp:p}"); - // Linux expects a non-zero ring size and an empty user context slot. + debug!( + "sys_io_setup called: nr_events={}, ctxp={:p}", + nr_events, ctxp + ); if nr_events == 0 { return Err(AxError::InvalidInput); } @@ -228,51 +1215,99 @@ pub fn sys_io_setup(nr_events: u32, ctxp: *mut AioContextId) -> AxResult } let ctx_id = NEXT_AIO_CONTEXT_ID.fetch_add(1, Ordering::Relaxed); - if ctx_id == 0 { + if ctx_id == 0 || u32::try_from(ctx_id).is_err() { return Err(AxError::NoMemory); } + // Allocate the user ring before publishing the context globally. + let (ring_size, ring_events) = aio_ring_layout(nr_events)?; + let curr = ax_task::current(); + let aspace = curr.as_thread().proc_data.aspace(); + let ring_vaddr = { + let mut guard = aspace.lock(); + allocate_aio_ring(&mut guard, ring_size)? + }; + let ring = initial_ring(ctx_id, ring_events)?; + aspace.lock().write(ring_vaddr, typed_as_bytes(&ring))?; - // Keep contexts process-local so stale ids from other processes are rejected. - AIO_CONTEXTS - .write() - .insert(ctx_id, AioContext::new(current_pid(), nr_events)); - ctxp.vm_write(ctx_id)?; + let context = Arc::new(AioContext::new( + ctx_id, + current_pid(), + aspace.clone(), + ring_vaddr, + ring_size, + ring_events, + )); + AIO_CONTEXTS.write().insert(ctx_id, context); + + // If writing ctxp fails, roll back both the global entry and mapping. + let ctx_value = ring_vaddr.as_usize(); + if let Err(err) = ctxp.vm_write(ctx_value) { + AIO_CONTEXTS.write().remove(&ctx_id); + let _ = aspace.lock().unmap(ring_vaddr, ring_size); + return Err(err.into()); + } + debug!( + "sys_io_setup: success, ctx_id={:#x}, ring_vaddr={:#x}", + ctx_id, ctx_value + ); Ok(0) } -// Destroy a process-local AIO context and drop pending events. -pub fn sys_io_destroy(ctx_id: AioContextId) -> AxResult { - debug!("sys_io_destroy <= ctx_id: {ctx_id:#x}"); - let owner = current_pid(); - let mut contexts = AIO_CONTEXTS.write(); - match contexts.get(&ctx_id) { - Some(context) if context_belongs_to(context, owner) => { - contexts.remove(&ctx_id); - Ok(0) - } - _ => Err(invalid_context()), +// Destroy an AIO context after cancelling queued work and draining workers. +pub fn sys_io_destroy(ctx: AioContextId) -> AxResult { + debug!("sys_io_destroy called: ctx={:#x}", ctx); + let context = lookup_context(ctx)?; + AIO_CONTEXTS.write().remove(&context.id); + context.destroying.store(true, Ordering::Release); + + { + // Drop queued requests; running requests are allowed to complete. + let mut inner = context.inner.lock(); + let queued = inner.queue.len(); + inner.queue.clear(); + inner.pending.retain(|_, pending| pending.running); + inner.inflight = inner.inflight.saturating_sub(queued); + context.queued_count.store(0, Ordering::Release); + warn!( + "sys_io_destroy: cleared queue, inflight={}, pending={}", + inner.inflight, + inner.pending.len() + ); + } + context.work_wq.notify_all(true); + context.completion_wakers.wake(); + + // Wait outside the inner lock so workers can finish_request. + warn!("sys_io_destroy: waiting for inflight to drain"); + wait_for_inflight_drain(&context); + warn!("sys_io_destroy: all inflight drained"); + + let mut aspace = context.aspace.lock(); + if let Err(err) = aspace.unmap(context.ring_vaddr, context.ring_size) { + warn!("sys_io_destroy: failed to unmap ring buffer: {:?}", err); } + debug!("sys_io_destroy: success"); + Ok(0) } -// Submit iocbs and complete supported operations synchronously. -pub fn sys_io_submit( - ctx_id: AioContextId, - nr: isize, - iocbpp: *const *const Iocb, -) -> AxResult { - debug!("sys_io_submit <= ctx_id: {ctx_id:#x}, nr: {nr}, iocbpp: {iocbpp:p}"); - // Validate the request before touching the userspace iocb array. +// Submit a batch of iocbs to the target AIO context. +pub fn sys_io_submit(ctx: AioContextId, nr: isize, iocbpp: *const *const Iocb) -> AxResult { + debug!("sys_io_submit <= ctx: {ctx:#x}, nr: {nr}, iocbpp: {iocbpp:p}"); if nr < 0 { return Err(AxError::InvalidInput); } if nr == 0 { + lookup_context(ctx)?; return Ok(0); } - check_context(ctx_id)?; + let context = lookup_context(ctx)?; + if context.destroying.load(Ordering::Acquire) { + return Err(invalid_context()); + } let mut submitted = 0isize; for i in 0..nr as usize { - // Read each userspace iocb pointer, then copy the iocb itself. + // Linux returns a partial count once at least one request was queued. let cb_ptr = match iocbpp.wrapping_add(i).vm_read() { Ok(ptr) => ptr, Err(_) if submitted > 0 => return Ok(submitted), @@ -283,84 +1318,118 @@ pub fn sys_io_submit( Err(_) if submitted > 0 => return Ok(submitted), Err(err) => return Err(err.into()), }; - - // Operations complete synchronously; only their completion event is queued. - let event = complete_iocb(&cb, cb_ptr); - match enqueue_event(ctx_id, event) { - Ok(()) => submitted += 1, + debug!( + "sys_io_submit: opcode={}, fd={}, offset={}, nbytes={}", + cb.lio_opcode, cb.fildes, cb.offset, cb.nbytes + ); + let request = match prepare_request(&context, &cb, cb_ptr) { + Ok(request) => request, + Err(_) if submitted > 0 => return Ok(submitted), + Err(err) => return Err(err), + }; + match enqueue_request(&context, request) { + Ok(()) => { + submitted += 1; + debug!("sys_io_submit: enqueued, submitted={}", submitted); + } Err(_) if submitted > 0 => return Ok(submitted), Err(err) => return Err(err), } } - + debug!("sys_io_submit => submitted={}", submitted); Ok(submitted) } -// Copy queued AIO completions back to userspace. +// Retrieve completed events from an AIO context. pub fn sys_io_getevents( - ctx_id: AioContextId, + ctx: AioContextId, min_nr: isize, nr: isize, events: *mut IoEvent, - _timeout: *const timespec, + timeout: *const timespec, ) -> AxResult { - debug!( - "sys_io_getevents <= ctx_id: {ctx_id:#x}, min_nr: {min_nr}, nr: {nr}, events: {events:p}" - ); - // min_nr cannot exceed the maximum number of events requested. - if min_nr < 0 || nr < 0 || min_nr > nr { - return Err(AxError::InvalidInput); - } - if nr == 0 { - check_context(ctx_id)?; - return Ok(0); - } - - let owner = current_pid(); - let mut completed = 0usize; - let mut contexts = AIO_CONTEXTS.write(); - let context = contexts - .get_mut(&ctx_id) - .filter(|context| context_belongs_to(context, owner)) - .ok_or_else(invalid_context)?; - - while completed < nr as usize { - let Some(event) = context.events.front().copied() else { - break; - }; - // Write first, then pop, so EFAULT does not lose a completion. - if let Err(err) = events.wrapping_add(completed).vm_write(event) { - if completed > 0 { - return Ok(completed as isize); - } - return Err(err.into()); - } - context.events.pop_front(); - completed += 1; - } - - Ok(completed as isize) + debug!("sys_io_getevents <= ctx: {ctx:#x}, min_nr: {min_nr}, nr: {nr}, events: {events:p}"); + let context = lookup_context(ctx)?; + let result = do_io_getevents(context, min_nr, nr, events, timeout)?; + debug!("sys_io_getevents => result={}", result); + Ok(result) } -// Handle io_pgetevents with the same non-blocking completion queue. +// Retrieve events while temporarily applying a signal mask. pub fn sys_io_pgetevents( - ctx_id: AioContextId, + ctx: AioContextId, min_nr: isize, nr: isize, events: *mut IoEvent, timeout: *const timespec, - _sigmask: usize, + sigmask: usize, ) -> AxResult { - sys_io_getevents(ctx_id, min_nr, nr, events, timeout) + let context = lookup_context(ctx)?; + if sigmask == 0 { + return do_io_getevents(context, min_nr, nr, events, timeout); + } + + let sigset = unsafe { + (sigmask as *const AioSigSet) + .vm_read_uninit()? + .assume_init() + }; + check_sigset_size(sigset.sigsetsize)?; + // A null sigmask means pgetevents behaves like getevents. + let blocked = if sigset.sigmask.is_null() { + None + } else { + Some(unsafe { sigset.sigmask.vm_read_uninit()?.assume_init() }) + }; + with_blocked_signals(blocked, || { + do_io_getevents(context, min_nr, nr, events, timeout) + }) } -// Report cancellation as unavailable because operations complete immediately. +// Cancel a queued request that has not started running. pub fn sys_io_cancel( - ctx_id: AioContextId, - _iocb: *const Iocb, - _result: *mut IoEvent, + ctx: AioContextId, + iocb: *const Iocb, + result: *mut IoEvent, ) -> AxResult { - debug!("sys_io_cancel <= ctx_id: {ctx_id:#x}"); - check_context(ctx_id)?; - Err(AxError::InvalidInput) + debug!("sys_io_cancel <= ctx: {ctx:#x}, iocb: {iocb:p}, result: {result:p}"); + let context = lookup_context(ctx)?; + let cb_ptr = iocb as usize; + + let event = { + let mut inner = context.inner.lock(); + // Linux AIO can only cancel requests still waiting in the queue here. + let Some((&request_id, pending)) = inner + .pending + .iter() + .find(|(_, pending)| pending.cb_ptr == cb_ptr) + else { + return Err(AxError::InvalidInput); + }; + if pending.running { + return Err(AxError::InvalidInput); + } + let pending = inner + .pending + .remove(&request_id) + .ok_or(AxError::InvalidInput)?; + let before = inner.queue.len(); + inner.queue.retain(|request| request.id != request_id); + if inner.queue.len() != before { + context.queued_count.fetch_sub(1, Ordering::AcqRel); + } + inner.inflight = inner.inflight.saturating_sub(1); + IoEvent { + data: pending.data, + obj: cb_ptr as u64, + res: -(LinuxError::ECANCELED.code() as i64), + res2: 0, + } + }; + + result.vm_write(event)?; + context.inflight_wq.notify_all(true); + context.work_wq.notify_one(true); + context.completion_wakers.wake(); + Ok(0) } diff --git a/os/StarryOS/kernel/src/syscall/fs/ctl.rs b/os/StarryOS/kernel/src/syscall/fs/ctl.rs index f460ac98df..a4977e0aaf 100644 --- a/os/StarryOS/kernel/src/syscall/fs/ctl.rs +++ b/os/StarryOS/kernel/src/syscall/fs/ctl.rs @@ -17,7 +17,7 @@ use ax_task::current; use axfs_ng_vfs::{DeviceId, MetadataUpdate, NodePermission, NodeType, path::Path}; use linux_raw_sys::{ general::*, - ioctl::{BLKGETSIZE64, BLKRAGET, BLKSSZGET, FIOASYNC, FIONBIO, TIOCGWINSZ}, + ioctl::{BLKGETSIZE64, BLKRAGET, BLKSSZGET, FIOASYNC, FIONBIO, TCGETS, TIOCGWINSZ}, }; use starry_vm::{VmPtr, vm_write_slice}; @@ -57,7 +57,10 @@ pub fn sys_ioctl(fd: i32, cmd: u32, arg: usize) -> AxResult { if *err == AxError::NotATty { // Applications commonly probe non-terminal/blobk fds with // these ioctls; suppress noise. - if matches!(cmd, TIOCGWINSZ | BLKGETSIZE64 | BLKRAGET | BLKSSZGET) { + if matches!( + cmd, + TIOCGWINSZ | TCGETS | BLKGETSIZE64 | BLKRAGET | BLKSSZGET + ) { return; } warn!("Unsupported ioctl command: {cmd} for fd: {fd}"); @@ -382,6 +385,7 @@ pub fn sys_unlinkat(dirfd: i32, path: *const c_char, flags: usize) -> AxResult AxResult { - debug!("sys_eventfd2 <= initval: {initval}, flags: {flags}"); + debug!( + "sys_eventfd2 called: initval={}, flags={:#x}", + initval, flags + ); let flags = EventFdFlags::from_bits(flags).ok_or(AxError::InvalidInput)?; let event_fd = EventFd::new(initval as _, flags.contains(EventFdFlags::SEMAPHORE)); event_fd.set_nonblocking(flags.contains(EventFdFlags::NONBLOCK))?; - add_file_like(event_fd as _, flags.contains(EventFdFlags::CLOEXEC)).map(|fd| fd as _) + let fd = + add_file_like(event_fd as _, flags.contains(EventFdFlags::CLOEXEC)).map(|fd| fd as _)?; + debug!("sys_eventfd2: success, fd={}", fd); + Ok(fd) } diff --git a/os/StarryOS/kernel/src/syscall/fs/fd_ops.rs b/os/StarryOS/kernel/src/syscall/fs/fd_ops.rs index fdb94f1f89..010a4b61e4 100644 --- a/os/StarryOS/kernel/src/syscall/fs/fd_ops.rs +++ b/os/StarryOS/kernel/src/syscall/fs/fd_ops.rs @@ -336,6 +336,7 @@ pub fn sys_openat( Err(err) => Err(err), })?; + // Open first, then install the file so filesystem errors propagate unchanged. let fd = with_fs(dirfd, |fs| options.open(fs, path)).and_then(|it| add_to_fd(it, flags as _))?; if should_notify_create { diff --git a/os/StarryOS/kernel/src/syscall/fs/io.rs b/os/StarryOS/kernel/src/syscall/fs/io.rs index 4a6312eaa6..07fb2f61c0 100644 --- a/os/StarryOS/kernel/src/syscall/fs/io.rs +++ b/os/StarryOS/kernel/src/syscall/fs/io.rs @@ -10,7 +10,9 @@ use ax_io::{IoBuf, Read, Seek, SeekFrom}; use ax_task::current; use axfs_ng_vfs::{NodePermission, NodeType}; use axpoll::{IoEvents, Pollable}; -use linux_raw_sys::general::{__kernel_off_t, O_APPEND}; +use linux_raw_sys::general::{ + __kernel_off_t, FALLOC_FL_KEEP_SIZE, FALLOC_FL_PUNCH_HOLE, FALLOC_FL_ZERO_RANGE, O_APPEND, +}; use starry_vm::{VmMutPtr, VmPtr}; use syscalls::Sysno; @@ -68,6 +70,29 @@ fn offset_from_hilo(pos_l: __kernel_off_t, _pos_h: usize) -> __kernel_off_t { } } +// Writes zero-filled chunks into the file over the requested byte range. +fn write_zero_range(file: &ax_fs::FileBackend, mut offset: u64, len: u64) -> AxResult<()> { + const ZERO_CHUNK_SIZE: usize = 64 * 1024; + + let zeroes = vec![0; ZERO_CHUNK_SIZE]; + let mut remaining = len; + while remaining > 0 { + let chunk = remaining.min(ZERO_CHUNK_SIZE as u64) as usize; + let mut written = 0; + while written < chunk { + let n = file.write_at(&zeroes[written..chunk], offset)?; + if n == 0 { + return Err(AxError::WriteZero); + } + written += n; + offset += n as u64; + remaining -= n as u64; + } + } + + Ok(()) +} + struct DummyFd; impl FileLike for DummyFd { fn path(&self) -> Cow<'_, str> { @@ -244,7 +269,13 @@ pub fn sys_fallocate( let f = file_or_espipe_write(fd)?; let f_like = get_file_like(fd)?; memfd_check_write_seal(&f_like)?; - if mode != 0 { + + let keep_size = mode & FALLOC_FL_KEEP_SIZE != 0; + let operation = mode & !FALLOC_FL_KEEP_SIZE; + let supported_mode = operation == 0 && !keep_size + || operation == FALLOC_FL_ZERO_RANGE + || operation == FALLOC_FL_PUNCH_HOLE && keep_size; + if !supported_mode { return Err(AxError::OperationNotSupported); } if offset < 0 || len <= 0 { @@ -267,15 +298,39 @@ pub fn sys_fallocate( return Err(AxError::OperationNotPermitted); } let cur_len = f.inner().backend()?.location().len()?; - if end > cur_len && seals & F_SEAL_GROW != 0 { + if !keep_size && end > cur_len && seals & F_SEAL_GROW != 0 { return Err(AxError::OperationNotPermitted); } } let inner = f.inner(); let file = inner.access(FileFlags::WRITE)?; let old_len = file.location().len()?; - memfd_check_resize_seals(&f_like, old_len, old_len.max(end))?; - file.set_len(old_len.max(end))?; + let new_len = if keep_size { old_len } else { old_len.max(end) }; + memfd_check_resize_seals(&f_like, old_len, new_len)?; + + match operation { + 0 => { + if new_len != old_len { + file.set_len(new_len)?; + } + } + FALLOC_FL_ZERO_RANGE => { + if new_len != old_len { + file.set_len(new_len)?; + } + let zero_end = old_len.min(end); + if (offset as u64) < zero_end { + write_zero_range(file, offset as u64, zero_end - offset as u64)?; + } + } + FALLOC_FL_PUNCH_HOLE => { + let zero_end = old_len.min(end); + if (offset as u64) < zero_end { + write_zero_range(file, offset as u64, zero_end - offset as u64)?; + } + } + _ => unreachable!(), + } Ok(0) } diff --git a/os/StarryOS/kernel/src/syscall/io_mpx/epoll.rs b/os/StarryOS/kernel/src/syscall/io_mpx/epoll.rs index be5ba4dfab..60477b0e90 100644 --- a/os/StarryOS/kernel/src/syscall/io_mpx/epoll.rs +++ b/os/StarryOS/kernel/src/syscall/io_mpx/epoll.rs @@ -109,6 +109,15 @@ pub fn sys_epoll_create1(flags: u32) -> AxResult { .map(|fd| fd as isize) } +/// Implements legacy `epoll_create`, validating size before creating an epoll fd without flags. +#[cfg(target_arch = "x86_64")] +pub fn sys_epoll_create(size: i32) -> AxResult { + if size <= 0 { + return Err(AxError::InvalidInput); + } + sys_epoll_create1(0) +} + pub fn sys_epoll_ctl( epfd: i32, op: u32, @@ -118,6 +127,10 @@ pub fn sys_epoll_ctl( let epoll = Epoll::from_fd(epfd)?; debug!("sys_epoll_ctl <= epfd: {epfd}, op: {op}, fd: {fd}"); + if epfd == fd { + return Err(AxError::InvalidInput); + } + let parse_event = || -> AxResult<(u32, EpollEvent, EpollFlags)> { let event = read_epoll_event(event)?; let raw_events = event.events; @@ -214,14 +227,25 @@ pub fn sys_epoll_pwait( sigmask: UserConstPtr, sigsetsize: usize, ) -> AxResult { - let timeout = match timeout { - -1 => None, - t if t >= 0 => Some(Duration::from_millis(t as u64)), - _ => return Err(AxError::InvalidInput), + let timeout = if timeout < 0 { + None + } else { + Some(Duration::from_millis(timeout as u64)) }; do_epoll_wait(epfd, events, maxevents, timeout, sigmask, sigsetsize) } +/// Implements legacy `epoll_wait` as an x86_64 wrapper around `epoll_pwait`. +#[cfg(target_arch = "x86_64")] +pub fn sys_epoll_wait( + epfd: i32, + events: UserPtr, + maxevents: i32, + timeout: i32, +) -> AxResult { + sys_epoll_pwait(epfd, events, maxevents, timeout, 0usize.into(), 0) +} + pub fn sys_epoll_pwait2( epfd: i32, events: UserPtr, diff --git a/os/StarryOS/kernel/src/syscall/mod.rs b/os/StarryOS/kernel/src/syscall/mod.rs index cd408a6072..ba2bc38f3b 100644 --- a/os/StarryOS/kernel/src/syscall/mod.rs +++ b/os/StarryOS/kernel/src/syscall/mod.rs @@ -359,6 +359,8 @@ pub fn handle_syscall(uctx: &mut UserContext) { uctx.arg4().into(), uctx.arg5().into(), ), + #[cfg(target_arch = "x86_64")] + Sysno::epoll_create => sys_epoll_create(uctx.arg0() as _), Sysno::epoll_create1 => sys_epoll_create1(uctx.arg0() as _), Sysno::epoll_ctl => sys_epoll_ctl( uctx.arg0() as _, @@ -366,6 +368,13 @@ pub fn handle_syscall(uctx: &mut UserContext) { uctx.arg2() as _, uctx.arg3().into(), ), + #[cfg(target_arch = "x86_64")] + Sysno::epoll_wait => sys_epoll_wait( + uctx.arg0() as _, + uctx.arg1().into(), + uctx.arg2() as _, + uctx.arg3() as _, + ), Sysno::epoll_pwait => sys_epoll_pwait( uctx.arg0() as _, uctx.arg1().into(), @@ -565,6 +574,17 @@ pub fn handle_syscall(uctx: &mut UserContext) { uctx.arg3() as _, uctx.arg4() as _, ), + Sysno::set_mempolicy => { + sys_set_mempolicy(uctx.arg0() as _, uctx.arg1() as _, uctx.arg2() as _) + } + Sysno::mbind => sys_mbind( + uctx.arg0() as _, + uctx.arg1() as _, + uctx.arg2() as _, + uctx.arg3() as _, + uctx.arg4() as _, + uctx.arg5() as _, + ), // task management Sysno::clone => sys_clone( diff --git a/os/StarryOS/kernel/src/syscall/task/ctl.rs b/os/StarryOS/kernel/src/syscall/task/ctl.rs index a5eede5205..b0f16345ce 100644 --- a/os/StarryOS/kernel/src/syscall/task/ctl.rs +++ b/os/StarryOS/kernel/src/syscall/task/ctl.rs @@ -13,6 +13,49 @@ use crate::{ const CAPABILITY_VERSION_3: u32 = 0x20080522; const PERSONALITY_GET: u32 = 0xffff_ffff; const PR_THP_DISABLE_EXCEPT_ADVISED: usize = 1 << 1; +const MPOL_DEFAULT: i32 = 0; +const MPOL_PREFERRED: i32 = 1; +const MPOL_BIND: i32 = 2; +const MPOL_INTERLEAVE: i32 = 3; +const MPOL_LOCAL: i32 = 4; +const MPOL_PREFERRED_MANY: i32 = 5; +const MPOL_WEIGHTED_INTERLEAVE: i32 = 6; +const MPOL_F_NODE: usize = 1 << 0; +const MPOL_F_ADDR: usize = 1 << 1; +const MPOL_F_MEMS_ALLOWED: usize = 1 << 2; +const MPOL_F_STATIC_NODES: i32 = 1 << 15; +const MPOL_F_RELATIVE_NODES: i32 = 1 << 14; +const MPOL_MODE_FLAGS: i32 = MPOL_F_STATIC_NODES | MPOL_F_RELATIVE_NODES; +const MPOL_MF_STRICT: u32 = 1 << 0; +const MPOL_MF_MOVE: u32 = 1 << 1; +const MPOL_MF_MOVE_ALL: u32 = 1 << 2; +const MPOL_MF_VALID: u32 = MPOL_MF_STRICT | MPOL_MF_MOVE | MPOL_MF_MOVE_ALL; + +/// Split a NUMA policy mode from its optional mode flags. +fn parse_mempolicy_mode(mode: i32) -> AxResult { + if mode < 0 { + return Err(AxError::InvalidInput); + } + let policy = mode & !MPOL_MODE_FLAGS; + match policy { + MPOL_DEFAULT + | MPOL_PREFERRED + | MPOL_BIND + | MPOL_INTERLEAVE + | MPOL_LOCAL + | MPOL_PREFERRED_MANY + | MPOL_WEIGHTED_INTERLEAVE => Ok(policy), + _ => Err(AxError::InvalidInput), + } +} + +/// Validate a user nodemask pointer when a policy consumes it. +fn check_nodemask(nodemask: *const usize, maxnode: usize) -> AxResult<()> { + if !nodemask.is_null() && maxnode > 0 { + nodemask.vm_read()?; + } + Ok(()) +} /// Validate the cap header and return the target pid (0 means self). fn validate_cap_header(header_ptr: *mut __user_cap_header_struct) -> AxResult { @@ -99,14 +142,121 @@ pub fn sys_personality(persona: usize) -> AxResult { Ok(old as isize) } +/// Get NUMA memory policy for a thread. +/// +/// For single-node systems (which StarryOS currently models), all memory +/// is on node 0 with default policy MPOL_DEFAULT. +/// +/// Arguments: +/// - policy: output pointer for policy mode (MPOL_DEFAULT, MPOL_BIND, etc.) +/// - nodemask: output pointer for node mask bitmap +/// - maxnode: size of nodemask bitmap in bits +/// - addr: memory address to query (when MPOL_F_ADDR flag set) +/// - flags: MPOL_F_NODE, MPOL_F_ADDR, MPOL_F_MEMS_ALLOWED +/// +/// Returns 0 on success, or -errno on error. pub fn sys_get_mempolicy( - _policy: *mut i32, - _nodemask: *mut usize, - _maxnode: usize, + policy: *mut i32, + nodemask: *mut usize, + maxnode: usize, _addr: usize, - _flags: usize, + flags: usize, +) -> AxResult { + debug!( + "sys_get_mempolicy <= policy: {:?}, nodemask: {:?}, maxnode: {}, flags: {:#x}", + policy, nodemask, maxnode, flags + ); + + if flags & !(MPOL_F_NODE | MPOL_F_ADDR | MPOL_F_MEMS_ALLOWED) != 0 { + return Err(AxError::InvalidInput); + } + if flags & MPOL_F_MEMS_ALLOWED != 0 && flags != MPOL_F_MEMS_ALLOWED { + return Err(AxError::InvalidInput); + } + if flags & MPOL_F_NODE != 0 && flags & MPOL_F_ADDR == 0 { + return Err(AxError::InvalidInput); + } + + // StarryOS models one NUMA node, so every query resolves to node 0. + if flags & MPOL_F_MEMS_ALLOWED != 0 { + if !nodemask.is_null() && maxnode > 0 { + nodemask.vm_write(1usize)?; + } + return Ok(0); + } + + if flags & MPOL_F_NODE != 0 { + if !policy.is_null() { + policy.vm_write(0i32)?; + } + return Ok(0); + } + + if !policy.is_null() { + policy.vm_write(MPOL_DEFAULT)?; + } + + if !nodemask.is_null() && maxnode > 0 { + nodemask.vm_write(1usize)?; + } + + Ok(0) +} + +/// Set NUMA memory policy for a thread. +/// +/// For single-node systems, this is a no-op that always succeeds. +/// +/// Arguments: +/// - mode: policy mode (MPOL_DEFAULT, MPOL_BIND, MPOL_INTERLEAVE, etc.) +/// - nodemask: node mask bitmap +/// - maxnode: size of nodemask bitmap in bits +/// +/// Returns 0 on success. +pub fn sys_set_mempolicy(mode: i32, nodemask: *const usize, maxnode: usize) -> AxResult { + debug!("sys_set_mempolicy <= mode: {}", mode); + + let policy = parse_mempolicy_mode(mode)?; + if policy != MPOL_DEFAULT { + check_nodemask(nodemask, maxnode)?; + } + + // Single-node system: accept valid policies and ignore placement. + Ok(0) +} + +/// Bind memory range to NUMA nodes. +/// +/// For single-node systems, this is a no-op that always succeeds. +/// +/// Arguments: +/// - addr: start address of memory range +/// - len: length of memory range +/// - mode: policy mode +/// - nodemask: node mask bitmap +/// - maxnode: size of nodemask bitmap in bits +/// - flags: MPOL_MF_STRICT, MPOL_MF_MOVE, MPOL_MF_MOVE_ALL +/// +/// Returns 0 on success. +pub fn sys_mbind( + addr: usize, + len: usize, + mode: i32, + nodemask: *const usize, + maxnode: usize, + flags: u32, ) -> AxResult { - warn!("Dummy get_mempolicy called"); + debug!("sys_mbind <= mode: {}", mode); + + let policy = parse_mempolicy_mode(mode)?; + if addr & 0xfff != 0 || len == 0 || flags & !MPOL_MF_VALID != 0 { + return Err(AxError::InvalidInput); + } + if policy != MPOL_DEFAULT { + check_nodemask(nodemask, maxnode)?; + } + + // Single-node system: accept valid bindings and ignore placement. Ok(0) } diff --git a/os/StarryOS/kernel/src/task/signal.rs b/os/StarryOS/kernel/src/task/signal.rs index 3f2336057f..972e80bd43 100644 --- a/os/StarryOS/kernel/src/task/signal.rs +++ b/os/StarryOS/kernel/src/task/signal.rs @@ -444,11 +444,11 @@ pub fn with_blocked_signals( let sig = &curr.as_thread().signal; let old_blocked = blocked.map(|set| sig.set_blocked(set)); - f().inspect(|_| { - if let Some(old) = old_blocked { - sig.set_blocked(old); - } - }) + let result = f(); + if let Some(old) = old_blocked { + sig.set_blocked(old); + } + result } pub(super) fn send_signal_thread_inner(task: &TaskInner, thr: &Thread, sig: SignalInfo) { diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-epoll-compat-entrypoints/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/bugfix/bug-epoll-compat-entrypoints/c/CMakeLists.txt new file mode 100644 index 0000000000..2b3d225e9f --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/bugfix/bug-epoll-compat-entrypoints/c/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.20) +project(bug-epoll-compat-entrypoints C) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +add_executable(bug-epoll-compat-entrypoints src/main.c) +target_compile_options(bug-epoll-compat-entrypoints PRIVATE -Wall -Wextra -Werror) +install(TARGETS bug-epoll-compat-entrypoints RUNTIME DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-epoll-compat-entrypoints/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/bugfix/bug-epoll-compat-entrypoints/c/src/main.c new file mode 100644 index 0000000000..23bdc4b8d4 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/bugfix/bug-epoll-compat-entrypoints/c/src/main.c @@ -0,0 +1,79 @@ +/* + * bug-epoll-compat-entrypoints: x86_64 programs may use old epoll_create + * and epoll_wait syscall numbers instead of epoll_create1/epoll_pwait. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +static int failures = 0; + +static void expect_errno(long ret, int expected, const char *what) +{ + if (ret == -1 && errno == expected) { + printf("PASS: %s errno=%d\n", what, errno); + } else { + printf("FAIL: %s expected errno=%d got ret=%ld errno=%d (%s)\n", + what, expected, ret, errno, strerror(errno)); + failures++; + } +} + +int main(void) +{ +#ifndef SYS_epoll_create + printf("SKIP: old epoll syscall numbers unavailable on this arch\n"); + return 0; +#else + int epfd = epoll_create(8); + if (epfd < 0) { + printf("FAIL: epoll_create(8) errno=%d (%s)\n", errno, strerror(errno)); + return 1; + } + + errno = 0; + expect_errno(epoll_create(0), EINVAL, "epoll_create rejects size=0"); + + int pipefd[2]; + if (pipe(pipefd) != 0) { + printf("FAIL: pipe errno=%d (%s)\n", errno, strerror(errno)); + return 1; + } + struct epoll_event ev = { + .events = EPOLLIN, + .data.fd = pipefd[0], + }; + if (epoll_ctl(epfd, EPOLL_CTL_ADD, pipefd[0], &ev) != 0) { + printf("FAIL: epoll_ctl add errno=%d (%s)\n", errno, strerror(errno)); + return 1; + } + write(pipefd[1], "x", 1); + + struct epoll_event out; + errno = 0; + long ret = syscall(SYS_epoll_wait, epfd, &out, 1, 0); + if (ret == 1 && out.data.fd == pipefd[0] && (out.events & EPOLLIN)) { + printf("PASS: old epoll_wait syscall returns ready event\n"); + } else { + printf("FAIL: old epoll_wait ret=%ld events=%#x data=%d errno=%d (%s)\n", + ret, out.events, out.data.fd, errno, strerror(errno)); + failures++; + } + + errno = 0; + expect_errno(epoll_ctl(epfd, EPOLL_CTL_ADD, epfd, &ev), + EINVAL, "epoll_ctl rejects adding epoll fd to itself"); + + close(pipefd[0]); + close(pipefd[1]); + close(epfd); + if (failures == 0) { + printf("bug-epoll-compat-entrypoints: passed\n"); + } + return failures == 0 ? 0 : 1; +#endif +} diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-fallocate-zero-punch/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/bugfix/bug-fallocate-zero-punch/c/CMakeLists.txt new file mode 100644 index 0000000000..54888a6c57 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/bugfix/bug-fallocate-zero-punch/c/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.20) +project(bug-fallocate-zero-punch C) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +add_executable(bug-fallocate-zero-punch src/main.c) +target_compile_options(bug-fallocate-zero-punch PRIVATE -Wall -Wextra -Werror) +install(TARGETS bug-fallocate-zero-punch RUNTIME DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-fallocate-zero-punch/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/bugfix/bug-fallocate-zero-punch/c/src/main.c new file mode 100644 index 0000000000..532426d5bc --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/bugfix/bug-fallocate-zero-punch/c/src/main.c @@ -0,0 +1,75 @@ +/* + * bug-fallocate-zero-punch: zero-range and punch-hole modes must zero + * existing bytes instead of rejecting the supported mode. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +#ifndef FALLOC_FL_KEEP_SIZE +#define FALLOC_FL_KEEP_SIZE 0x01 +#endif +#ifndef FALLOC_FL_PUNCH_HOLE +#define FALLOC_FL_PUNCH_HOLE 0x02 +#endif +#ifndef FALLOC_FL_ZERO_RANGE +#define FALLOC_FL_ZERO_RANGE 0x10 +#endif + +static int failures = 0; + +static void expect(int cond, const char *what) +{ + if (cond) { + printf("PASS: %s\n", what); + } else { + printf("FAIL: %s errno=%d (%s)\n", what, errno, strerror(errno)); + failures++; + } +} + +int main(void) +{ + char tmpl[] = "/tmp/bug-fallocate-XXXXXX"; + int fd = mkstemp(tmpl); + if (fd < 0) { + printf("FAIL: mkstemp errno=%d (%s)\n", errno, strerror(errno)); + return 1; + } + + expect(write(fd, "abcdefghij", 10) == 10, "write fixture"); + expect(fallocate(fd, FALLOC_FL_ZERO_RANGE, 2, 4) == 0, + "FALLOC_FL_ZERO_RANGE succeeds"); + + char buf[11] = {0}; + expect(pread(fd, buf, 10, 0) == 10, "read after ZERO_RANGE"); + expect(memcmp(buf, "ab\0\0\0\0ghij", 10) == 0, + "ZERO_RANGE zeros the requested bytes"); + + expect(pwrite(fd, "1234567890", 10, 0) == 10, "restore fixture"); + expect(fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, 0, 5) == 0, + "PUNCH_HOLE|KEEP_SIZE succeeds"); + + struct stat st; + expect(fstat(fd, &st) == 0 && st.st_size == 10, + "PUNCH_HOLE keeps file size"); + const char expected[10] = { + 0, 0, 0, 0, 0, '6', '7', '8', '9', '0', + }; + memset(buf, 0, sizeof(buf)); + expect(pread(fd, buf, 10, 0) == 10, "read after PUNCH_HOLE"); + expect(memcmp(buf, expected, sizeof(expected)) == 0, + "PUNCH_HOLE zeros the requested bytes"); + + close(fd); + unlink(tmpl); + if (failures == 0) { + printf("bug-fallocate-zero-punch: passed\n"); + } + return failures == 0 ? 0 : 1; +} diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-mempolicy-validation/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/bugfix/bug-mempolicy-validation/c/CMakeLists.txt new file mode 100644 index 0000000000..ede9ee7561 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/bugfix/bug-mempolicy-validation/c/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.20) +project(bug-mempolicy-validation C) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +add_executable(bug-mempolicy-validation src/main.c) +target_compile_options(bug-mempolicy-validation PRIVATE -Wall -Wextra -Werror) +install(TARGETS bug-mempolicy-validation RUNTIME DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/bug-mempolicy-validation/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/bugfix/bug-mempolicy-validation/c/src/main.c new file mode 100644 index 0000000000..a6f4944003 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/bugfix/bug-mempolicy-validation/c/src/main.c @@ -0,0 +1,137 @@ +/* + * bug-mempolicy-validation: NUMA policy syscalls must reject invalid + * arguments while still accepting single-node no-op policies. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +#ifndef SYS_get_mempolicy +#if defined(__x86_64__) +#define SYS_get_mempolicy 239 +#elif defined(__aarch64__) || defined(__riscv) || defined(__loongarch__) +#define SYS_get_mempolicy 236 +#endif +#endif + +#ifndef SYS_set_mempolicy +#if defined(__x86_64__) +#define SYS_set_mempolicy 238 +#elif defined(__aarch64__) || defined(__riscv) || defined(__loongarch__) +#define SYS_set_mempolicy 237 +#endif +#endif + +#ifndef SYS_mbind +#if defined(__x86_64__) +#define SYS_mbind 237 +#elif defined(__aarch64__) || defined(__riscv) || defined(__loongarch__) +#define SYS_mbind 235 +#endif +#endif + +#define MPOL_DEFAULT 0 +#define MPOL_BIND 2 +#define MPOL_F_NODE (1UL << 0) +#define MPOL_F_ADDR (1UL << 1) +#define MPOL_F_MEMS_ALLOWED (1UL << 2) + +static int failures = 0; + +static void expect_ret(long ret, long expected, const char *what) +{ + if (ret == expected) { + printf("PASS: %s ret=%ld\n", what, ret); + } else { + printf("FAIL: %s expected=%ld got=%ld errno=%d (%s)\n", + what, expected, ret, errno, strerror(errno)); + failures++; + } +} + +static void expect_errno(long ret, int expected, const char *what) +{ + if (ret == -1 && errno == expected) { + printf("PASS: %s errno=%d\n", what, errno); + } else { + printf("FAIL: %s expected errno=%d got ret=%ld errno=%d (%s)\n", + what, expected, ret, errno, strerror(errno)); + failures++; + } +} + +int main(void) +{ +#if !defined(SYS_get_mempolicy) || !defined(SYS_set_mempolicy) || !defined(SYS_mbind) + printf("SKIP: mempolicy syscall numbers unavailable\n"); + return 0; +#else + int policy = -1; + unsigned long node0 = 1; + unsigned long mask = 0; + long pagesize = sysconf(_SC_PAGESIZE); + void *page = mmap(NULL, (size_t)pagesize, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (page == MAP_FAILED) { + printf("FAIL: mmap errno=%d (%s)\n", errno, strerror(errno)); + return 1; + } + + errno = 0; + expect_ret(syscall(SYS_get_mempolicy, &policy, &mask, 8, NULL, 0), 0, + "get_mempolicy default"); + if (policy != MPOL_DEFAULT || mask != 1UL) { + printf("FAIL: expected default policy/node0, got policy=%d mask=%lu\n", + policy, mask); + failures++; + } + + errno = 0; + expect_ret(syscall(SYS_set_mempolicy, MPOL_BIND, &node0, 8), 0, + "set_mempolicy MPOL_BIND node0"); + + errno = 0; + expect_ret(syscall(SYS_mbind, page, (unsigned long)pagesize, + MPOL_BIND, &node0, 8, 0), + 0, "mbind MPOL_BIND node0"); + + errno = 0; + expect_errno(syscall(SYS_get_mempolicy, &policy, &mask, 8, NULL, MPOL_F_NODE), + EINVAL, "get_mempolicy rejects MPOL_F_NODE without MPOL_F_ADDR"); + + errno = 0; + expect_errno(syscall(SYS_get_mempolicy, &policy, &mask, 8, NULL, + MPOL_F_MEMS_ALLOWED | MPOL_F_ADDR), + EINVAL, "get_mempolicy rejects combined MPOL_F_MEMS_ALLOWED"); + + errno = 0; + expect_errno(syscall(SYS_set_mempolicy, 99, NULL, 0), + EINVAL, "set_mempolicy rejects unknown mode"); + + errno = 0; + expect_errno(syscall(SYS_set_mempolicy, MPOL_BIND, (void *)(uintptr_t)1, 8), + EFAULT, "set_mempolicy rejects bad nodemask"); + + errno = 0; + expect_errno(syscall(SYS_mbind, (char *)page + 1, (unsigned long)pagesize, + MPOL_DEFAULT, NULL, 0, 0), + EINVAL, "mbind rejects unaligned address"); + + errno = 0; + expect_errno(syscall(SYS_mbind, page, (unsigned long)pagesize, + MPOL_BIND, (void *)(uintptr_t)1, 8, 0), + EFAULT, "mbind rejects bad nodemask"); + + munmap(page, (size_t)pagesize); + + if (failures == 0) { + printf("bug-mempolicy-validation: passed\n"); + } + return failures == 0 ? 0 : 1; +#endif +} diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-aarch64.toml b/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-aarch64.toml index c7331890ef..91725bd01a 100644 --- a/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-aarch64.toml +++ b/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-aarch64.toml @@ -18,6 +18,8 @@ to_bin = true shell_prefix = "root@starry:" test_commands = [ "/usr/bin/bug-futex-wait-wake", + "/usr/bin/bug-fallocate-zero-punch", + "/usr/bin/bug-mempolicy-validation", "/usr/bin/bug-lseek-negative-einval", "/usr/bin/bug-mkdir-empty-path", "/usr/bin/bug-packet-arping", diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-loongarch64.toml b/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-loongarch64.toml index af3b72e5a5..8e62da20f4 100644 --- a/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-loongarch64.toml +++ b/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-loongarch64.toml @@ -20,6 +20,8 @@ to_bin = true shell_prefix = "root@starry:" test_commands = [ "/usr/bin/bug-futex-wait-wake", + "/usr/bin/bug-fallocate-zero-punch", + "/usr/bin/bug-mempolicy-validation", "/usr/bin/bug-lseek-negative-einval", "/usr/bin/bug-mkdir-empty-path", "/usr/bin/bug-packet-arping", diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-riscv64.toml b/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-riscv64.toml index fbb38c2640..49b58d53a5 100644 --- a/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-riscv64.toml +++ b/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-riscv64.toml @@ -18,6 +18,8 @@ to_bin = true shell_prefix = "root@starry:" test_commands = [ "/usr/bin/bug-futex-wait-wake", + "/usr/bin/bug-fallocate-zero-punch", + "/usr/bin/bug-mempolicy-validation", "/usr/bin/bug-faccessat2-validation", "/usr/bin/bug-fchmodat2-flags", "/usr/bin/bug-linkat-flags-symlink", diff --git a/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-x86_64.toml index bae1ba0a16..94b331af04 100644 --- a/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-x86_64.toml +++ b/test-suit/starryos/normal/qemu-smp1/bugfix/qemu-x86_64.toml @@ -17,6 +17,9 @@ shell_prefix = "root@starry:" test_commands = [ "/usr/bin/bug-futex-wait-wake", "/usr/bin/bug-epoll-wait-user-buffer-race", + "/usr/bin/bug-epoll-compat-entrypoints", + "/usr/bin/bug-fallocate-zero-punch", + "/usr/bin/bug-mempolicy-validation", "/usr/bin/bug-getcwd-syscall-return", "/usr/bin/bug-lseek-negative-einval", "/usr/bin/bug-mkdir-empty-path", diff --git a/test-suit/starryos/normal/qemu-smp1/c-regression/test-file-io-edge/c/prebuild.sh b/test-suit/starryos/normal/qemu-smp1/c-regression/test-file-io-edge/c/prebuild.sh deleted file mode 100755 index 6e808ab6f6..0000000000 --- a/test-suit/starryos/normal/qemu-smp1/c-regression/test-file-io-edge/c/prebuild.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -eu - -apk add binutils gcc musl-dev diff --git a/test-suit/starryos/normal/qemu-smp1/c-regression/test-memfd-seals/c/prebuild.sh b/test-suit/starryos/normal/qemu-smp1/c-regression/test-memfd-seals/c/prebuild.sh deleted file mode 100755 index 6e808ab6f6..0000000000 --- a/test-suit/starryos/normal/qemu-smp1/c-regression/test-memfd-seals/c/prebuild.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -eu - -apk add binutils gcc musl-dev diff --git a/test-suit/starryos/normal/qemu-smp1/c-regression/test-mremap/c/prebuild.sh b/test-suit/starryos/normal/qemu-smp1/c-regression/test-mremap/c/prebuild.sh deleted file mode 100755 index 6e808ab6f6..0000000000 --- a/test-suit/starryos/normal/qemu-smp1/c-regression/test-mremap/c/prebuild.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -eu - -apk add binutils gcc musl-dev diff --git a/test-suit/starryos/normal/qemu-smp1/c-regression/test-msync/c/prebuild.sh b/test-suit/starryos/normal/qemu-smp1/c-regression/test-msync/c/prebuild.sh deleted file mode 100644 index 6e808ab6f6..0000000000 --- a/test-suit/starryos/normal/qemu-smp1/c-regression/test-msync/c/prebuild.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -eu - -apk add binutils gcc musl-dev diff --git a/test-suit/starryos/normal/qemu-smp1/c-regression/test-netlink-genl/c/prebuild.sh b/test-suit/starryos/normal/qemu-smp1/c-regression/test-netlink-genl/c/prebuild.sh deleted file mode 100755 index 6e808ab6f6..0000000000 --- a/test-suit/starryos/normal/qemu-smp1/c-regression/test-netlink-genl/c/prebuild.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -eu - -apk add binutils gcc musl-dev diff --git a/test-suit/starryos/normal/qemu-smp1/c-regression/test-netlink-rtnetlink/c/prebuild.sh b/test-suit/starryos/normal/qemu-smp1/c-regression/test-netlink-rtnetlink/c/prebuild.sh deleted file mode 100755 index 6e808ab6f6..0000000000 --- a/test-suit/starryos/normal/qemu-smp1/c-regression/test-netlink-rtnetlink/c/prebuild.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -eu - -apk add binutils gcc musl-dev diff --git a/test-suit/starryos/normal/qemu-smp1/c-regression/test-netlink-uevent/c/prebuild.sh b/test-suit/starryos/normal/qemu-smp1/c-regression/test-netlink-uevent/c/prebuild.sh deleted file mode 100755 index 6e808ab6f6..0000000000 --- a/test-suit/starryos/normal/qemu-smp1/c-regression/test-netlink-uevent/c/prebuild.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -eu - -apk add binutils gcc musl-dev diff --git a/test-suit/starryos/normal/qemu-smp1/c-regression/test-rmdir-nonempty/c/prebuild.sh b/test-suit/starryos/normal/qemu-smp1/c-regression/test-rmdir-nonempty/c/prebuild.sh deleted file mode 100644 index a7657c8e61..0000000000 --- a/test-suit/starryos/normal/qemu-smp1/c-regression/test-rmdir-nonempty/c/prebuild.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -eu - -apk add gcc musl-dev diff --git a/test-suit/starryos/normal/qemu-smp1/evdev/test-evdev-prop-bits/c/prebuild.sh b/test-suit/starryos/normal/qemu-smp1/evdev/test-evdev-prop-bits/c/prebuild.sh deleted file mode 100644 index 808c448e8c..0000000000 --- a/test-suit/starryos/normal/qemu-smp1/evdev/test-evdev-prop-bits/c/prebuild.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -eu - -apk add binutils gcc musl-dev linux-headers diff --git a/test-suit/starryos/normal/qemu-smp1/evdev/test-evdev-prop-bits/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/evdev/test-evdev-prop-bits/c/src/main.c index d4e46bb52b..96cdf85280 100644 --- a/test-suit/starryos/normal/qemu-smp1/evdev/test-evdev-prop-bits/c/src/main.c +++ b/test-suit/starryos/normal/qemu-smp1/evdev/test-evdev-prop-bits/c/src/main.c @@ -15,19 +15,41 @@ #include #include -#include -#include #include #include #include #include +/* Keep this test independent from linux-headers in the staging root. */ +#define EV_REL 0x02 +#define EV_ABS 0x03 +#define EV_MAX 0x1f +#define ABS_X 0x00 +#define ABS_MAX 0x3f + #ifndef INPUT_PROP_POINTER #define INPUT_PROP_POINTER 0x00 #endif #ifndef INPUT_PROP_DIRECT #define INPUT_PROP_DIRECT 0x01 #endif +#ifndef EVIOCGBIT +#define EVIOCGBIT(ev, len) _IOC(_IOC_READ, 'E', 0x20 + (ev), len) +#endif +#ifndef EVIOCGPROP +#define EVIOCGPROP(len) _IOC(_IOC_READ, 'E', 0x09, len) +#endif +#ifndef EVIOCGABS +struct input_absinfo { + int value; + int minimum; + int maximum; + int fuzz; + int flat; + int resolution; +}; +#define EVIOCGABS(axis) _IOC(_IOC_READ, 'E', 0x40 + (axis), sizeof(struct input_absinfo)) +#endif #ifndef EVIOCGABS_SMALL #define EVIOCGABS_SMALL(axis) _IOC(_IOC_READ, 'E', 0x40 + (axis), sizeof(int)) #endif diff --git a/test-suit/starryos/normal/qemu-smp1/syscall/test-epoll/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/syscall/test-epoll/c/src/main.c index e6d808ba85..baa0cba260 100644 --- a/test-suit/starryos/normal/qemu-smp1/syscall/test-epoll/c/src/main.c +++ b/test-suit/starryos/normal/qemu-smp1/syscall/test-epoll/c/src/main.c @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,17 @@ int main(void) int epfd = epoll_create1(0); CHECK(epfd >= 0, "PART1: epoll_create1(0) 成功"); if (epfd >= 0) close(epfd); + +#ifdef __x86_64__ + epfd = epoll_create(16); + CHECK(epfd >= 0, "PART1: epoll_create(size>0) 成功"); + if (epfd >= 0) close(epfd); + + CHECK_ERR(epoll_create(0), EINVAL, + "PART1: epoll_create(size=0) 返回 EINVAL"); + CHECK_ERR(epoll_create(-1), EINVAL, + "PART1: epoll_create(size<0) 返回 EINVAL"); +#endif } /* ================================================================ @@ -85,6 +97,10 @@ int main(void) "PART3: data.fd 与注册时一致"); } + nready = syscall(SYS_epoll_pwait, epfd, events, 4, -2, NULL, 0); + CHECK_RET(nready, 1, + "PART3: epoll_pwait timeout<-1 在已有事件时返回事件"); + char buf[16]; read(fds[0], buf, 5); @@ -352,6 +368,30 @@ int main(void) close(fds[1]); } + errno = 0; + CHECK(epoll_ctl(epfd, EPOLL_CTL_ADD, epfd, &ev) == -1 && + errno == EINVAL, + "PART10: epoll_ctl 拒绝把 epoll fd 加入自身"); + + errno = 0; + CHECK(epoll_wait(epfd, events, 0, 0) == -1 && + errno == EINVAL, + "PART10: epoll_wait maxevents=0 返回 EINVAL"); + + errno = 0; +#ifdef SYS_epoll_wait + CHECK(syscall(SYS_epoll_wait, epfd, NULL, 1, 0) == -1 && + errno == EFAULT, + "PART10: epoll_wait NULL events 返回 EFAULT"); +#elif defined(SYS_epoll_pwait) + CHECK(syscall(SYS_epoll_pwait, epfd, NULL, 1, 0, NULL, 0) == -1 && + errno == EFAULT, + "PART10: epoll_wait NULL events 返回 EFAULT"); +#else + printf(" SKIP | %s:%d | PART10: raw epoll wait syscall is not defined\n", + __FILE__, __LINE__); +#endif + close(epfd); } diff --git a/test-suit/starryos/normal/qemu-smp1/syscall/test-fallocate/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/syscall/test-fallocate/c/src/main.c index 715ae614ec..7c4b7cc0ad 100644 --- a/test-suit/starryos/normal/qemu-smp1/syscall/test-fallocate/c/src/main.c +++ b/test-suit/starryos/normal/qemu-smp1/syscall/test-fallocate/c/src/main.c @@ -4,6 +4,16 @@ #include #include +#ifndef FALLOC_FL_KEEP_SIZE +#define FALLOC_FL_KEEP_SIZE 0x01 +#endif +#ifndef FALLOC_FL_PUNCH_HOLE +#define FALLOC_FL_PUNCH_HOLE 0x02 +#endif +#ifndef FALLOC_FL_ZERO_RANGE +#define FALLOC_FL_ZERO_RANGE 0x10 +#endif + /* * fallocate 对比测试: * Linux 作为正确基线,验证 StarryOS 的 errno 优先级和 fd 类型语义。 @@ -302,6 +312,11 @@ int main(void) check_ret_or_err(ret, 1, ok, __FILE__, __LINE__, "FALLOC_FL_KEEP_SIZE: 期望 0 或 EOPNOTSUPP"); } + if (ret == 0) { + struct stat st; + CHECK_RET(fstat(fd, &st), 0, "KEEP_SIZE 后 fstat 成功"); + CHECK(st.st_size == 0, "KEEP_SIZE 不扩展文件大小"); + } close(fd); unlink(tmpl); @@ -329,7 +344,36 @@ int main(void) } /* ================================================================ - * 18. mode = FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE + * 18. mode = FALLOC_FL_ZERO_RANGE + * 支持时应把已有范围清零,并按需要扩展文件。 + * ================================================================ */ + { + char tmpl[] = "/tmp/test-fallocate-XXXXXX"; + int fd = mkstemp(tmpl); + CHECK(fd >= 0, "mkstemp 应成功"); + + CHECK_RET(write(fd, "abcdefghij", 10), 10, "写入 10 字节"); + + errno = 0; + long ret = (long)call_fallocate(fd, FALLOC_FL_ZERO_RANGE, 2, 4); + { + const int ok[] = { EOPNOTSUPP }; + check_ret_or_err(ret, 1, ok, __FILE__, __LINE__, + "FALLOC_FL_ZERO_RANGE: 期望 0 或 EOPNOTSUPP"); + } + if (ret == 0) { + char buf[11] = {0}; + CHECK_RET(pread(fd, buf, 10, 0), 10, "ZERO_RANGE 后读回完整内容"); + CHECK(memcmp(buf, "ab\0\0\0\0ghij", 10) == 0, + "ZERO_RANGE 把指定范围清零"); + } + + close(fd); + unlink(tmpl); + } + + /* ================================================================ + * 19. mode = FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE * Linux: 返回 0 (tmpfs/ext4 支持) 或 EOPNOTSUPP (不支持) * ================================================================ */ { @@ -348,13 +392,25 @@ int main(void) check_ret_or_err(ret, 1, ok, __FILE__, __LINE__, "FALLOC_FL_PUNCH_HOLE|KEEP_SIZE: 期望 0 或 EOPNOTSUPP"); } + if (ret == 0) { + char buf[11] = {0}; + const char expected[10] = { + 0, 0, 0, 0, 0, '6', '7', '8', '9', '0', + }; + struct stat st; + CHECK_RET(fstat(fd, &st), 0, "PUNCH_HOLE 后 fstat 成功"); + CHECK(st.st_size == 10, "PUNCH_HOLE|KEEP_SIZE 不改变文件大小"); + CHECK_RET(pread(fd, buf, 10, 0), 10, "PUNCH_HOLE 后读回完整内容"); + CHECK(memcmp(buf, expected, sizeof(expected)) == 0, + "PUNCH_HOLE 把洞范围读为零"); + } close(fd); unlink(tmpl); } /* ================================================================ - * 19. 超大 offset (2^60) — Linux 返回 EFBIG + * 20. 超大 offset (2^60) — Linux 返回 EFBIG * ================================================================ */ { char tmpl[] = "/tmp/test-fallocate-XXXXXX"; diff --git a/test-suit/starryos/normal/qemu-smp1/syscall/test-get-mempolicy/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/syscall/test-get-mempolicy/c/CMakeLists.txt new file mode 100644 index 0000000000..8948ec45a5 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/syscall/test-get-mempolicy/c/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.20) +project(test-get-mempolicy C) +set(APP_NAME test-get-mempolicy) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +add_executable(${APP_NAME} src/main.c) +target_include_directories(${APP_NAME} PRIVATE src ${CMAKE_CURRENT_SOURCE_DIR}/../../common) +target_compile_options(${APP_NAME} PRIVATE -Wall -Wextra -Werror) +install(TARGETS ${APP_NAME} RUNTIME DESTINATION usr/bin/starry-test-suit) diff --git a/test-suit/starryos/normal/qemu-smp1/syscall/test-get-mempolicy/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/syscall/test-get-mempolicy/c/src/main.c new file mode 100644 index 0000000000..9d80c4c8c1 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/syscall/test-get-mempolicy/c/src/main.c @@ -0,0 +1,75 @@ +#include "test_framework.h" + +#include +#include +#include + +#ifndef SYS_get_mempolicy +#if defined(__x86_64__) +#define SYS_get_mempolicy 239 +#elif defined(__aarch64__) +#define SYS_get_mempolicy 236 +#elif defined(__riscv) +#define SYS_get_mempolicy 236 +#elif defined(__loongarch__) +#define SYS_get_mempolicy 236 +#endif +#endif + +#define MPOL_DEFAULT 0 +#define MPOL_F_NODE (1UL << 0) +#define MPOL_F_ADDR (1UL << 1) +#define MPOL_F_MEMS_ALLOWED (1UL << 2) + +static long call_get_mempolicy(int *policy, unsigned long *nodemask, + unsigned long maxnode, void *addr, + unsigned long flags) +{ + return syscall(SYS_get_mempolicy, policy, nodemask, maxnode, addr, flags); +} + +int main(void) +{ +#ifndef SYS_get_mempolicy + TEST_START("get_mempolicy unavailable on this libc"); + printf(" SKIP | SYS_get_mempolicy is not defined\n"); + TEST_DONE(); +#else + TEST_START("get_mempolicy single-node semantics"); + + int policy = -1; + unsigned long mask = 0; + CHECK_RET(call_get_mempolicy(&policy, &mask, 8, NULL, 0), 0, + "get_mempolicy returns current policy"); + CHECK(policy == MPOL_DEFAULT, "default policy is MPOL_DEFAULT"); + CHECK(mask == 1UL, "single-node nodemask contains node 0"); + + policy = -1; + CHECK_RET(call_get_mempolicy(&policy, NULL, 0, NULL, + MPOL_F_NODE | MPOL_F_ADDR), + 0, "MPOL_F_NODE|MPOL_F_ADDR returns node id"); + CHECK(policy == 0, "address belongs to node 0"); + + mask = 0; + CHECK_RET(call_get_mempolicy(NULL, &mask, 8, NULL, MPOL_F_MEMS_ALLOWED), + 0, "MPOL_F_MEMS_ALLOWED returns allowed node mask"); + CHECK(mask == 1UL, "allowed node mask contains node 0"); + + CHECK_RET(call_get_mempolicy(NULL, NULL, 0, NULL, 0), 0, + "NULL outputs are accepted when nothing is written"); + CHECK_RET(call_get_mempolicy(NULL, &mask, 0, NULL, 0), 0, + "maxnode=0 skips nodemask write"); + + CHECK_ERR(call_get_mempolicy(&policy, &mask, 8, NULL, 0x100), EINVAL, + "unknown get_mempolicy flags return EINVAL"); + CHECK_ERR(call_get_mempolicy(&policy, &mask, 8, NULL, MPOL_F_NODE), EINVAL, + "MPOL_F_NODE without MPOL_F_ADDR returns EINVAL"); + CHECK_ERR(call_get_mempolicy(&policy, &mask, 8, NULL, + MPOL_F_MEMS_ALLOWED | MPOL_F_ADDR), + EINVAL, "MPOL_F_MEMS_ALLOWED cannot be combined"); + CHECK_ERR(call_get_mempolicy(NULL, (void *)(uintptr_t)1, 8, NULL, 0), + EFAULT, "bad nodemask pointer returns EFAULT"); + + TEST_DONE(); +#endif +} diff --git a/test-suit/starryos/normal/qemu-smp1/syscall/test-io-cancel/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/syscall/test-io-cancel/c/src/main.c index 3a40e3ef32..c5cb20075d 100644 --- a/test-suit/starryos/normal/qemu-smp1/syscall/test-io-cancel/c/src/main.c +++ b/test-suit/starryos/normal/qemu-smp1/syscall/test-io-cancel/c/src/main.c @@ -1,9 +1,15 @@ #include "test_framework.h" +#include #include #include #include +#define IOCB_CMD_POLL 5 +#ifndef ECANCELED +#define ECANCELED 125 +#endif + typedef unsigned long aio_context_t; struct io_event { @@ -33,7 +39,7 @@ int main(void) TEST_START("io_cancel syscall compatibility"); aio_context_t ctx = 0; - CHECK_RET(syscall(SYS_io_setup, 2, &ctx), 0, + CHECK_RET(syscall(SYS_io_setup, 8, &ctx), 0, "create context for io_cancel"); if (ctx != 0) { @@ -44,6 +50,39 @@ int main(void) CHECK_ERR(syscall(SYS_io_cancel, ctx, &cb, &event), EINVAL, "io_cancel reports unavailable for completed operations"); + + int pipefd[2] = {-1, -1}; + CHECK_RET(pipe(pipefd), 0, "create pipe for queued cancel test"); + if (pipefd[0] >= 0) { + struct iocb poll_cb[6]; + struct iocb *list[6]; + memset(poll_cb, 0, sizeof(poll_cb)); + for (int i = 0; i < 6; i++) { + poll_cb[i].aio_data = 0x7000u + (uint64_t)i; + poll_cb[i].aio_lio_opcode = IOCB_CMD_POLL; + poll_cb[i].aio_fildes = (uint32_t)pipefd[0]; + poll_cb[i].aio_buf = POLLIN; + list[i] = &poll_cb[i]; + } + + CHECK_RET(syscall(SYS_io_submit, ctx, 6, list), 6, + "queue more poll requests than AIO workers"); + memset(&event, 0, sizeof(event)); + CHECK_RET(syscall(SYS_io_cancel, ctx, &poll_cb[5], &event), 0, + "io_cancel cancels a request still waiting in the queue"); + CHECK(event.data == 0x7005u, + "io_cancel returns canceled request data"); + CHECK(event.obj == (uint64_t)(uintptr_t)&poll_cb[5], + "io_cancel returns canceled iocb pointer"); + CHECK(event.res == -ECANCELED && event.res2 == 0, + "io_cancel reports ECANCELED in the result event"); + CHECK_ERR(syscall(SYS_io_cancel, ctx, &poll_cb[5], &event), EINVAL, + "io_cancel rejects an already canceled request"); + + CHECK_RET(close(pipefd[0]), 0, "close cancel pipe read end"); + CHECK_RET(close(pipefd[1]), 0, "close cancel pipe write end"); + } + CHECK_RET(syscall(SYS_io_destroy, ctx), 0, "destroy io_cancel context"); } diff --git a/test-suit/starryos/normal/qemu-smp1/syscall/test-io-destroy/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/syscall/test-io-destroy/c/src/main.c index da1ff0a9c3..dfdef4ddeb 100644 --- a/test-suit/starryos/normal/qemu-smp1/syscall/test-io-destroy/c/src/main.c +++ b/test-suit/starryos/normal/qemu-smp1/syscall/test-io-destroy/c/src/main.c @@ -1,10 +1,29 @@ #include "test_framework.h" +#include +#include #include #include +#define IOCB_CMD_POLL 5 + typedef unsigned long aio_context_t; +struct iocb { + uint64_t aio_data; + uint32_t aio_key; + uint32_t aio_rw_flags; + uint16_t aio_lio_opcode; + int16_t aio_reqprio; + uint32_t aio_fildes; + uint64_t aio_buf; + uint64_t aio_nbytes; + int64_t aio_offset; + uint64_t aio_reserved2; + uint32_t aio_flags; + uint32_t aio_resfd; +}; + int main(void) { TEST_START("io_destroy syscall semantics"); @@ -20,6 +39,34 @@ int main(void) "io_destroy rejects a destroyed context"); } + ctx = 0; + CHECK_RET(syscall(SYS_io_setup, 4, &ctx), 0, + "create context for io_destroy with in-flight poll"); + int pipefd[2] = {-1, -1}; + CHECK_RET(pipe(pipefd), 0, "create pipe for blocking aio poll"); + if (ctx != 0 && pipefd[0] >= 0) { + struct iocb cb; + memset(&cb, 0, sizeof(cb)); + cb.aio_data = 0x5151; + cb.aio_lio_opcode = IOCB_CMD_POLL; + cb.aio_fildes = (uint32_t)pipefd[0]; + cb.aio_buf = POLLIN; + struct iocb *list[1] = {&cb}; + + CHECK_RET(syscall(SYS_io_submit, ctx, 1, list), 1, + "queue blocking poll request before io_destroy"); + CHECK_RET(syscall(SYS_io_destroy, ctx), 0, + "io_destroy drains in-flight blocking request"); + CHECK_ERR(syscall(SYS_io_destroy, ctx), EINVAL, + "destroyed in-flight context is removed"); + } + if (pipefd[0] >= 0) { + CHECK_RET(close(pipefd[0]), 0, "close poll pipe read end"); + } + if (pipefd[1] >= 0) { + CHECK_RET(close(pipefd[1]), 0, "close poll pipe write end"); + } + CHECK_ERR(syscall(SYS_io_destroy, 0), EINVAL, "io_destroy rejects context 0"); CHECK_ERR(syscall(SYS_io_destroy, 0x7fffffffUL), EINVAL, diff --git a/test-suit/starryos/normal/qemu-smp1/syscall/test-io-getevents/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/syscall/test-io-getevents/c/src/main.c index 3fcebb6181..f8d76e8077 100644 --- a/test-suit/starryos/normal/qemu-smp1/syscall/test-io-getevents/c/src/main.c +++ b/test-suit/starryos/normal/qemu-smp1/syscall/test-io-getevents/c/src/main.c @@ -2,12 +2,25 @@ #include #include +#include #include #define IOCB_CMD_NOOP 6 +#define AIO_RING_MAGIC 0xa10a10a1u typedef unsigned long aio_context_t; +struct aio_ring { + uint32_t id; + uint32_t nr; + uint32_t head; + uint32_t tail; + uint32_t magic; + uint32_t compat_features; + uint32_t incompat_features; + uint32_t header_length; +}; + struct io_event { uint64_t data; uint64_t obj; @@ -39,34 +52,75 @@ int main(void) "create context for io_getevents"); if (ctx != 0) { + struct aio_ring *ring = (struct aio_ring *)(uintptr_t)ctx; + CHECK(ring->magic == AIO_RING_MAGIC, + "io_getevents context points to a readable AIO ring"); + CHECK(ring->head == 0 && ring->tail == 0, + "new AIO ring has no completions"); + struct io_event events[2]; memset(events, 0, sizeof(events)); + struct timespec zero_timeout = {0, 0}; CHECK_RET(syscall(SYS_io_getevents, ctx, 0, 1, events, NULL), 0, "io_getevents returns 0 when no completions are queued"); + CHECK_RET(syscall(SYS_io_getevents, ctx, 1, 1, events, &zero_timeout), 0, + "io_getevents honors zero timeout when min_nr waits"); + CHECK_RET(syscall(SYS_io_getevents, ctx, 0, 0, NULL, NULL), 0, + "io_getevents with nr 0 returns 0 without touching events"); CHECK_ERR(syscall(SYS_io_getevents, ctx, 2, 1, events, NULL), EINVAL, "io_getevents rejects min_nr greater than nr"); CHECK_ERR(syscall(SYS_io_getevents, ctx, -1, 1, events, NULL), EINVAL, "io_getevents rejects negative min_nr"); + CHECK_ERR(syscall(SYS_io_getevents, ctx, 0, -1, events, NULL), EINVAL, + "io_getevents rejects negative nr"); - struct iocb cb; - memset(&cb, 0, sizeof(cb)); - cb.aio_data = 0x2222; - cb.aio_lio_opcode = IOCB_CMD_NOOP; - struct iocb *list[1] = {&cb}; + struct iocb cb[2]; + memset(cb, 0, sizeof(cb)); + cb[0].aio_data = 0x2222; + cb[0].aio_lio_opcode = IOCB_CMD_NOOP; + cb[1].aio_data = 0x3333; + cb[1].aio_lio_opcode = IOCB_CMD_NOOP; + struct iocb *list[2] = {&cb[0], &cb[1]}; - CHECK_RET(syscall(SYS_io_submit, ctx, 1, list), 1, - "queue one noop completion"); + CHECK_RET(syscall(SYS_io_submit, ctx, 2, list), 2, + "queue two noop requests"); memset(events, 0, sizeof(events)); - CHECK_RET(syscall(SYS_io_getevents, ctx, 1, 1, events, NULL), 1, - "io_getevents returns queued completion"); - CHECK(events[0].data == 0x2222, "io_getevents preserves completion data"); - CHECK(events[0].obj == (uint64_t)(uintptr_t)&cb, - "io_getevents preserves iocb pointer"); - CHECK(events[0].res == 0 && events[0].res2 == 0, - "io_getevents reports noop success"); + CHECK_RET(syscall(SYS_io_getevents, ctx, 1, 2, events, NULL), 2, + "io_getevents returns multiple queued completions"); + int saw_first = 0; + int saw_second = 0; + for (int i = 0; i < 2; i++) { + if (events[i].data == 0x2222 && + events[i].obj == (uint64_t)(uintptr_t)&cb[0] && + events[i].res == 0 && events[i].res2 == 0) { + saw_first = 1; + } + if (events[i].data == 0x3333 && + events[i].obj == (uint64_t)(uintptr_t)&cb[1] && + events[i].res == 0 && events[i].res2 == 0) { + saw_second = 1; + } + } + CHECK(saw_first && saw_second, + "io_getevents returns both noop completions with correct data"); + CHECK(ring->head == ring->tail, + "io_getevents advances AIO ring head after draining"); CHECK_RET(syscall(SYS_io_getevents, ctx, 0, 1, events, NULL), 0, "io_getevents drains completions"); + memset(&cb[0], 0, sizeof(cb[0])); + cb[0].aio_data = 0x4444; + cb[0].aio_lio_opcode = IOCB_CMD_NOOP; + list[0] = &cb[0]; + CHECK_RET(syscall(SYS_io_submit, ctx, 1, list), 1, + "queue one noop for invalid events pointer test"); + CHECK_ERR(syscall(SYS_io_getevents, ctx, 1, 1, (void *)(uintptr_t)1, NULL), EFAULT, + "io_getevents rejects invalid output events pointer"); + memset(events, 0, sizeof(events)); + CHECK_RET(syscall(SYS_io_getevents, ctx, 1, 1, events, NULL), 1, + "completion remains available after failed user copy"); + CHECK(events[0].data == 0x4444, "failed copy does not lose completion"); + CHECK_RET(syscall(SYS_io_destroy, ctx), 0, "destroy io_getevents context"); } diff --git a/test-suit/starryos/normal/qemu-smp1/syscall/test-io-pgetevents/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/syscall/test-io-pgetevents/c/src/main.c index d64a7080a6..6296730b8e 100644 --- a/test-suit/starryos/normal/qemu-smp1/syscall/test-io-pgetevents/c/src/main.c +++ b/test-suit/starryos/normal/qemu-smp1/syscall/test-io-pgetevents/c/src/main.c @@ -1,10 +1,13 @@ #include "test_framework.h" +#include #include #include +#include #include #define IOCB_CMD_NOOP 6 +#define KERNEL_SIGSET_SIZE 8 typedef unsigned long aio_context_t; @@ -30,6 +33,11 @@ struct iocb { uint32_t aio_resfd; }; +struct aio_sigset_arg { + const sigset_t *sigmask; + size_t sigsetsize; +}; + int main(void) { TEST_START("io_pgetevents syscall semantics"); @@ -41,10 +49,17 @@ int main(void) if (ctx != 0) { struct io_event event; memset(&event, 0, sizeof(event)); + struct timespec zero_timeout = {0, 0}; CHECK_RET(syscall(SYS_io_pgetevents, ctx, 0, 1, &event, NULL, 0), 0, "io_pgetevents returns 0 when no completions are queued"); + CHECK_RET(syscall(SYS_io_pgetevents, ctx, 1, 1, &event, &zero_timeout, 0), 0, + "io_pgetevents honors zero timeout when min_nr waits"); + CHECK_RET(syscall(SYS_io_pgetevents, ctx, 0, 0, NULL, NULL, 0), 0, + "io_pgetevents with nr 0 returns 0"); CHECK_ERR(syscall(SYS_io_pgetevents, ctx, 2, 1, &event, NULL, 0), EINVAL, "io_pgetevents rejects min_nr greater than nr"); + CHECK_ERR(syscall(SYS_io_pgetevents, ctx, -1, 1, &event, NULL, 0), EINVAL, + "io_pgetevents rejects negative min_nr"); struct iocb cb; memset(&cb, 0, sizeof(cb)); @@ -63,6 +78,31 @@ int main(void) CHECK(event.res == 0 && event.res2 == 0, "io_pgetevents reports noop success"); + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, SIGUSR1); + struct aio_sigset_arg sigarg = { + .sigmask = &mask, + .sigsetsize = KERNEL_SIGSET_SIZE, + }; + memset(&event, 0, sizeof(event)); + CHECK_RET(syscall(SYS_io_pgetevents, ctx, 0, 1, &event, &zero_timeout, &sigarg), 0, + "io_pgetevents accepts a valid raw sigmask wrapper"); + + sigarg.sigmask = NULL; + sigarg.sigsetsize = KERNEL_SIGSET_SIZE; + CHECK_RET(syscall(SYS_io_pgetevents, ctx, 0, 1, &event, &zero_timeout, &sigarg), 0, + "io_pgetevents accepts wrapper with NULL inner sigmask"); + + sigarg.sigmask = &mask; + sigarg.sigsetsize = KERNEL_SIGSET_SIZE - 1; + CHECK_ERR(syscall(SYS_io_pgetevents, ctx, 0, 1, &event, &zero_timeout, &sigarg), EINVAL, + "io_pgetevents rejects invalid sigsetsize"); + CHECK_ERR(syscall(SYS_io_pgetevents, ctx, 0, 1, &event, &zero_timeout, + (void *)(uintptr_t)1), + EFAULT, + "io_pgetevents rejects invalid sigmask wrapper pointer"); + CHECK_RET(syscall(SYS_io_destroy, ctx), 0, "destroy io_pgetevents context"); } diff --git a/test-suit/starryos/normal/qemu-smp1/syscall/test-io-setup/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/syscall/test-io-setup/c/src/main.c index 9469f54c68..f130bac161 100644 --- a/test-suit/starryos/normal/qemu-smp1/syscall/test-io-setup/c/src/main.c +++ b/test-suit/starryos/normal/qemu-smp1/syscall/test-io-setup/c/src/main.c @@ -1,10 +1,24 @@ #include "test_framework.h" +#include #include #include +#define AIO_RING_MAGIC 0xa10a10a1u + typedef unsigned long aio_context_t; +struct aio_ring { + uint32_t id; + uint32_t nr; + uint32_t head; + uint32_t tail; + uint32_t magic; + uint32_t compat_features; + uint32_t incompat_features; + uint32_t header_length; +}; + int main(void) { TEST_START("io_setup syscall semantics"); @@ -13,6 +27,20 @@ int main(void) CHECK_RET(syscall(SYS_io_setup, 4, &ctx), 0, "io_setup creates an AIO context"); CHECK(ctx != 0, "io_setup stores a nonzero context id"); + if (ctx != 0) { + struct aio_ring *ring = (struct aio_ring *)(uintptr_t)ctx; + CHECK(ring->magic == AIO_RING_MAGIC, + "io_setup context points to a readable AIO ring"); + CHECK(ring->nr >= 4, "AIO ring exposes at least requested events"); + CHECK(ring->head == 0 && ring->tail == 0, + "new AIO ring starts empty"); + CHECK(ring->compat_features == 1, + "AIO ring exposes expected compatible features"); + CHECK(ring->incompat_features == 0, + "AIO ring exposes no incompatible features"); + CHECK(ring->header_length == sizeof(struct aio_ring), + "AIO ring header length matches userspace layout"); + } if (ctx != 0) { CHECK_RET(syscall(SYS_io_destroy, ctx), 0, "destroy context created by io_setup"); @@ -28,5 +56,25 @@ int main(void) "io_setup rejects nonzero user context slot"); CHECK(ctx == 0x1234, "failed io_setup leaves nonzero context unchanged"); + ctx = 0; + aio_context_t ctx2 = 0; + CHECK_RET(syscall(SYS_io_setup, 1, &ctx), 0, + "io_setup creates a small context"); + CHECK_RET(syscall(SYS_io_setup, 8, &ctx2), 0, + "io_setup creates a second independent context"); + CHECK(ctx != 0 && ctx2 != 0 && ctx != ctx2, + "io_setup returns distinct context handles"); + if (ctx != 0) { + CHECK_RET(syscall(SYS_io_destroy, ctx), 0, + "destroy first independent context"); + } + if (ctx2 != 0) { + CHECK_RET(syscall(SYS_io_destroy, ctx2), 0, + "destroy second independent context"); + } + + CHECK_ERR(syscall(SYS_io_setup, 4, (void *)(uintptr_t)1), EFAULT, + "io_setup rejects an invalid user context pointer"); + TEST_DONE(); } diff --git a/test-suit/starryos/normal/qemu-smp1/syscall/test-io-submit/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/syscall/test-io-submit/c/src/main.c index 83e0d29281..bfe61f699b 100644 --- a/test-suit/starryos/normal/qemu-smp1/syscall/test-io-submit/c/src/main.c +++ b/test-suit/starryos/normal/qemu-smp1/syscall/test-io-submit/c/src/main.c @@ -1,14 +1,23 @@ #include "test_framework.h" #include +#include #include +#include #include +#include #include #define TMPFILE "/tmp/starry_test_io_submit" #define IOCB_CMD_PREAD 0 #define IOCB_CMD_PWRITE 1 +#define IOCB_CMD_FSYNC 2 +#define IOCB_CMD_FDSYNC 3 +#define IOCB_CMD_POLL 5 #define IOCB_CMD_NOOP 6 +#define IOCB_CMD_PREADV 7 +#define IOCB_CMD_PWRITEV 8 +#define IOCB_FLAG_RESFD 1u typedef unsigned long aio_context_t; @@ -47,12 +56,26 @@ static int create_fixture(void) return fd; } +static struct io_event wait_one(aio_context_t ctx, const char *msg) +{ + struct io_event event; + memset(&event, 0, sizeof(event)); + CHECK_RET(syscall(SYS_io_getevents, ctx, 1, 1, &event, NULL), 1, msg); + return event; +} + +static void submit_one(aio_context_t ctx, struct iocb *cb, const char *msg) +{ + struct iocb *list[1] = {cb}; + CHECK_RET(syscall(SYS_io_submit, ctx, 1, list), 1, msg); +} + int main(void) { TEST_START("io_submit syscall semantics"); aio_context_t ctx = 0; - CHECK_RET(syscall(SYS_io_setup, 4, &ctx), 0, + CHECK_RET(syscall(SYS_io_setup, 8, &ctx), 0, "create context for io_submit"); int fd = create_fixture(); @@ -67,20 +90,16 @@ int main(void) cb.aio_buf = (uint64_t)(uintptr_t)buf; cb.aio_nbytes = 4; cb.aio_offset = 5; - struct iocb *list[1] = {&cb}; - CHECK_RET(syscall(SYS_io_submit, ctx, 1, list), 1, - "io_submit accepts one pread iocb"); + submit_one(ctx, &cb, "io_submit accepts one pread iocb"); + struct io_event event = wait_one(ctx, "io_getevents drains pread completion"); CHECK(memcmp(buf, "5678", 4) == 0, - "io_submit executes supported pread operation synchronously"); - - struct io_event event; - memset(&event, 0, sizeof(event)); - CHECK_RET(syscall(SYS_io_getevents, ctx, 1, 1, &event, NULL), 1, - "io_getevents drains submitted completion"); - CHECK(event.data == 0x1111, "completion preserves iocb data"); - CHECK(event.obj == (uint64_t)(uintptr_t)&cb, "completion preserves iocb pointer"); - CHECK(event.res == 4 && event.res2 == 0, "completion reports pread byte count"); + "io_submit pread fills the requested user buffer"); + CHECK(event.data == 0x1111, "pread completion preserves iocb data"); + CHECK(event.obj == (uint64_t)(uintptr_t)&cb, + "pread completion preserves iocb pointer"); + CHECK(event.res == 4 && event.res2 == 0, + "completion reports pread byte count"); const char write_data[] = "WXYZ"; memset(&cb, 0, sizeof(cb)); @@ -90,16 +109,14 @@ int main(void) cb.aio_buf = (uint64_t)(uintptr_t)write_data; cb.aio_nbytes = 4; cb.aio_offset = 8; - list[0] = &cb; - - CHECK_RET(lseek(fd, 1, SEEK_SET), 1, "set current offset before aio pwrite"); - CHECK_RET(syscall(SYS_io_submit, ctx, 1, list), 1, - "io_submit accepts one pwrite iocb"); - memset(&event, 0, sizeof(event)); - CHECK_RET(syscall(SYS_io_getevents, ctx, 1, 1, &event, NULL), 1, - "io_getevents drains pwrite completion"); + + CHECK_RET(lseek(fd, 1, SEEK_SET), 1, + "set current offset before aio pwrite"); + submit_one(ctx, &cb, "io_submit accepts one pwrite iocb"); + event = wait_one(ctx, "io_getevents drains pwrite completion"); CHECK(event.data == 0x2222, "pwrite completion preserves iocb data"); - CHECK(event.res == 4 && event.res2 == 0, "completion reports pwrite byte count"); + CHECK(event.res == 4 && event.res2 == 0, + "completion reports pwrite byte count"); CHECK_RET(lseek(fd, 0, SEEK_CUR), 1, "aio pwrite does not change current file offset"); memset(buf, 0, sizeof(buf)); @@ -108,10 +125,133 @@ int main(void) CHECK(memcmp(buf, "WXYZ", 4) == 0, "io_submit pwrite updates the requested range"); + char left[3] = {0}; + char right[3] = {0}; + struct iovec read_iov[2] = { + {.iov_base = left, .iov_len = 2}, + {.iov_base = right, .iov_len = 2}, + }; + memset(&cb, 0, sizeof(cb)); + cb.aio_data = 0x3333; + cb.aio_lio_opcode = IOCB_CMD_PREADV; + cb.aio_fildes = (uint32_t)fd; + cb.aio_buf = (uint64_t)(uintptr_t)read_iov; + cb.aio_nbytes = 2; + cb.aio_offset = 0; + + submit_one(ctx, &cb, "io_submit accepts one preadv iocb"); + event = wait_one(ctx, "io_getevents drains preadv completion"); + CHECK(event.data == 0x3333 && event.res == 4, + "preadv completion reports total byte count"); + CHECK(memcmp(left, "01", 2) == 0 && memcmp(right, "23", 2) == 0, + "preadv scatters bytes into both iov segments"); + + const char vec_a[] = "AB"; + const char vec_b[] = "CD"; + struct iovec write_iov[2] = { + {.iov_base = (void *)vec_a, .iov_len = 2}, + {.iov_base = (void *)vec_b, .iov_len = 2}, + }; + memset(&cb, 0, sizeof(cb)); + cb.aio_data = 0x4444; + cb.aio_lio_opcode = IOCB_CMD_PWRITEV; + cb.aio_fildes = (uint32_t)fd; + cb.aio_buf = (uint64_t)(uintptr_t)write_iov; + cb.aio_nbytes = 2; + cb.aio_offset = 12; + + submit_one(ctx, &cb, "io_submit accepts one pwritev iocb"); + event = wait_one(ctx, "io_getevents drains pwritev completion"); + CHECK(event.data == 0x4444 && event.res == 4, + "pwritev completion reports total byte count"); + memset(buf, 0, sizeof(buf)); + CHECK_RET(syscall(SYS_pread64, fd, buf, 4, 12), 4, + "read back aio pwritev bytes"); + CHECK(memcmp(buf, "ABCD", 4) == 0, + "pwritev gathers bytes from both iov segments"); + + memset(&cb, 0, sizeof(cb)); + cb.aio_data = 0x5555; + cb.aio_lio_opcode = IOCB_CMD_FSYNC; + cb.aio_fildes = (uint32_t)fd; + submit_one(ctx, &cb, "io_submit accepts fsync iocb"); + event = wait_one(ctx, "io_getevents drains fsync completion"); + CHECK(event.data == 0x5555 && event.res == 0 && event.res2 == 0, + "fsync completion reports success"); + memset(&cb, 0, sizeof(cb)); - cb.aio_lio_opcode = IOCB_CMD_NOOP; - list[0] = &cb; - CHECK_RET(syscall(SYS_io_submit, ctx, 0, list), 0, + cb.aio_data = 0x6666; + cb.aio_lio_opcode = IOCB_CMD_FDSYNC; + cb.aio_fildes = (uint32_t)fd; + submit_one(ctx, &cb, "io_submit accepts fdatasync iocb"); + event = wait_one(ctx, "io_getevents drains fdatasync completion"); + CHECK(event.data == 0x6666 && event.res == 0 && event.res2 == 0, + "fdatasync completion reports success"); + + int pipefd[2] = {-1, -1}; + CHECK_RET(pipe(pipefd), 0, "create pipe for aio poll"); + if (pipefd[0] >= 0) { + CHECK_RET(write(pipefd[1], "P", 1), 1, + "make pipe readable before aio poll"); + memset(&cb, 0, sizeof(cb)); + cb.aio_data = 0x7777; + cb.aio_lio_opcode = IOCB_CMD_POLL; + cb.aio_fildes = (uint32_t)pipefd[0]; + cb.aio_buf = POLLIN; + submit_one(ctx, &cb, "io_submit accepts poll iocb"); + event = wait_one(ctx, "io_getevents drains poll completion"); + CHECK(event.data == 0x7777 && (event.res & POLLIN) != 0, + "poll completion reports readable pipe"); + CHECK_RET(close(pipefd[0]), 0, "close aio poll read end"); + CHECK_RET(close(pipefd[1]), 0, "close aio poll write end"); + } + + int efd = eventfd(0, EFD_NONBLOCK); + CHECK(efd >= 0, "create eventfd for AIO resfd notification"); + if (efd >= 0) { + uint64_t counter = 0; + memset(&cb, 0, sizeof(cb)); + cb.aio_data = 0x8888; + cb.aio_lio_opcode = IOCB_CMD_NOOP; + cb.aio_flags = IOCB_FLAG_RESFD; + cb.aio_resfd = (uint32_t)efd; + submit_one(ctx, &cb, "io_submit accepts noop with resfd"); + event = wait_one(ctx, "io_getevents drains resfd noop completion"); + CHECK(event.data == 0x8888 && event.res == 0, + "noop completion with resfd reports success"); + CHECK_RET(read(efd, &counter, sizeof(counter)), (ssize_t)sizeof(counter), + "eventfd receives one AIO completion notification"); + CHECK(counter == 1, "eventfd notification counter is one"); + CHECK_RET(close(efd), 0, "close aio eventfd"); + } + + struct iocb valid; + struct iocb invalid; + memset(&valid, 0, sizeof(valid)); + memset(&invalid, 0, sizeof(invalid)); + valid.aio_data = 0x9999; + valid.aio_lio_opcode = IOCB_CMD_NOOP; + invalid.aio_lio_opcode = 0xffff; + struct iocb *batch[2] = {&valid, &invalid}; + CHECK_RET(syscall(SYS_io_submit, ctx, 2, batch), 1, + "io_submit returns partial count after first queued iocb"); + event = wait_one(ctx, "io_getevents drains partial-submit completion"); + CHECK(event.data == 0x9999 && event.obj == (uint64_t)(uintptr_t)&valid, + "partial-submit completion belongs to first iocb"); + + struct iocb *single[1] = {&invalid}; + CHECK_ERR(syscall(SYS_io_submit, ctx, 1, single), EINVAL, + "io_submit rejects unsupported opcode without partial success"); + invalid.aio_lio_opcode = IOCB_CMD_NOOP; + invalid.aio_reserved2 = 1; + CHECK_ERR(syscall(SYS_io_submit, ctx, 1, single), EINVAL, + "io_submit rejects nonzero reserved field"); + invalid.aio_reserved2 = 0; + invalid.aio_flags = 0x80000000u; + CHECK_ERR(syscall(SYS_io_submit, ctx, 1, single), EINVAL, + "io_submit rejects unknown iocb flags"); + + CHECK_RET(syscall(SYS_io_submit, ctx, 0, single), 0, "io_submit with nr 0 returns 0"); } @@ -119,6 +259,8 @@ int main(void) "io_submit rejects invalid context"); CHECK_ERR(syscall(SYS_io_submit, ctx, -1, NULL), EINVAL, "io_submit rejects negative nr"); + CHECK_ERR(syscall(SYS_io_submit, ctx, 1, (void *)(uintptr_t)1), EFAULT, + "io_submit rejects invalid iocb pointer array"); if (fd >= 0) { CHECK_RET(close(fd), 0, "close io_submit fixture"); diff --git a/test-suit/starryos/normal/qemu-smp1/syscall/test-mbind/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/syscall/test-mbind/c/CMakeLists.txt new file mode 100644 index 0000000000..e113161a3f --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/syscall/test-mbind/c/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.20) +project(test-mbind C) +set(APP_NAME test-mbind) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +add_executable(${APP_NAME} src/main.c) +target_include_directories(${APP_NAME} PRIVATE src ${CMAKE_CURRENT_SOURCE_DIR}/../../common) +target_compile_options(${APP_NAME} PRIVATE -Wall -Wextra -Werror) +install(TARGETS ${APP_NAME} RUNTIME DESTINATION usr/bin/starry-test-suit) diff --git a/test-suit/starryos/normal/qemu-smp1/syscall/test-mbind/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/syscall/test-mbind/c/src/main.c new file mode 100644 index 0000000000..dfe693a987 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/syscall/test-mbind/c/src/main.c @@ -0,0 +1,80 @@ +#include "test_framework.h" + +#include +#include +#include +#include + +#ifndef SYS_mbind +#if defined(__x86_64__) +#define SYS_mbind 237 +#elif defined(__aarch64__) +#define SYS_mbind 235 +#elif defined(__riscv) +#define SYS_mbind 235 +#elif defined(__loongarch__) +#define SYS_mbind 235 +#endif +#endif + +#define MPOL_DEFAULT 0 +#define MPOL_BIND 2 +#define MPOL_F_STATIC_NODES (1 << 15) +#define MPOL_MF_STRICT (1U << 0) +#define MPOL_MF_MOVE (1U << 1) +#define MPOL_MF_MOVE_ALL (1U << 2) + +static long call_mbind(void *addr, unsigned long len, int mode, + const unsigned long *nodemask, unsigned long maxnode, + unsigned int flags) +{ + return syscall(SYS_mbind, addr, len, mode, nodemask, maxnode, flags); +} + +int main(void) +{ +#ifndef SYS_mbind + TEST_START("mbind unavailable on this libc"); + printf(" SKIP | SYS_mbind is not defined\n"); + TEST_DONE(); +#else + TEST_START("mbind single-node acceptance"); + + long pagesize = sysconf(_SC_PAGESIZE); + CHECK(pagesize > 0, "sysconf _SC_PAGESIZE succeeds"); + + void *page = mmap(NULL, (size_t)pagesize, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + CHECK(page != MAP_FAILED, "mmap one page for mbind"); + if (page == MAP_FAILED) { + TEST_DONE(); + } + + unsigned long node0 = 1; + CHECK_RET(call_mbind(page, (unsigned long)pagesize, MPOL_DEFAULT, NULL, 0, 0), 0, + "MPOL_DEFAULT binding succeeds"); + CHECK_RET(call_mbind(page, (unsigned long)pagesize, MPOL_BIND, &node0, 8, 0), 0, + "MPOL_BIND node0 succeeds"); + CHECK_RET(call_mbind(page, (unsigned long)pagesize, + MPOL_BIND | MPOL_F_STATIC_NODES, &node0, 8, + MPOL_MF_STRICT | MPOL_MF_MOVE | MPOL_MF_MOVE_ALL), + 0, "valid mode flags and mbind flags succeed"); + + CHECK_ERR(call_mbind((char *)page + 1, (unsigned long)pagesize, + MPOL_DEFAULT, NULL, 0, 0), + EINVAL, "unaligned addr returns EINVAL"); + CHECK_ERR(call_mbind(page, 0, MPOL_DEFAULT, NULL, 0, 0), + EINVAL, "zero length returns EINVAL"); + CHECK_ERR(call_mbind(page, (unsigned long)pagesize, 99, NULL, 0, 0), + EINVAL, "unknown mode returns EINVAL"); + CHECK_ERR(call_mbind(page, (unsigned long)pagesize, MPOL_DEFAULT, + NULL, 0, 0x80000000U), + EINVAL, "unknown mbind flags return EINVAL"); + CHECK_ERR(call_mbind(page, (unsigned long)pagesize, MPOL_BIND, + (void *)(uintptr_t)1, 8, 0), + EFAULT, "bad nodemask pointer returns EFAULT"); + + munmap(page, (size_t)pagesize); + TEST_DONE(); +#endif +} diff --git a/test-suit/starryos/normal/qemu-smp1/syscall/test-set-mempolicy/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/syscall/test-set-mempolicy/c/CMakeLists.txt new file mode 100644 index 0000000000..3cc15c29df --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/syscall/test-set-mempolicy/c/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.20) +project(test-set-mempolicy C) +set(APP_NAME test-set-mempolicy) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +add_executable(${APP_NAME} src/main.c) +target_include_directories(${APP_NAME} PRIVATE src ${CMAKE_CURRENT_SOURCE_DIR}/../../common) +target_compile_options(${APP_NAME} PRIVATE -Wall -Wextra -Werror) +install(TARGETS ${APP_NAME} RUNTIME DESTINATION usr/bin/starry-test-suit) diff --git a/test-suit/starryos/normal/qemu-smp1/syscall/test-set-mempolicy/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/syscall/test-set-mempolicy/c/src/main.c new file mode 100644 index 0000000000..4a035160f1 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/syscall/test-set-mempolicy/c/src/main.c @@ -0,0 +1,70 @@ +#include "test_framework.h" + +#include +#include +#include + +#ifndef SYS_set_mempolicy +#if defined(__x86_64__) +#define SYS_set_mempolicy 238 +#elif defined(__aarch64__) +#define SYS_set_mempolicy 237 +#elif defined(__riscv) +#define SYS_set_mempolicy 237 +#elif defined(__loongarch__) +#define SYS_set_mempolicy 237 +#endif +#endif + +#define MPOL_DEFAULT 0 +#define MPOL_PREFERRED 1 +#define MPOL_BIND 2 +#define MPOL_INTERLEAVE 3 +#define MPOL_LOCAL 4 +#define MPOL_PREFERRED_MANY 5 +#define MPOL_WEIGHTED_INTERLEAVE 6 +#define MPOL_F_STATIC_NODES (1 << 15) + +static long call_set_mempolicy(int mode, const unsigned long *nodemask, + unsigned long maxnode) +{ + return syscall(SYS_set_mempolicy, mode, nodemask, maxnode); +} + +int main(void) +{ +#ifndef SYS_set_mempolicy + TEST_START("set_mempolicy unavailable on this libc"); + printf(" SKIP | SYS_set_mempolicy is not defined\n"); + TEST_DONE(); +#else + TEST_START("set_mempolicy single-node acceptance"); + + unsigned long node0 = 1; + CHECK_RET(call_set_mempolicy(MPOL_DEFAULT, NULL, 0), 0, + "MPOL_DEFAULT without nodemask succeeds"); + CHECK_RET(call_set_mempolicy(MPOL_PREFERRED, &node0, 8), 0, + "MPOL_PREFERRED with node0 succeeds"); + CHECK_RET(call_set_mempolicy(MPOL_BIND, &node0, 8), 0, + "MPOL_BIND with node0 succeeds"); + CHECK_RET(call_set_mempolicy(MPOL_INTERLEAVE, &node0, 8), 0, + "MPOL_INTERLEAVE with node0 succeeds"); + CHECK_RET(call_set_mempolicy(MPOL_LOCAL, NULL, 0), 0, + "MPOL_LOCAL succeeds"); + CHECK_RET(call_set_mempolicy(MPOL_PREFERRED_MANY, &node0, 8), 0, + "MPOL_PREFERRED_MANY with node0 succeeds"); + CHECK_RET(call_set_mempolicy(MPOL_WEIGHTED_INTERLEAVE, &node0, 8), 0, + "MPOL_WEIGHTED_INTERLEAVE with node0 succeeds"); + CHECK_RET(call_set_mempolicy(MPOL_BIND | MPOL_F_STATIC_NODES, &node0, 8), 0, + "mode flags are accepted with a valid policy"); + + CHECK_ERR(call_set_mempolicy(-1, NULL, 0), EINVAL, + "negative policy mode returns EINVAL"); + CHECK_ERR(call_set_mempolicy(99, NULL, 0), EINVAL, + "unknown policy mode returns EINVAL"); + CHECK_ERR(call_set_mempolicy(MPOL_BIND, (void *)(uintptr_t)1, 8), EFAULT, + "bad nodemask pointer returns EFAULT"); + + TEST_DONE(); +#endif +} diff --git a/test-suit/starryos/normal/qemu-smp1/test-aarch64-boot-smoke/c/prebuild.sh b/test-suit/starryos/normal/qemu-smp1/test-aarch64-boot-smoke/c/prebuild.sh deleted file mode 100755 index df13fc9b49..0000000000 --- a/test-suit/starryos/normal/qemu-smp1/test-aarch64-boot-smoke/c/prebuild.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -eu - -apk add --no-cache binutils gcc musl-dev diff --git a/test-suit/starryos/normal/qemu-smp1/test-aarch64-cpu-feat/c/prebuild.sh b/test-suit/starryos/normal/qemu-smp1/test-aarch64-cpu-feat/c/prebuild.sh deleted file mode 100755 index 6e808ab6f6..0000000000 --- a/test-suit/starryos/normal/qemu-smp1/test-aarch64-cpu-feat/c/prebuild.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -eu - -apk add binutils gcc musl-dev diff --git a/test-suit/starryos/normal/qemu-smp1/test-aarch64-stack-size/c/prebuild.sh b/test-suit/starryos/normal/qemu-smp1/test-aarch64-stack-size/c/prebuild.sh deleted file mode 100755 index 6e808ab6f6..0000000000 --- a/test-suit/starryos/normal/qemu-smp1/test-aarch64-stack-size/c/prebuild.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -eu - -apk add binutils gcc musl-dev diff --git a/test-suit/starryos/normal/qemu-smp1/test-memfd-write-busy/c/prebuild.sh b/test-suit/starryos/normal/qemu-smp1/test-memfd-write-busy/c/prebuild.sh deleted file mode 100755 index 6e808ab6f6..0000000000 --- a/test-suit/starryos/normal/qemu-smp1/test-memfd-write-busy/c/prebuild.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -eu - -apk add binutils gcc musl-dev diff --git a/test-suit/starryos/normal/qemu-smp1/test-mt-execve/c/prebuild.sh b/test-suit/starryos/normal/qemu-smp1/test-mt-execve/c/prebuild.sh deleted file mode 100644 index cc59b76010..0000000000 --- a/test-suit/starryos/normal/qemu-smp1/test-mt-execve/c/prebuild.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -set -eu -apk add binutils gcc musl-dev diff --git a/test-suit/starryos/normal/qemu-smp1/test-sync/c/prebuild.sh b/test-suit/starryos/normal/qemu-smp1/test-sync/c/prebuild.sh deleted file mode 100644 index 6e808ab6f6..0000000000 --- a/test-suit/starryos/normal/qemu-smp1/test-sync/c/prebuild.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -eu - -apk add binutils gcc musl-dev diff --git a/test-suit/starryos/normal/qemu-smp1/test-syncfs/c/prebuild.sh b/test-suit/starryos/normal/qemu-smp1/test-syncfs/c/prebuild.sh deleted file mode 100644 index 6e808ab6f6..0000000000 --- a/test-suit/starryos/normal/qemu-smp1/test-syncfs/c/prebuild.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -eu - -apk add binutils gcc musl-dev diff --git a/test-suit/starryos/normal/qemu-smp1/udp/c/prebuild.sh b/test-suit/starryos/normal/qemu-smp1/udp/c/prebuild.sh deleted file mode 100644 index 6e808ab6f6..0000000000 --- a/test-suit/starryos/normal/qemu-smp1/udp/c/prebuild.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -set -eu - -apk add binutils gcc musl-dev