diff options
| author | Maxim Cournoyer <maxim@guixotic.coop> | 2025-10-21 23:22:24 +0900 |
|---|---|---|
| committer | Maxim Cournoyer <maxim@guixotic.coop> | 2025-10-30 16:13:03 +0900 |
| commit | e1994a021437b3fd73089c08d7e8db876fad698d (patch) | |
| tree | 4c277554d2167559e9325afc191c53e262733918 | |
| parent | 36a90a1a044e9e141da71f6ff9c7fcf68bcf3016 (diff) | |
syscalls: Add mmap support.
* guix/build/syscalls.scm (PROT_NONE, PROT_READ, PROT_WRITE, PROT_EXEC)
(PROT_SEM, MAP_SHARED, MAP_PRIVATE, MAP_FAILED)
(MS_ASYNC, MS_INVALIDATE, MS_SYNC)
(%mmap-guardian, %unmapped-bytevectors): New variables.
(unmapped-bytevector?, pump-mmap-guardian, %mmap, mmap, %munmap, munmap)
(%msync, msync): New procedures.
* guix/build/io.scm: New file.
* Makefile.am: Register it.
* tests/syscalls.scm (strace-output): New variable.
("mmap and munmap", "file->bytevector, reading", "file->bytevector, writing")
("manual munmap does not lead to double free"): New tests.
Change-Id: I19ec687899eda635559e91200dd8d98669b0e35f
| -rw-r--r-- | Makefile.am | 1 | ||||
| -rw-r--r-- | guix/build/io.scm | 58 | ||||
| -rw-r--r-- | guix/build/syscalls.scm | 112 | ||||
| -rw-r--r-- | tests/syscalls.scm | 70 |
4 files changed, 238 insertions, 3 deletions
diff --git a/Makefile.am b/Makefile.am index a4e7277d6d4..a6c2e73388f 100644 --- a/Makefile.am +++ b/Makefile.am | |||
| @@ -265,6 +265,7 @@ MODULES = \ | |||
| 265 | guix/build/kconfig.scm \ | 265 | guix/build/kconfig.scm \ |
| 266 | guix/build/linux-module-build-system.scm \ | 266 | guix/build/linux-module-build-system.scm \ |
| 267 | guix/build/store-copy.scm \ | 267 | guix/build/store-copy.scm \ |
| 268 | guix/build/io.scm \ | ||
| 268 | guix/build/json.scm \ | 269 | guix/build/json.scm \ |
| 269 | guix/build/pack.scm \ | 270 | guix/build/pack.scm \ |
| 270 | guix/build/utils.scm \ | 271 | guix/build/utils.scm \ |
diff --git a/guix/build/io.scm b/guix/build/io.scm new file mode 100644 index 00000000000..1dddbf239cb --- /dev/null +++ b/guix/build/io.scm | |||
| @@ -0,0 +1,58 @@ | |||
| 1 | ;;; GNU Guix --- Functional package management for GNU | ||
| 2 | ;;; Copyright © 2025 Maxim Cournoyer <maxim@guixotic.coop> | ||
| 3 | ;;; | ||
| 4 | ;;; This file is part of GNU Guix. | ||
| 5 | ;;; | ||
| 6 | ;;; GNU Guix is free software; you can redistribute it and/or modify it | ||
| 7 | ;;; under the terms of the GNU General Public License as published by | ||
| 8 | ;;; the Free Software Foundation; either version 3 of the License, or (at | ||
| 9 | ;;; your option) any later version. | ||
| 10 | ;;; | ||
| 11 | ;;; GNU Guix is distributed in the hope that it will be useful, but | ||
| 12 | ;;; WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 13 | ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 14 | ;;; GNU General Public License for more details. | ||
| 15 | ;;; | ||
| 16 | ;;; You should have received a copy of the GNU General Public License | ||
| 17 | ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. | ||
| 18 | |||
| 19 | (define-module (guix build io) | ||
| 20 | #:use-module (guix build syscalls) | ||
| 21 | #:use-module (ice-9 format) | ||
| 22 | #:use-module (rnrs bytevectors) | ||
| 23 | #:use-module (rnrs io ports) | ||
| 24 | #:use-module (system foreign) | ||
| 25 | #:export (file->bytevector) | ||
| 26 | ;; For convenience. | ||
| 27 | #:re-export (PROT_READ | ||
| 28 | PROT_NONE | ||
| 29 | PROT_READ | ||
| 30 | PROT_WRITE | ||
| 31 | PROT_EXEC | ||
| 32 | PROT_SEM | ||
| 33 | MAP_SHARED | ||
| 34 | MAP_PRIVATE | ||
| 35 | MAP_FAILED | ||
| 36 | munmap)) | ||
| 37 | |||
| 38 | ;;; | ||
| 39 | ;;; Memory mapped files. | ||
| 40 | ;;; | ||
| 41 | |||
| 42 | (define* (file->bytevector file #:key | ||
| 43 | (protection PROT_READ) | ||
| 44 | (flags (if (logtest PROT_WRITE protection) | ||
| 45 | MAP_SHARED | ||
| 46 | MAP_PRIVATE)) | ||
| 47 | (offset 0)) | ||
| 48 | "Return a bytevector object that is backed by a memory mapped FILE. This | ||
| 49 | avoids eagerly copying the full file contents into memory, instead letting the | ||
| 50 | kernel lazily page it in on demand. The underlying memory map is | ||
| 51 | automatically unmapped when the bytevector is no longer referenced." | ||
| 52 | (let* ((mode (format #f "rb~:[~;+~]" (and (logtest PROT_WRITE protection) | ||
| 53 | (logtest MAP_SHARED flags)))) | ||
| 54 | (port (open-file file mode))) | ||
| 55 | (call-with-port port | ||
| 56 | (lambda (port) | ||
| 57 | (mmap (fileno port) (- (stat:size (stat file)) offset) | ||
| 58 | #:protection protection #:flags flags #:offset offset))))) | ||
diff --git a/guix/build/syscalls.scm b/guix/build/syscalls.scm index d40b1ae5d93..ef678754706 100644 --- a/guix/build/syscalls.scm +++ b/guix/build/syscalls.scm | |||
| @@ -42,8 +42,23 @@ | |||
| 42 | #:use-module (ice-9 regex) | 42 | #:use-module (ice-9 regex) |
| 43 | #:use-module (ice-9 match) | 43 | #:use-module (ice-9 match) |
| 44 | #:use-module (ice-9 ftw) | 44 | #:use-module (ice-9 ftw) |
| 45 | #:use-module (ice-9 threads) | 45 | #:export (PROT_NONE |
| 46 | #:export (MS_RDONLY | 46 | PROT_READ |
| 47 | PROT_WRITE | ||
| 48 | PROT_EXEC | ||
| 49 | PROT_SEM | ||
| 50 | MAP_SHARED | ||
| 51 | MAP_PRIVATE | ||
| 52 | MAP_FAILED | ||
| 53 | mmap | ||
| 54 | munmap | ||
| 55 | |||
| 56 | MS_ASYNC | ||
| 57 | MS_INVALIDATE | ||
| 58 | MS_SYNC | ||
| 59 | msync | ||
| 60 | |||
| 61 | MS_RDONLY | ||
| 47 | MS_NOSUID | 62 | MS_NOSUID |
| 48 | MS_NODEV | 63 | MS_NODEV |
| 49 | MS_NOEXEC | 64 | MS_NOEXEC |
| @@ -1108,6 +1123,99 @@ backend device." | |||
| 1108 | 1123 | ||
| 1109 | 1124 | ||
| 1110 | ;;; | 1125 | ;;; |
| 1126 | ;;; Memory maps. | ||
| 1127 | ;;; | ||
| 1128 | |||
| 1129 | ;;; Constants from <sys/mman.h> | ||
| 1130 | (define PROT_NONE #x0) ;page can not be accessed | ||
| 1131 | (define PROT_READ #x1) ;page can be read | ||
| 1132 | (define PROT_WRITE #x2) ;page can be written | ||
| 1133 | (define PROT_EXEC #x4) ;page can be executed | ||
| 1134 | (define PROT_SEM #x8) ;page can be used for atomic operations | ||
| 1135 | |||
| 1136 | (define MAP_SHARED #x01) ;share changes with other processes | ||
| 1137 | (define MAP_PRIVATE #x02) ;private copy-on-write mapping | ||
| 1138 | (define MAP_FAILED #xffffffffffffffff) ;mmap failure sentinel | ||
| 1139 | |||
| 1140 | (define %mmap | ||
| 1141 | (syscall->procedure '* "mmap" (list '* size_t int int int long))) | ||
| 1142 | |||
| 1143 | (define %mmap-guardian | ||
| 1144 | (make-guardian)) | ||
| 1145 | |||
| 1146 | (define %unmapped-bytevectors | ||
| 1147 | (make-weak-key-hash-table)) | ||
| 1148 | |||
| 1149 | (define (unmapped-bytevector? bv) | ||
| 1150 | "True if the bytevector BV was already munmap'd." | ||
| 1151 | (hashq-ref %unmapped-bytevectors bv #f)) | ||
| 1152 | |||
| 1153 | (define (pump-mmap-guardian) | ||
| 1154 | (let ((bv (%mmap-guardian))) | ||
| 1155 | (when bv | ||
| 1156 | (if (unmapped-bytevector? bv) | ||
| 1157 | (hashq-remove! %unmapped-bytevectors bv) | ||
| 1158 | (munmap bv)) | ||
| 1159 | (pump-mmap-guardian)))) | ||
| 1160 | |||
| 1161 | (add-hook! after-gc-hook pump-mmap-guardian) | ||
| 1162 | |||
| 1163 | (define* (mmap fd len #:key | ||
| 1164 | (protection PROT_READ) | ||
| 1165 | (flags (if (logtest PROT_WRITE protection) | ||
| 1166 | MAP_SHARED | ||
| 1167 | MAP_PRIVATE)) | ||
| 1168 | (offset 0)) | ||
| 1169 | "Return a bytevector to a memory-mapped region of length LEN bytes | ||
| 1170 | for the open file descriptor FD. The mapping is created with the given memory | ||
| 1171 | PROTECTION and FLAGS, biwise-or of PROT_* and MAP_* constants which | ||
| 1172 | determine whether updates are visible to other processes and/or carried | ||
| 1173 | through to the underlying file. Raise a 'system-error' exception on error. | ||
| 1174 | The memory is automatically unmapped with `munmap' when the bytevector object | ||
| 1175 | is no longer referenced." | ||
| 1176 | (let-values (((ptr err) (%mmap %null-pointer len protection flags fd offset))) | ||
| 1177 | (when (= MAP_FAILED (pointer-address ptr)) | ||
| 1178 | (throw 'system-error "mmap" "mmap ~S with len ~S: ~A" | ||
| 1179 | (list fd len (strerror err)) | ||
| 1180 | (list err))) | ||
| 1181 | (let ((bv (pointer->bytevector ptr len))) | ||
| 1182 | (%mmap-guardian bv) | ||
| 1183 | bv))) | ||
| 1184 | |||
| 1185 | (define %munmap | ||
| 1186 | (syscall->procedure int "munmap" (list '* size_t))) | ||
| 1187 | |||
| 1188 | (define (munmap bv) | ||
| 1189 | "Unmap the memory region described by BV, a bytevector object." | ||
| 1190 | (let*-values (((ptr) (bytevector->pointer bv)) | ||
| 1191 | ((len) (bytevector-length bv)) | ||
| 1192 | ((ret err) (%munmap ptr len))) | ||
| 1193 | (unless (zero? ret) | ||
| 1194 | (throw 'system-error "munmap" "munmap ~S with len ~S: ~A" | ||
| 1195 | (list ptr len (strerror err)) | ||
| 1196 | (list err))) | ||
| 1197 | (hashq-set! %unmapped-bytevectors bv #t))) | ||
| 1198 | |||
| 1199 | (define MS_ASYNC 1) ;sync memory asynchronously | ||
| 1200 | (define MS_INVALIDATE 2) ;invalidate the caches | ||
| 1201 | (define MS_SYNC 4) ;synchronous memory sync | ||
| 1202 | |||
| 1203 | (define %msync | ||
| 1204 | (syscall->procedure int "msync" (list '* size_t int))) | ||
| 1205 | |||
| 1206 | (define* (msync bv #:key (flags MS_SYNC)) | ||
| 1207 | "Flush changes made to the in-core copy of a file that was mapped into memory | ||
| 1208 | using `mmap' back to the file system." | ||
| 1209 | (let*-values (((ptr) (bytevector->pointer bv)) | ||
| 1210 | ((len) (bytevector-length bv)) | ||
| 1211 | ((ret err) (%msync ptr len flags))) | ||
| 1212 | (unless (zero? ret) | ||
| 1213 | (throw 'system-error "msync" "msync ~S with len ~S: ~A" | ||
| 1214 | (list ptr len (strerror err)) | ||
| 1215 | (list err))))) | ||
| 1216 | |||
| 1217 | |||
| 1218 | ;;; | ||
| 1111 | ;;; Random. | 1219 | ;;; Random. |
| 1112 | ;;; | 1220 | ;;; |
| 1113 | 1221 | ||
diff --git a/tests/syscalls.scm b/tests/syscalls.scm index a0483e68f08..1ea49b0acc4 100644 --- a/tests/syscalls.scm +++ b/tests/syscalls.scm | |||
| @@ -22,8 +22,11 @@ | |||
| 22 | 22 | ||
| 23 | (define-module (test-syscalls) | 23 | (define-module (test-syscalls) |
| 24 | #:use-module (guix utils) | 24 | #:use-module (guix utils) |
| 25 | #:use-module (guix build io) | ||
| 25 | #:use-module (guix build syscalls) | 26 | #:use-module (guix build syscalls) |
| 27 | #:use-module (guix build utils) | ||
| 26 | #:use-module (gnu build linux-container) | 28 | #:use-module (gnu build linux-container) |
| 29 | #:use-module (rnrs bytevectors) | ||
| 27 | #:use-module (srfi srfi-1) | 30 | #:use-module (srfi srfi-1) |
| 28 | #:use-module (srfi srfi-26) | 31 | #:use-module (srfi srfi-26) |
| 29 | #:use-module (srfi srfi-64) | 32 | #:use-module (srfi srfi-64) |
| @@ -31,7 +34,7 @@ | |||
| 31 | #:use-module (system foreign) | 34 | #:use-module (system foreign) |
| 32 | #:use-module ((ice-9 ftw) #:select (scandir)) | 35 | #:use-module ((ice-9 ftw) #:select (scandir)) |
| 33 | #:use-module (ice-9 match) | 36 | #:use-module (ice-9 match) |
| 34 | #:use-module (ice-9 threads)) | 37 | #:use-module (ice-9 textual-ports)) |
| 35 | 38 | ||
| 36 | ;; Test the (guix build syscalls) module, although there's not much that can | 39 | ;; Test the (guix build syscalls) module, although there's not much that can |
| 37 | ;; actually be tested without being root. | 40 | ;; actually be tested without being root. |
| @@ -39,6 +42,9 @@ | |||
| 39 | (define temp-file | 42 | (define temp-file |
| 40 | (string-append "t-utils-" (number->string (getpid)))) | 43 | (string-append "t-utils-" (number->string (getpid)))) |
| 41 | 44 | ||
| 45 | (define strace-output | ||
| 46 | (string-append "t-utils-strace" (number->string (getpid)))) | ||
| 47 | |||
| 42 | 48 | ||
| 43 | (test-begin "syscalls") | 49 | (test-begin "syscalls") |
| 44 | 50 | ||
| @@ -735,6 +741,68 @@ | |||
| 735 | (member (system-error-errno args) | 741 | (member (system-error-errno args) |
| 736 | (list EPERM ENOSYS))))) | 742 | (list EPERM ENOSYS))))) |
| 737 | 743 | ||
| 744 | (test-assert "mmap and munmap" | ||
| 745 | (begin | ||
| 746 | (call-with-output-file temp-file | ||
| 747 | (lambda (p) | ||
| 748 | (display "abcdefghij"))) | ||
| 749 | (let* ((len 5) | ||
| 750 | (bv (mmap (open-fdes temp-file O_RDONLY) len))) | ||
| 751 | (munmap bv)))) | ||
| 752 | |||
| 753 | (test-equal "file->bytevector, reading" | ||
| 754 | #\6 | ||
| 755 | (begin | ||
| 756 | (call-with-output-file temp-file | ||
| 757 | (lambda (p) | ||
| 758 | (display "0123456789\n" p))) | ||
| 759 | (sync) | ||
| 760 | (integer->char | ||
| 761 | (bytevector-u8-ref (file->bytevector temp-file) 6)))) | ||
| 762 | |||
| 763 | (test-equal "file->bytevector, writing" | ||
| 764 | "0000000700" | ||
| 765 | (begin | ||
| 766 | (call-with-output-file temp-file | ||
| 767 | (lambda (p) | ||
| 768 | (display "0000000000" p))) | ||
| 769 | (sync) | ||
| 770 | (let ((bv (file->bytevector temp-file | ||
| 771 | #:protection PROT_WRITE))) | ||
| 772 | |||
| 773 | (bytevector-u8-set! bv 7 (char->integer #\7)) | ||
| 774 | (msync bv)) ;ensure the file gets written | ||
| 775 | (call-with-input-file temp-file get-string-all))) | ||
| 776 | |||
| 777 | (unless (which "strace") | ||
| 778 | (test-skip 1)) | ||
| 779 | ;;; This test currently fails, due to protected items in a guardian being | ||
| 780 | ;;; dropped from weak hash tables (see: | ||
| 781 | ;;; <https://codeberg.org/guile/guile/issues/44>). | ||
| 782 | (test-expect-fail 1) | ||
| 783 | (test-equal "manual munmap does not lead to double free" | ||
| 784 | 1 ;single munmap call | ||
| 785 | (begin | ||
| 786 | (call-with-output-file temp-file | ||
| 787 | (lambda (p) | ||
| 788 | (display "something interesting\n" p))) | ||
| 789 | (sync) | ||
| 790 | (gc) | ||
| 791 | (system (string-append "strace -o " strace-output | ||
| 792 | " -p " (number->string (getpid)) | ||
| 793 | " -e trace=munmap &")) | ||
| 794 | (sleep 1) ;allow strace to start | ||
| 795 | (let ((bv (file->bytevector temp-file))) | ||
| 796 | (munmap bv)) | ||
| 797 | (gc) | ||
| 798 | (sync) | ||
| 799 | (let ((text (call-with-input-file strace-output get-string-all))) | ||
| 800 | ;; The address seen by strace is not the same as the one seen by Guile, | ||
| 801 | ;; so we can't use it in the pattern. | ||
| 802 | (length (filter (cut string-prefix? "munmap(0x" <>) | ||
| 803 | (string-split text #\newline)))))) | ||
| 804 | |||
| 738 | (test-end) | 805 | (test-end) |
| 739 | 806 | ||
| 740 | (false-if-exception (delete-file temp-file)) | 807 | (false-if-exception (delete-file temp-file)) |
| 808 | (false-if-exception (delete-file strace-output)) | ||
