gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/pkg/sentry/platform/platform.go (about) 1 // Copyright 2018 The gVisor Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package platform provides a Platform abstraction. 16 // 17 // See Platform for more information. 18 package platform 19 20 import ( 21 "fmt" 22 23 "golang.org/x/sys/unix" 24 "gvisor.dev/gvisor/pkg/abi/linux" 25 "gvisor.dev/gvisor/pkg/context" 26 "gvisor.dev/gvisor/pkg/fd" 27 "gvisor.dev/gvisor/pkg/hostarch" 28 "gvisor.dev/gvisor/pkg/seccomp" 29 "gvisor.dev/gvisor/pkg/seccomp/precompiledseccomp" 30 "gvisor.dev/gvisor/pkg/sentry/arch" 31 "gvisor.dev/gvisor/pkg/sentry/hostmm" 32 "gvisor.dev/gvisor/pkg/sentry/memmap" 33 "gvisor.dev/gvisor/pkg/usermem" 34 ) 35 36 // Platform provides abstractions for execution contexts (Context, 37 // AddressSpace). 38 type Platform interface { 39 // SupportsAddressSpaceIO returns true if AddressSpaces returned by this 40 // Platform support AddressSpaceIO methods. 41 // 42 // The value returned by SupportsAddressSpaceIO is guaranteed to remain 43 // unchanged over the lifetime of the Platform. 44 SupportsAddressSpaceIO() bool 45 46 // CooperativelySchedulesAddressSpace returns true if the Platform has a 47 // limited number of AddressSpaces, such that mm.MemoryManager.Deactivate 48 // should call AddressSpace.Release when there are no goroutines that 49 // require the mm.MemoryManager to have an active AddressSpace. 50 // 51 // The value returned by CooperativelySchedulesAddressSpace is guaranteed 52 // to remain unchanged over the lifetime of the Platform. 53 CooperativelySchedulesAddressSpace() bool 54 55 // DetectsCPUPreemption returns true if Contexts returned by the Platform 56 // can reliably return ErrContextCPUPreempted. 57 DetectsCPUPreemption() bool 58 59 // HaveGlobalMemoryBarrier returns true if the GlobalMemoryBarrier method 60 // is supported. 61 HaveGlobalMemoryBarrier() bool 62 63 // OwnsPageTables returns true if the Platform implementation manages any 64 // page tables directly (rather than via host mmap(2) etc.) As of this 65 // writing, this property is relevant because the AddressSpace interface 66 // does not support specification of memory type (cacheability), such that 67 // host FDs specifying memory types (e.g. device drivers) can only set them 68 // correctly in host-managed page tables. 69 OwnsPageTables() bool 70 71 // MapUnit returns the alignment used for optional mappings into this 72 // platform's AddressSpaces. Higher values indicate lower per-page costs 73 // for AddressSpace.MapFile. As a special case, a MapUnit of 0 indicates 74 // that the cost of AddressSpace.MapFile is effectively independent of the 75 // number of pages mapped. If MapUnit is non-zero, it must be a power-of-2 76 // multiple of hostarch.PageSize. 77 MapUnit() uint64 78 79 // MinUserAddress returns the minimum mappable address on this 80 // platform. 81 MinUserAddress() hostarch.Addr 82 83 // MaxUserAddress returns the maximum mappable address on this 84 // platform. 85 MaxUserAddress() hostarch.Addr 86 87 // NewAddressSpace returns a new memory context for this platform. 88 // 89 // If mappingsID is not nil, the platform may assume that (1) all calls 90 // to NewAddressSpace with the same mappingsID represent the same 91 // (mutable) set of mappings, and (2) the set of mappings has not 92 // changed since the last time AddressSpace.Release was called on an 93 // AddressSpace returned by a call to NewAddressSpace with the same 94 // mappingsID. 95 // 96 // If a new AddressSpace cannot be created immediately, a nil 97 // AddressSpace is returned, along with channel that is closed when 98 // the caller should retry a call to NewAddressSpace. 99 // 100 // In general, this blocking behavior only occurs when 101 // CooperativelySchedulesAddressSpace (above) returns false. 102 NewAddressSpace(mappingsID any) (AddressSpace, <-chan struct{}, error) 103 104 // NewContext returns a new execution context. 105 NewContext(context.Context) Context 106 107 // PreemptAllCPUs causes all concurrent calls to Context.Switch(), as well 108 // as the first following call to Context.Switch() for each Context, to 109 // return ErrContextCPUPreempted. 110 // 111 // PreemptAllCPUs is only supported if DetectsCPUPremption() == true. 112 // Platforms for which this does not hold may panic if PreemptAllCPUs is 113 // called. 114 PreemptAllCPUs() error 115 116 // GlobalMemoryBarrier blocks until all threads running application code 117 // (via Context.Switch) and all task goroutines "have passed through a 118 // state where all memory accesses to user-space addresses match program 119 // order between entry to and return from [GlobalMemoryBarrier]", as for 120 // membarrier(2). 121 // 122 // Preconditions: HaveGlobalMemoryBarrier() == true. 123 GlobalMemoryBarrier() error 124 125 // SeccompInfo returns seccomp-related information about this platform. 126 SeccompInfo() SeccompInfo 127 } 128 129 // NoCPUPreemptionDetection implements Platform.DetectsCPUPreemption and 130 // dependent methods for Platforms that do not support this feature. 131 type NoCPUPreemptionDetection struct{} 132 133 // DetectsCPUPreemption implements Platform.DetectsCPUPreemption. 134 func (NoCPUPreemptionDetection) DetectsCPUPreemption() bool { 135 return false 136 } 137 138 // PreemptAllCPUs implements Platform.PreemptAllCPUs. 139 func (NoCPUPreemptionDetection) PreemptAllCPUs() error { 140 panic("This platform does not support CPU preemption detection") 141 } 142 143 // UseHostGlobalMemoryBarrier implements Platform.HaveGlobalMemoryBarrier and 144 // Platform.GlobalMemoryBarrier by invoking equivalent functionality on the 145 // host. 146 type UseHostGlobalMemoryBarrier struct{} 147 148 // HaveGlobalMemoryBarrier implements Platform.HaveGlobalMemoryBarrier. 149 func (UseHostGlobalMemoryBarrier) HaveGlobalMemoryBarrier() bool { 150 return hostmm.HaveGlobalMemoryBarrier() 151 } 152 153 // GlobalMemoryBarrier implements Platform.GlobalMemoryBarrier. 154 func (UseHostGlobalMemoryBarrier) GlobalMemoryBarrier() error { 155 return hostmm.GlobalMemoryBarrier() 156 } 157 158 // UseHostProcessMemoryBarrier implements Platform.HaveGlobalMemoryBarrier and 159 // Platform.GlobalMemoryBarrier by invoking a process-local memory barrier. 160 // This is faster than UseHostGlobalMemoryBarrier, but is only appropriate for 161 // platforms for which application code executes while using the sentry's 162 // mm_struct. 163 type UseHostProcessMemoryBarrier struct{} 164 165 // HaveGlobalMemoryBarrier implements Platform.HaveGlobalMemoryBarrier. 166 func (UseHostProcessMemoryBarrier) HaveGlobalMemoryBarrier() bool { 167 // Fall back to a global memory barrier if a process-local one isn't 168 // available. 169 return hostmm.HaveProcessMemoryBarrier() || hostmm.HaveGlobalMemoryBarrier() 170 } 171 172 // GlobalMemoryBarrier implements Platform.GlobalMemoryBarrier. 173 func (UseHostProcessMemoryBarrier) GlobalMemoryBarrier() error { 174 if hostmm.HaveProcessMemoryBarrier() { 175 return hostmm.ProcessMemoryBarrier() 176 } 177 return hostmm.GlobalMemoryBarrier() 178 } 179 180 // DoesOwnPageTables implements Platform.OwnsPageTables in the positive. 181 type DoesOwnPageTables struct{} 182 183 // OwnsPageTables implements Platform.OwnsPageTables. 184 func (DoesOwnPageTables) OwnsPageTables() bool { 185 return true 186 } 187 188 // DoesNotOwnPageTables implements Platform.OwnsPageTables in the negative. 189 type DoesNotOwnPageTables struct{} 190 191 // OwnsPageTables implements Platform.OwnsPageTables. 192 func (DoesNotOwnPageTables) OwnsPageTables() bool { 193 return false 194 } 195 196 // MemoryManager represents an abstraction above the platform address space 197 // which manages memory mappings and their contents. 198 type MemoryManager interface { 199 //usermem.IO provides access to the contents of a virtual memory space. 200 usermem.IO 201 // MMap establishes a memory mapping. 202 MMap(ctx context.Context, opts memmap.MMapOpts) (hostarch.Addr, error) 203 // AddressSpace returns the AddressSpace bound to mm. 204 AddressSpace() AddressSpace 205 // FindVMAByName finds a vma with the specified name. 206 FindVMAByName(ar hostarch.AddrRange, hint string) (hostarch.Addr, uint64, error) 207 } 208 209 // Context represents the execution context for a single thread. 210 type Context interface { 211 // Switch resumes execution of the thread specified by the arch.Context64 212 // in the provided address space. This call will block while the thread 213 // is executing. 214 // 215 // If cpu is non-negative, and it is not the number of the CPU that the 216 // thread executes on, Context should return ErrContextCPUPreempted. cpu 217 // can only be non-negative if Platform.DetectsCPUPreemption() is true; 218 // Contexts from Platforms for which this does not hold may ignore cpu, or 219 // panic if cpu is non-negative. 220 // 221 // Switch may return one of the following special errors: 222 // 223 // - nil: The Context invoked a system call. 224 // 225 // - ErrContextSignal: The Context was interrupted by a signal. The 226 // returned *linux.SignalInfo contains information about the signal. If 227 // linux.SignalInfo.Signo == SIGSEGV, the returned hostarch.AccessType 228 // contains the access type of the triggering fault. The caller owns 229 // the returned SignalInfo. 230 // 231 // - ErrContextInterrupt: The Context was interrupted by a call to 232 // Interrupt(). Switch() may return ErrContextInterrupt spuriously. In 233 // particular, most implementations of Interrupt() will cause the first 234 // following call to Switch() to return ErrContextInterrupt if there is no 235 // concurrent call to Switch(). 236 // 237 // - ErrContextCPUPreempted: See the definition of that error for details. 238 Switch(ctx context.Context, mm MemoryManager, ac *arch.Context64, cpu int32) (*linux.SignalInfo, hostarch.AccessType, error) 239 240 // PullFullState() pulls a full state of the application thread. 241 // 242 // A platform can support lazy loading/restoring of a thread state 243 // which includes registers and a floating point state. 244 // 245 // For example, when the Sentry handles a system call, it may have only 246 // syscall arguments without other registers and a floating point 247 // state. And in this case, if the Sentry will need to construct a 248 // signal frame to call a signal handler, it will need to call 249 // PullFullState() to load all registers and FPU state. 250 // 251 // Preconditions: The caller must be running on the task goroutine. 252 PullFullState(as AddressSpace, ac *arch.Context64) error 253 254 // FullStateChanged() indicates that a thread state has been changed by 255 // the Sentry. This happens in case of the rt_sigreturn, execve, etc. 256 // 257 // First, it indicates that the Sentry has the full state of the thread 258 // and PullFullState() has to do nothing if it is called after 259 // FullStateChanged(). 260 // 261 // Second, it forces restoring the full state of the application 262 // thread. A platform can support lazy loading/restoring of a thread 263 // state. This means that if the Sentry has not changed a thread state, 264 // the platform may not restore it. 265 // 266 // Preconditions: The caller must be running on the task goroutine. 267 FullStateChanged() 268 269 // Interrupt interrupts a concurrent call to Switch(), causing it to return 270 // ErrContextInterrupt. 271 Interrupt() 272 273 // Release() releases any resources associated with this context. 274 Release() 275 276 // PrepareSleep() is called when the tread switches to the 277 // interruptible sleep state. 278 PrepareSleep() 279 } 280 281 // ContextError is one of the possible errors returned by Context.Switch(). 282 type ContextError struct { 283 // Err is the underlying error. 284 Err error 285 // Errno is an approximation of what type of error this is supposed to 286 // be as defined by the linux errnos. 287 Errno unix.Errno 288 } 289 290 func (e *ContextError) Error() string { 291 return e.Err.Error() 292 } 293 294 var ( 295 // ErrContextSignal is returned by Context.Switch() to indicate that the 296 // Context was interrupted by a signal. 297 ErrContextSignal = fmt.Errorf("interrupted by signal") 298 299 // ErrContextInterrupt is returned by Context.Switch() to indicate that the 300 // Context was interrupted by a call to Context.Interrupt(). 301 ErrContextInterrupt = fmt.Errorf("interrupted by platform.Context.Interrupt()") 302 303 // ErrContextCPUPreempted is returned by Context.Switch() to indicate that 304 // one of the following occurred: 305 // 306 // - The CPU executing the Context is not the CPU passed to 307 // Context.Switch(). 308 // 309 // - The CPU executing the Context may have executed another Context since 310 // the last time it executed this one; or the CPU has previously executed 311 // another Context, and has never executed this one. 312 // 313 // - Platform.PreemptAllCPUs() was called since the last return from 314 // Context.Switch(). 315 ErrContextCPUPreempted = fmt.Errorf("interrupted by CPU preemption") 316 ) 317 318 // SignalInterrupt is a signal reserved for use by implementations of 319 // Context.Interrupt(). The sentry guarantees that it will ignore delivery of 320 // this signal both to Contexts and to the sentry itself, under the assumption 321 // that they originate from races with Context.Interrupt(). 322 // 323 // NOTE(b/23420492): The Go runtime only guarantees that a small subset 324 // of signals will be always be unblocked on all threads, one of which 325 // is SIGCHLD. 326 const SignalInterrupt = linux.SIGCHLD 327 328 // AddressSpace represents a virtual address space in which a Context can 329 // execute. 330 type AddressSpace interface { 331 // MapFile creates a shared mapping of offsets fr from f at address addr. 332 // Any existing overlapping mappings are silently replaced. 333 // 334 // If precommit is true, the platform should eagerly commit resources (e.g. 335 // physical memory) to the mapping. The precommit flag is advisory and 336 // implementations may choose to ignore it. 337 // 338 // Preconditions: 339 // * addr and fr must be page-aligned. 340 // * fr.Length() > 0. 341 // * at.Any() == true. 342 // * At least one reference must be held on all pages in fr, and must 343 // continue to be held as long as pages are mapped. 344 MapFile(addr hostarch.Addr, f memmap.File, fr memmap.FileRange, at hostarch.AccessType, precommit bool) error 345 346 // Unmap unmaps the given range. 347 // 348 // Preconditions: 349 // * addr is page-aligned. 350 // * length > 0. 351 Unmap(addr hostarch.Addr, length uint64) 352 353 // Release releases this address space. After releasing, a new AddressSpace 354 // must be acquired via platform.NewAddressSpace(). 355 Release() 356 357 // PreFork() is called before creating a copy of AddressSpace. This 358 // guarantees that this address space will be in a consistent state. 359 PreFork() 360 361 // PostFork() is called after creating a copy of AddressSpace. 362 PostFork() 363 364 // AddressSpaceIO methods are supported iff the associated platform's 365 // Platform.SupportsAddressSpaceIO() == true. AddressSpaces for which this 366 // does not hold may panic if AddressSpaceIO methods are invoked. 367 AddressSpaceIO 368 } 369 370 // AddressSpaceIO supports IO through the memory mappings installed in an 371 // AddressSpace. 372 // 373 // AddressSpaceIO implementors are responsible for ensuring that address ranges 374 // are application-mappable. 375 type AddressSpaceIO interface { 376 // CopyOut copies len(src) bytes from src to the memory mapped at addr. It 377 // returns the number of bytes copied. If the number of bytes copied is < 378 // len(src), it returns a non-nil error explaining why. 379 CopyOut(addr hostarch.Addr, src []byte) (int, error) 380 381 // CopyIn copies len(dst) bytes from the memory mapped at addr to dst. 382 // It returns the number of bytes copied. If the number of bytes copied is 383 // < len(dst), it returns a non-nil error explaining why. 384 CopyIn(addr hostarch.Addr, dst []byte) (int, error) 385 386 // ZeroOut sets toZero bytes to 0, starting at addr. It returns the number 387 // of bytes zeroed. If the number of bytes zeroed is < toZero, it returns a 388 // non-nil error explaining why. 389 ZeroOut(addr hostarch.Addr, toZero uintptr) (uintptr, error) 390 391 // SwapUint32 atomically sets the uint32 value at addr to new and returns 392 // the previous value. 393 // 394 // Preconditions: addr must be aligned to a 4-byte boundary. 395 SwapUint32(addr hostarch.Addr, new uint32) (uint32, error) 396 397 // CompareAndSwapUint32 atomically compares the uint32 value at addr to 398 // old; if they are equal, the value in memory is replaced by new. In 399 // either case, the previous value stored in memory is returned. 400 // 401 // Preconditions: addr must be aligned to a 4-byte boundary. 402 CompareAndSwapUint32(addr hostarch.Addr, old, new uint32) (uint32, error) 403 404 // LoadUint32 atomically loads the uint32 value at addr and returns it. 405 // 406 // Preconditions: addr must be aligned to a 4-byte boundary. 407 LoadUint32(addr hostarch.Addr) (uint32, error) 408 } 409 410 // NoAddressSpaceIO implements AddressSpaceIO methods by panicking. 411 type NoAddressSpaceIO struct{} 412 413 // CopyOut implements AddressSpaceIO.CopyOut. 414 func (NoAddressSpaceIO) CopyOut(addr hostarch.Addr, src []byte) (int, error) { 415 panic("This platform does not support AddressSpaceIO") 416 } 417 418 // CopyIn implements AddressSpaceIO.CopyIn. 419 func (NoAddressSpaceIO) CopyIn(addr hostarch.Addr, dst []byte) (int, error) { 420 panic("This platform does not support AddressSpaceIO") 421 } 422 423 // ZeroOut implements AddressSpaceIO.ZeroOut. 424 func (NoAddressSpaceIO) ZeroOut(addr hostarch.Addr, toZero uintptr) (uintptr, error) { 425 panic("This platform does not support AddressSpaceIO") 426 } 427 428 // SwapUint32 implements AddressSpaceIO.SwapUint32. 429 func (NoAddressSpaceIO) SwapUint32(addr hostarch.Addr, new uint32) (uint32, error) { 430 panic("This platform does not support AddressSpaceIO") 431 } 432 433 // CompareAndSwapUint32 implements AddressSpaceIO.CompareAndSwapUint32. 434 func (NoAddressSpaceIO) CompareAndSwapUint32(addr hostarch.Addr, old, new uint32) (uint32, error) { 435 panic("This platform does not support AddressSpaceIO") 436 } 437 438 // LoadUint32 implements AddressSpaceIO.LoadUint32. 439 func (NoAddressSpaceIO) LoadUint32(addr hostarch.Addr) (uint32, error) { 440 panic("This platform does not support AddressSpaceIO") 441 } 442 443 // SegmentationFault is an error returned by AddressSpaceIO methods when IO 444 // fails due to access of an unmapped page, or a mapped page with insufficient 445 // permissions. 446 type SegmentationFault struct { 447 // Addr is the address at which the fault occurred. 448 Addr hostarch.Addr 449 } 450 451 // Error implements error.Error. 452 func (f SegmentationFault) Error() string { 453 return fmt.Sprintf("segmentation fault at %#x", f.Addr) 454 } 455 456 // Requirements is used to specify platform specific requirements. 457 type Requirements struct { 458 // RequiresCurrentPIDNS indicates that the sandbox has to be started in the 459 // current pid namespace. 460 RequiresCurrentPIDNS bool 461 // RequiresCapSysPtrace indicates that the sandbox has to be started with 462 // the CAP_SYS_PTRACE capability. 463 RequiresCapSysPtrace bool 464 } 465 466 // SeccompInfo represents seccomp-bpf data for a given platform. 467 type SeccompInfo interface { 468 // Variables returns a map from named variables to the value they should 469 // have with the platform as currently initialized. 470 // Variables are known only at runtime, but are not part of a platform's 471 // configuration. For example, the KVM platform having an FD representing 472 // the KVM VM is a variable: it is only known at runtime, but does not 473 // change the structure of the syscall rules. 474 // The set of variable names must be static regardless of platform 475 // configuration. 476 Variables() precompiledseccomp.Values 477 478 // ConfigKey returns a string that uniquely represents the set of 479 // configuration information from which syscall rules are derived, 480 // other than variables or CPU architecture. 481 // This should at least contain the platform name. 482 // If syscall rules are dependent on the platform's configuration, 483 // this should return a string that encapsulates the values of these 484 // configuration options. 485 // For example, if some option of the platform causes it to require a 486 // new syscall to be allowed, this option should be part of this string. 487 ConfigKey() string 488 489 // SyscallFilters returns syscalls made exclusively by this platform. 490 // `vars` maps variable names (as returned by `Variables()`) to values, 491 // and **the rules should depend on `vars`**. These will not necessarily 492 // map to the result of calling `Variables()` on the current `SeccompInfo`; 493 // during seccomp rule precompilation, these will be set to placeholder 494 // values. 495 SyscallFilters(vars precompiledseccomp.Values) seccomp.SyscallRules 496 497 // HottestSyscalls returns the list of syscall numbers that this platform 498 // calls most often, most-frequently-called first. No more than a dozen 499 // syscalls. Returning an empty or a nil slice is OK. 500 // This is used to produce a more efficient seccomp-bpf program that can 501 // check for the most frequently called syscalls first. 502 // What matters here is only the frequency at which a syscall is called, 503 // not the total amount of CPU time that is used to process it in the host 504 // kernel. 505 HottestSyscalls() []uintptr 506 } 507 508 // StaticSeccompInfo implements `SeccompInfo` for platforms which don't have 509 // any configuration or variables. 510 type StaticSeccompInfo struct { 511 // PlatformName is the platform name. 512 PlatformName string 513 514 // Filters is the platform's syscall filters. 515 Filters seccomp.SyscallRules 516 517 // HotSyscalls is the list of syscalls numbers that this platform 518 // calls most often, most-frequently-called first. 519 // See `SeccompInfo.HottestSyscalls` for more. 520 HotSyscalls []uintptr 521 } 522 523 // Variables implements `SeccompInfo.Variables`. 524 func (StaticSeccompInfo) Variables() precompiledseccomp.Values { 525 return nil 526 } 527 528 // ConfigKey implements `SeccompInfo.ConfigKey`. 529 func (s StaticSeccompInfo) ConfigKey() string { 530 return s.PlatformName 531 } 532 533 // SyscallFilters implements `SeccompInfo.SyscallFilters`. 534 func (s StaticSeccompInfo) SyscallFilters(precompiledseccomp.Values) seccomp.SyscallRules { 535 return s.Filters 536 } 537 538 // HottestSyscalls implements `SeccompInfo.HottestSyscalls`. 539 func (s StaticSeccompInfo) HottestSyscalls() []uintptr { 540 return s.HotSyscalls 541 } 542 543 // Constructor represents a platform type. 544 type Constructor interface { 545 // New returns a new platform instance. 546 // 547 // Arguments: 548 // 549 // * deviceFile - the device file (e.g. /dev/kvm for the KVM platform). 550 New(deviceFile *fd.FD) (Platform, error) 551 552 // OpenDevice opens the path to the device used by the platform. 553 // Passing in an empty string will use the default path for the device, 554 // e.g. "/dev/kvm" for the KVM platform. 555 OpenDevice(devicePath string) (*fd.FD, error) 556 557 // Requirements returns platform specific requirements. 558 Requirements() Requirements 559 560 // PrecompiledSeccompInfo returns a list of `SeccompInfo`s that is 561 // useful to precompile into the Sentry. 562 PrecompiledSeccompInfo() []SeccompInfo 563 } 564 565 // platforms contains all available platform types. 566 var platforms = map[string]Constructor{} 567 568 // Register registers a new platform type. 569 func Register(name string, platform Constructor) { 570 platforms[name] = platform 571 } 572 573 // List lists available platforms. 574 func List() (available []string) { 575 for name := range platforms { 576 available = append(available, name) 577 } 578 return 579 } 580 581 // Lookup looks up the platform constructor by name. 582 func Lookup(name string) (Constructor, error) { 583 p, ok := platforms[name] 584 if !ok { 585 return nil, fmt.Errorf("unknown platform: %v", name) 586 } 587 return p, nil 588 }