Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,20 @@ type FindVppDevice = typeof import('../../../shared/hardware/find-vpp-device')

const listInstalled = jest.fn()
const getInstalledPackageManifest = jest.fn()
// The build-time integrity gate (DOPE-539). Defaults to "intact" so the
// provisioning tests below exercise the packaging behaviour they are about; the
// refusal case overrides it explicitly.
const verifyBoardPackageIntegrity = jest.fn<{ ok: boolean; packageId?: string; reason?: string }, [string]>(() => ({
ok: true,
}))
jest.mock('../../package-manager', () => ({
formatPackageIntegrityError: (boardName: string, failure: { packageId: string; reason: string }) =>
`Board "${boardName}" is provided by the VPP package "${failure.packageId}", which no longer matches its signature: ${failure.reason}.`,
PackageManagerModule: jest.fn().mockImplementation(() => {
const port = { listInstalled, getInstalledPackageManifest }
return {
...port,
verifyBoardPackageIntegrity,
// Board lookup runs through the shared `findVppDeviceByBoardName`, and
// the mock runs the real one over these two stubs rather than
// re-implementing the search — a stub that resolved boards its own way
Expand Down Expand Up @@ -159,6 +168,32 @@ describe('handleVendorPluginPackaging — provisioning branch', () => {
expect(logs.some((l) => /source file\(s\)/.test(l.message))).toBe(true)
})

it('refuses to copy the payload when the package no longer matches its signature', async () => {
// DOPE-539: this step is re-gated because it runs minutes after the
// compile-entry check, and what it copies is compiled on the live PLC.
// A throw is the contract — `packageVppPlugin` in the platform port turns
// it into the `errors[]` the pipeline bails on, whereas a logged error
// would let the build upload a bundle with no vendor I/O.
verifyBoardPackageIntegrity.mockReturnValueOnce({
ok: false,
packageId: 'com.openplc.rpi',
reason: 'Tampered file detected: hal/runtime-v4/plugin/rpi_plugin.o',
})

await expect(
runFor({
type: 'runtime-v4-plugin',
pluginType: 'native',
provisioning: 'prebuilt',
pluginEntry: 'hal/runtime-v4/plugin',
configTemplate: 'hal/runtime-v4/plugin/config_template.json',
}),
).rejects.toThrow(/com\.openplc\.rpi/)

expect(existsSync(join(targetDir, 'vpp_plugin'))).toBe(false)
expect(logs.some((l) => l.level === 'error' && /rpi_plugin\.o/.test(l.message))).toBe(true)
})

it('copies the payload byte-for-byte (prebuilt object content preserved)', async () => {
await runFor({
type: 'runtime-v4-plugin',
Expand Down
51 changes: 50 additions & 1 deletion src/backend/editor/compiler/compiler-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@

import type { PlatformOption } from '../../../middleware/shared/ports/types'
import { BoardInfoResolver } from '../../shared/hardware/board-info-resolver'
import { PackageManagerModule } from '../package-manager'
import { formatPackageIntegrityError, PackageManagerModule } from '../package-manager'
import { CreateXMLFile } from '../utils'
import { createDesktopLibraryBuildPort } from './desktop-library-build-port'
import { createEditorCompilerPlatformPort } from './editor-compiler-platform-port'
Expand Down Expand Up @@ -491,8 +491,8 @@

checkStrucppAvailability(): MethodsResult<string> {
try {
const { getVersion } = loadStrucpp()

Check warning on line 494 in src/backend/editor/compiler/compiler-module.ts

View workflow job for this annotation

GitHub Actions / lint / Lint Check

Unsafe array destructuring of a tuple element with an error typed value
return { success: true, data: getVersion() }

Check warning on line 495 in src/backend/editor/compiler/compiler-module.ts

View workflow job for this annotation

GitHub Actions / lint / Lint Check

Unsafe call of a(n) `error` type typed value

Check warning on line 495 in src/backend/editor/compiler/compiler-module.ts

View workflow job for this annotation

GitHub Actions / lint / Lint Check

Unsafe assignment of an error typed value
} catch {
throw new Error('STruC++ not available. Run "npm run setup:binaries" to install it.')
}
Expand Down Expand Up @@ -2010,6 +2010,24 @@
sourceTargetFolderPath: string,
handleOutputData: HandleOutputDataCallback,
): Promise<void> {
// Second gate, deliberately re-run here rather than trusted from
// `compileProgram` (DOPE-539). This step is what copies vendor C into the
// bundle the runtime compiles ON the PLC, and it runs late — transpile,
// strucpp and the v4 bundle compose happen in between, which on a large
// project is minutes of wall clock during which the package directory is
// still writable. Checking again costs one directory hash.
//
// This sits OUTSIDE the catch-all below on purpose. Every other failure in
// this method degrades the build and reports it; this one has to stop it,
// and `packageVppPlugin` in the platform port turns a throw into the
// `errors[]` the pipeline bails on.
const integrity = new PackageManagerModule().verifyBoardPackageIntegrity(boardTarget)
if (!integrity.ok) {
const message = formatPackageIntegrityError(boardTarget, integrity)
handleOutputData(message, 'error')
throw new Error(message)
}

try {
const match = new PackageManagerModule().findDeviceByBoardName(boardTarget)

Expand Down Expand Up @@ -2370,6 +2388,22 @@
Record<string, unknown> | undefined,
]

// VPP integrity gate (DOPE-539). FIRST, before the manifest is read for
// anything else: from here on this method trusts the package directory for
// the HAL it links, the licence-store backend it injects and every
// capability it branches on. The import check and the project-open sweep
// are both behind us and neither says anything about the package as it
// exists right now.
const boardPackageIntegrity = new PackageManagerModule().verifyBoardPackageIntegrity(boardTarget)
if (!boardPackageIntegrity.ok) {
_mainProcessPort.postMessage({
logLevel: 'error',
message: `${formatPackageIntegrityError(boardTarget, boardPackageIntegrity)}\nStopping compilation process.`,
})
_mainProcessPort.close()
return
}

// Resolve board info uniformly across hals.json + installed VPP
// packages via the shared `resolveBoardSelection` helper — the
// same code path runs on web (no VPP packages installed → falls
Expand Down Expand Up @@ -2829,6 +2863,21 @@

const [projectPath, boardTarget, projectData] = args as [string, string, PLCProjectData]

// Same gate as `compileProgram` (DOPE-539). The debug build is a smaller
// consumer of the package — it resolves the board and its debug spec — but
// it is still a build the user runs against a live device, and letting it
// through on a package the normal compile just refused would only teach
// that the check is avoidable.
const debugPackageIntegrity = new PackageManagerModule().verifyBoardPackageIntegrity(boardTarget)
if (!debugPackageIntegrity.ok) {
_mainProcessPort.postMessage({
logLevel: 'error',
message: `${formatPackageIntegrityError(boardTarget, debugPackageIntegrity)}\nStopping debug compilation process.`,
})
_mainProcessPort.close()
return
}

const debugResolver = await this.#createBoardInfoResolver()
const { boardRuntime } = debugResolver.resolve(boardTarget)
const normalizedProjectPath = projectPath.replace('project.json', '')
Expand Down Expand Up @@ -2934,7 +2983,7 @@
_mainProcessPort.postMessage({
logLevel,
message: data,
...(compileError ? { compileError } : {}),

Check warning on line 2986 in src/backend/editor/compiler/compiler-module.ts

View workflow job for this annotation

GitHub Actions / lint / Lint Check

Unsafe assignment of an error typed value
})
},
{ hasCBlocks, pous: knownPous, libraries, missingLibraries },
Expand Down
Loading
Loading