contracts: removeDevice is inert while allowAnyDevice is true, and both the mapping and the audit event report a revocation that changed no decision
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 5/5
- Thời gian dự kiến
- Hơn một tuần
- Mức phù hợp với người mới
- 30/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Khá rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- solidity
- Lĩnh vực
- authorization, blockchain, security
Hướng nghiên cứu
Start with contracts/DstackApp.sol, then read docs/onchain-governance.md, docs/auth-simple-operations.md, and script/Manage.s.sol to understand the documented policy and operator flow. Run the pinned scenarios in test/ScenarioWalk.t.sol and review test/EventAudit.t.sol; done means an agreed design that aligns authorization decisions, audit events, and documentation without breaking staged device-list updates.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Label: DESIGN. No test fails against the current contracts; the tests below pass and pin present behaviour.
What the design currently is
DstackApp.isAppAllowed composes the device flag and the device list as a short-circuit:
// contracts/DstackApp.sol:209
if (!allowAnyDevice && !allowedDeviceIds[bootInfo.deviceId]) {
return (false, "Device not allowed");
}
So whenever allowAnyDevice == true, the contents of allowedDeviceIds do not participate in any authorization decision. addDevice and removeDevice (DstackApp.sol:175, DstackApp.sol:182) still write the mapping and still emit DeviceAdded / DeviceRemoved plus PolicyChanged(actor, keccak("device"), deviceId, enabled) unconditionally.
Call sequence an operator actually performs — the one docs/auth-simple-operations.md:160 recommends ("Use allowAnyDevice: true initially, then restrict to specific devices after capturing IDs from logs"), and the one the shipped script/Manage.s.sol:159 default (ALLOW_ANY_DEVICE defaults to true) puts you in by default:
appOwner : app.setAllowAnyDevice(true) // to bring up / debug a new host
...
appOwner : app.removeDevice(DEVICE_A) // "host A is compromised, revoke it"
Resulting state: allowedDeviceIds[DEVICE_A] == false, one DeviceRemoved, one PolicyChanged(..., false). isAppAllowed with deviceId = DEVICE_A still returns (true, "").
The mirror case: setAllowAnyDevice(false) re-arms every device ever added, and the single PolicyChanged("allow-any-device", bytes32(0), false) it emits names none of them.
Both are pinned in the new test/ScenarioWalk.t.sol:
[PASS] test_S4_RemoveDeviceIsANoOpUnderAllowAnyDevice_ButStillLogsARevocation() (gas: 307587)
assertEq(_auditCount(logs), 1, "the audit log records a revocation");
assertFalse(app.allowedDeviceIds(DEVICE_A), "and the mapping agrees");
assertTrue(ok, why); // <-- the revoked device still boots
[PASS] test_S4_SetAllowAnyDeviceFalse_SilentlyResurrectsAStaleDeviceList() (gas: 341290)
assertEq(_auditCount(logs), 1, "one flag event -- it names no device");
assertTrue(a && b, "both historical devices are live again");
Steelman
This is the cheapest correct composition. An allowAnyDevice escape hatch is genuinely useful during bring-up, and making the setters conditional on the flag would either cost an extra SLOAD on every write or make the setters revert in a state an operator legitimately wants to prepare (stage the device list before flipping the flag off — which the current design supports and a revert would break). Emitting the event unconditionally is also the simpler invariant for a log indexer: one event per state transition of the mapping, with no flag-dependent branch to replay.
test/EventAudit.t.sol:82-96 already walks this exact sequence and asserts the event and the mapping — so the current behaviour is deliberate at the level of the mapping. The gap is that the mapping is not the policy.
What it costs
The operator scenario it breaks is emergency device revocation, and it breaks it in the quietest possible way: the transaction succeeds, the mapping reads false, and the permanent audit log records a revocation. Every signal an operator or an auditor would check agrees that the device was revoked. Only isAppAllowed disagrees.
docs/onchain-governance.md:171 documents removeDevice as "Remove a device from whitelist" with no mention of the flag, and PolicyChanged is documented in-contract as the "additive audit event for reconstructing authorization policy" — which it is, for the mapping, and is not, for the decision.
Reachability: who — the app owner; credential — the app owner key; frequency — operator-paced. This is not an attacker-triggered defect. It is a control that reports success without taking effect.
Improvement direction
Redeployment status: none of these needs a new proxy. DstackApp is UUPS with __gap[49]; every code option below is an implementation upgrade behind existing proxy addresses with no storage change. The caveat that decides priority is different: DstackApp proxies are owned by each app's owner, so an implementation fix reaches an app only when that owner upgrades, and never for an app that has called disableUpgrades(). The documentation half of the fix therefore has to stand on its own.
Options, cheapest first:
- Docs only, no contract change. State in
docs/onchain-governance.mdand theremoveDevicenatspec that the device list is inert whileallowAnyDeviceis true, and that revoking a device requiressetAllowAnyDevice(false)first. Reaches every deployed app immediately, including frozen ones. Does not fix the misleading audit event. - Make the write reflect the policy (impl upgrade, no storage).
require(!allowAnyDevice, "device list is inert while allowAnyDevice")inaddDevice/removeDevice. Loudest and simplest, but breaks the legitimate stage-then-flip workflow, so it is probably the wrong trade. - Make the event tell the truth (impl upgrade, no storage). Keep the setters permissive, but widen the audit event so a log replayer can compute the decision — e.g. emit
PolicyChanged("device", deviceId, enabled && !allowAnyDevice), or add a distinctDevicePolicyInert(deviceId)marker. Preserves the workflow; the log becomes replayable to the decision rather than to the mapping. Cost: a log indexer keyed on the current semantics needs updating. - Emit the affected set on the flag transition (impl upgrade, needs an enumerable device set — new storage past
__gap). Highest review cost, and the unbounded-iteration gas profile is a real objection. Probably not worth it.
(3) plus (1) looks like the right combination: it keeps the flag's usefulness, keeps the staging workflow, and closes the gap between the audit story and the decision.
Found during a scenario-driven review of the authorization contracts; full walk in .agent/CONTRACT-SCENARIOS.md (scenario 4), tests in dstack/kms/auth-eth/test/ScenarioWalk.t.sol.
- Ngôn ngữ chính
- Rust
- Star
- 551
- Fork
- 97
- Merge trung bình
- 1 ngày 8 giờ
- Pull request đã merge (30 ngày)
- 182
Chuẩn bị môi trường
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của Dstack-TEE/dstack
-
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 35/100
Dstack-TEE/dstack#1384 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 30/100
Dstack-TEE/dstack#1301 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 55/100
Dstack-TEE/dstack#1300 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 48/100
Dstack-TEE/dstack#1299 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 48/100
Dstack-TEE/dstack#1298 ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của Dstack-TEE/dstack
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
linebender/parley#849 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 88/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 86/100
boxlite-ai/boxlite#1729 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
external
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
langchain-ai/docs#6255 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
P2 testing
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 90/100
Maintainer thường phản hồi trong vòng 1 ngày