diff --git a/Cargo.lock b/Cargo.lock
index 828e142ca965e1..7ce52073260fed 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5520,6 +5520,7 @@ name = "solana-sdk"
 version = "1.8.0"
 dependencies = [
  "assert_matches",
+ "base64 0.13.0",
  "bincode",
  "borsh",
  "borsh-derive",
diff --git a/docs/src/proposals/return-data.md b/docs/src/proposals/return-data.md
new file mode 100644
index 00000000000000..3d37ee6fca0509
--- /dev/null
+++ b/docs/src/proposals/return-data.md
@@ -0,0 +1,144 @@
+# Return data from BPF programs
+
+## Problem
+
+In the Solidity langauge it is permitted to return any number of values from a function,
+for example a variable length string can be returned:
+
+```
+function foo1() public returns (string) {
+    return "Hello, world!\n";
+}
+```
+
+Multiple values, arrays and structs are permitted too.
+
+```
+struct S {
+    int f1;
+    bool f2
+};
+
+function foo2() public returns (string, int[], S) {
+    return (a, b, c);
+}
+```
+
+All the return values are eth abi encoded to a variable-length byte array.
+
+On ethereum errors can be returned too:
+
+```
+function withdraw() public {
+    require(msg.sender == owner, "Permission denied");
+}
+
+function failure() public {
+    revert("I afraid I can't do that dave");
+}
+```
+These errors help the developer debug any issue they are having, and can
+also be caught in a Solidity `try` .. `catch` block. Outside of a `try` .. `catch`
+block, any of these would cause the transaction or rpc to fail.
+
+## Existing solution
+
+The existing solution that Solang uses, writes the return data to the callee account data.
+The caller's account cannot be used, since the callee may not be the same BPF program, so
+it will not have permission to write to the callee's account data.
+
+Another solution would be to have a single return data account which is passed
+around through CPI. Again this does not work for CPI as the callee may not have
+permission to write to it.
+
+The problem with this solution is:
+
+- It does not work for RPC calls
+- It is very racey; a client has to submit the Tx and then retrieve the account
+  data. This is not atomic so the return data can be overwritten by another transaction.
+
+## Requirements for Solution
+
+It must work for:
+
+- RPC: An RPC should be able to return any number of values without writing to account data
+- Transaction: An transaction should be able to return any number of values without needing to write them account data
+- CPI: The callee must "set" return value, and the caller must be able to retrieve it.
+
+## Review of other chains
+
+### Ethereum (EVM)
+
+The `RETURN` opcode allows a contract to set a buffer as a returndata. This opcode takes a pointer to memory and a size. The `REVERT` opcode works similarly but signals that the call failed, and all account data changes must be reverted.
+
+For CPI, the caller can retrieve the returned data of the callee using the `RETURNDATASIZE` opcode which returns the length, and the `RETURNDATACOPY` opcode, which takes a memory destination pointer, offset into the returndata, and a length argument.
+
+Ethereum stores the returndata in blocks.
+
+### Parity Substrate
+
+The return data can be set using the `seal_return(u32 flags, u32 pointer, u32 size)` syscall.
+- Flags can be 1 for revert, 0 for success (nothing else defined)
+- Function does not return
+
+CPI: The `seal_call()` syscall takes pointer to buffer and pointer to buffer size where return data goes
+ - There is a 32KB limit for return data.
+
+Parity Substrate does not write the return data to blocks.
+
+## Rejected Solution
+
+The concept of ephemeral accounts has been proposed a solution for this. This would
+certainly work for the CPI case, but this would not work RPC or Transaction case.
+
+## Proposed Solution
+
+The callee can set the return data using a new system call `sol_set_return_data(buf: *const u8, length: u64)`.
+There is a limit of 1024 bytes for the returndata. This function can be called multiple times, and
+will simply overwrite what was written in the last call.
+
+The return data can be retrieved with `sol_get_return_data(buf: *mut u8, length: u64, program_id: *mut Pubkey) -> u64`.
+This function copies the return buffer, and the program_id that set the return data, and
+returns the length of the return data, or `0` if no return data is set. In this case, program_id is not set.
+
+When an instruction calls `sol_invoke()`, the return data of the callee is copied into the return data
+of the current instruction. This means that any return data is automatically passed up the call stack,
+to the callee of the current instruction (or the RPC call).
+
+Note that `sol_invoke()` clears the returns data before invoking the callee, so that any return data from
+a previous invoke is not reused if the invoked fails to set a return data. For example:
+
+ - A invokes B
+ - Before entry to B, return data is cleared.0
+ - B sets some return data and returns
+ - A invokes C
+ - Before entry to C, return data is cleared.
+ - C does not set return data and returns
+ - A checks return data and finds it empty
+
+Another scenario to consider:
+
+ - A invokes B
+ - B invokes C
+ - C sets return data and returns
+ - B does not touch return data and returns
+ - A gets return data from C
+ - A does not touch return data
+ - Return data from transaction is what C set.
+
+The compute costs are calculated for getting and setting the return data using
+the syscalls.
+
+For a normal RPC or Transaction, the returndata is base64-encoded and stored along side the sol_log
+strings in the [stable log](https://github.com/solana-labs/solana/blob/95292841947763bdd47ef116b40fc34d0585bca8/sdk/src/process_instruction.rs#L275-L281).
+
+## Note on returning errors
+
+Solidity on Ethereum allows the contract to return an error in the return data. In this case, all
+the account data changes for the account should be reverted. On Solana, any non-zero exit code
+for a BPF prorgram means the entire transaction fails. We do not wish to support an error return
+by returning success and then returning an error in the return data. This would mean we would have
+to support reverting the account data changes; this too expensive both on the VM side and the BPF
+contract side.
+
+Errors will be reported via sol_log.
diff --git a/program-runtime/src/instruction_processor.rs b/program-runtime/src/instruction_processor.rs
index 42fb4b87df1b70..fda80ad83211da 100644
--- a/program-runtime/src/instruction_processor.rs
+++ b/program-runtime/src/instruction_processor.rs
@@ -631,6 +631,9 @@ impl InstructionProcessor {
             // Verify the calling program hasn't misbehaved
             invoke_context.verify_and_update(instruction, accounts, caller_write_privileges)?;
 
+            // clear the return data
+            invoke_context.set_return_data(None);
+
             // Invoke callee
             invoke_context.push(program_id, message, instruction, program_indices, accounts)?;
 
diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock
index fd8f12e2674cf5..1e8c9dba7c221e 100644
--- a/programs/bpf/Cargo.lock
+++ b/programs/bpf/Cargo.lock
@@ -3260,6 +3260,7 @@ name = "solana-sdk"
 version = "1.8.0"
 dependencies = [
  "assert_matches",
+ "base64 0.13.0",
  "bincode",
  "borsh",
  "borsh-derive",
diff --git a/programs/bpf/c/src/invoke/invoke.c b/programs/bpf/c/src/invoke/invoke.c
index f192ad5360feab..c7807b4f280fa3 100644
--- a/programs/bpf/c/src/invoke/invoke.c
+++ b/programs/bpf/c/src/invoke/invoke.c
@@ -8,6 +8,7 @@
 #include <sol/log.h>
 #include <sol/assert.h>
 #include <sol/deserialize.h>
+#include <sol/return_data.h>
 
 static const uint8_t TEST_SUCCESS = 1;
 static const uint8_t TEST_PRIVILEGE_ESCALATION_SIGNER = 2;
@@ -26,6 +27,7 @@ static const uint8_t TEST_WRITABLE_DEESCALATION_WRITABLE = 14;
 static const uint8_t TEST_NESTED_INVOKE_TOO_DEEP = 15;
 static const uint8_t TEST_EXECUTABLE_LAMPORTS = 16;
 static const uint8_t ADD_LAMPORTS = 17;
+static const uint8_t TEST_RETURN_DATA_TOO_LARGE = 18;
 
 static const int MINT_INDEX = 0;
 static const int ARGUMENT_INDEX = 1;
@@ -174,6 +176,32 @@ extern uint64_t entrypoint(const uint8_t *input) {
                  sol_invoke(&instruction, accounts, SOL_ARRAY_SIZE(accounts)));
     }
 
+    sol_log("Test return data");
+    {
+      SolAccountMeta arguments[] = {{accounts[ARGUMENT_INDEX].key, true, true}};
+      uint8_t data[] = { SET_RETURN_DATA };
+      uint8_t buf[100];
+
+      const SolInstruction instruction = {accounts[INVOKED_PROGRAM_INDEX].key,
+                                          arguments, SOL_ARRAY_SIZE(arguments),
+                                          data, SOL_ARRAY_SIZE(data)};
+
+      // set some return data, so that the callee can check it is cleared
+      sol_set_return_data((uint8_t[]){1, 2, 3, 4}, 4);
+
+      sol_assert(SUCCESS ==
+                 sol_invoke(&instruction, accounts, SOL_ARRAY_SIZE(accounts)));
+
+      SolPubkey setter;
+
+      uint64_t ret = sol_get_return_data(data, sizeof(data), &setter);
+
+      sol_assert(ret == sizeof(RETURN_DATA_VAL));
+
+      sol_assert(sol_memcmp(data, RETURN_DATA_VAL, sizeof(RETURN_DATA_VAL)));
+      sol_assert(SolPubkey_same(&setter, accounts[INVOKED_PROGRAM_INDEX].key));
+    }
+
     sol_log("Test create_program_address");
     {
       uint8_t seed1[] = {'Y', 'o', 'u', ' ', 'p', 'a', 's', 's',
@@ -542,27 +570,33 @@ extern uint64_t entrypoint(const uint8_t *input) {
     break;
   }
   case TEST_EXECUTABLE_LAMPORTS: {
-      sol_log("Test executable lamports");
-      accounts[ARGUMENT_INDEX].executable = true;
-      *accounts[ARGUMENT_INDEX].lamports -= 1;
-      *accounts[DERIVED_KEY1_INDEX].lamports +=1;
-      SolAccountMeta arguments[] = {
-          {accounts[ARGUMENT_INDEX].key, true, false},
-          {accounts[DERIVED_KEY1_INDEX].key, true, false},
-      };
-      uint8_t data[] = {ADD_LAMPORTS, 0, 0, 0};
-      SolPubkey program_id;
-      sol_memcpy(&program_id, params.program_id, sizeof(SolPubkey));
-      const SolInstruction instruction = {&program_id,
-                                          arguments, SOL_ARRAY_SIZE(arguments),
-                                          data, SOL_ARRAY_SIZE(data)};
-      sol_invoke(&instruction, accounts, SOL_ARRAY_SIZE(accounts));
-      *accounts[ARGUMENT_INDEX].lamports += 1;
-      break;
+    sol_log("Test executable lamports");
+    accounts[ARGUMENT_INDEX].executable = true;
+    *accounts[ARGUMENT_INDEX].lamports -= 1;
+    *accounts[DERIVED_KEY1_INDEX].lamports +=1;
+    SolAccountMeta arguments[] = {
+      {accounts[ARGUMENT_INDEX].key, true, false},
+      {accounts[DERIVED_KEY1_INDEX].key, true, false},
+    };
+    uint8_t data[] = {ADD_LAMPORTS, 0, 0, 0};
+    SolPubkey program_id;
+    sol_memcpy(&program_id, params.program_id, sizeof(SolPubkey));
+    const SolInstruction instruction = {&program_id,
+					arguments, SOL_ARRAY_SIZE(arguments),
+					data, SOL_ARRAY_SIZE(data)};
+    sol_invoke(&instruction, accounts, SOL_ARRAY_SIZE(accounts));
+    *accounts[ARGUMENT_INDEX].lamports += 1;
+    break;
   }
   case ADD_LAMPORTS: {
-      *accounts[0].lamports += 1;
-      break;
+    *accounts[0].lamports += 1;
+     break;
+  }
+  case TEST_RETURN_DATA_TOO_LARGE: {
+    sol_log("Test setting return data too long");
+    // The actual buffer doesn't matter, just pass null
+    sol_set_return_data(NULL, 1027);
+    break;
   }
 
   default:
diff --git a/programs/bpf/c/src/invoked/instruction.h b/programs/bpf/c/src/invoked/instruction.h
index 1e4f7ac8d364f3..70764a5e67d3bc 100644
--- a/programs/bpf/c/src/invoked/instruction.h
+++ b/programs/bpf/c/src/invoked/instruction.h
@@ -16,3 +16,6 @@ const uint8_t VERIFY_PRIVILEGE_DEESCALATION = 8;
 const uint8_t VERIFY_PRIVILEGE_DEESCALATION_ESCALATION_SIGNER = 9;
 const uint8_t VERIFY_PRIVILEGE_DEESCALATION_ESCALATION_WRITABLE = 10;
 const uint8_t WRITE_ACCOUNT = 11;
+const uint8_t SET_RETURN_DATA = 12;
+
+#define RETURN_DATA_VAL "return data test"
\ No newline at end of file
diff --git a/programs/bpf/c/src/invoked/invoked.c b/programs/bpf/c/src/invoked/invoked.c
index 273deb9d0607db..109f557c42f012 100644
--- a/programs/bpf/c/src/invoked/invoked.c
+++ b/programs/bpf/c/src/invoked/invoked.c
@@ -14,6 +14,9 @@ extern uint64_t entrypoint(const uint8_t *input) {
     return ERROR_INVALID_ARGUMENT;
   }
 
+  // on entry, return data must not be set
+  sol_assert(sol_get_return_data(NULL, 0, NULL) == 0);
+
   if (params.data_len == 0) {
     return SUCCESS;
   }
@@ -91,6 +94,12 @@ extern uint64_t entrypoint(const uint8_t *input) {
     sol_log("return Ok");
     return SUCCESS;
   }
+  case SET_RETURN_DATA: {
+    sol_set_return_data((const uint8_t*)RETURN_DATA_VAL, sizeof(RETURN_DATA_VAL));
+    sol_log("set return data");
+    sol_assert(sol_get_return_data(NULL, 0, NULL) == sizeof(RETURN_DATA_VAL));
+    return SUCCESS;
+  }
   case RETURN_ERROR: {
     sol_log("return error");
     return 42;
diff --git a/programs/bpf/c/src/return_data/return_data.c b/programs/bpf/c/src/return_data/return_data.c
new file mode 100644
index 00000000000000..006cdbcf508672
--- /dev/null
+++ b/programs/bpf/c/src/return_data/return_data.c
@@ -0,0 +1,40 @@
+/**
+ * @brief return data Syscall test
+ */
+#include <solana_sdk.h>
+
+#define DATA "the quick brown fox jumps over the lazy dog"
+
+extern uint64_t entrypoint(const uint8_t *input) {
+  uint8_t buf[1024];
+  SolPubkey me;
+
+  // There should be no return data on entry
+  uint64_t ret = sol_get_return_data(NULL, 0, NULL);
+
+  sol_assert(ret == 0);
+
+  // set some return data
+  sol_set_return_data((const uint8_t*)DATA, sizeof(DATA));
+
+  // ensure the length is correct
+  ret = sol_get_return_data(NULL, 0, &me);
+  sol_assert(ret == sizeof(DATA));
+
+  // try getting a subset
+  ret = sol_get_return_data(buf, 4, &me);
+
+  sol_assert(ret == sizeof(DATA));
+
+  sol_assert(!sol_memcmp(buf, "the ", 4));
+
+  // try getting the whole thing
+  ret = sol_get_return_data(buf, sizeof(buf), &me);
+
+  sol_assert(ret == sizeof(DATA));
+
+  sol_assert(!sol_memcmp(buf, (const uint8_t*)DATA, sizeof(DATA)));
+
+  // done
+  return SUCCESS;
+}
diff --git a/programs/bpf/rust/invoke/src/lib.rs b/programs/bpf/rust/invoke/src/lib.rs
index 008994181952ae..31171c7b0316f3 100644
--- a/programs/bpf/rust/invoke/src/lib.rs
+++ b/programs/bpf/rust/invoke/src/lib.rs
@@ -10,7 +10,7 @@ use solana_program::{
     entrypoint,
     entrypoint::{ProgramResult, MAX_PERMITTED_DATA_INCREASE},
     msg,
-    program::{invoke, invoke_signed},
+    program::{get_return_data, invoke, invoke_signed, set_return_data},
     program_error::ProgramError,
     pubkey::{Pubkey, PubkeyError},
     system_instruction,
@@ -394,6 +394,27 @@ fn process_instruction(
                     assert_eq!(data[i], i as u8);
                 }
             }
+
+            msg!("Test return data via invoked");
+            {
+                // this should be cleared on entry, the invoked tests for this
+                set_return_data(b"x");
+
+                let instruction = create_instruction(
+                    *accounts[INVOKED_PROGRAM_INDEX].key,
+                    &[(accounts[ARGUMENT_INDEX].key, false, true)],
+                    vec![SET_RETURN_DATA],
+                );
+                let _ = invoke(&instruction, accounts);
+
+                assert_eq!(
+                    get_return_data(),
+                    Some((
+                        *accounts[INVOKED_PROGRAM_INDEX].key,
+                        b"Set by invoked".to_vec()
+                    ))
+                );
+            }
         }
         TEST_PRIVILEGE_ESCALATION_SIGNER => {
             msg!("Test privilege escalation signer");
diff --git a/programs/bpf/rust/invoked/src/instruction.rs b/programs/bpf/rust/invoked/src/instruction.rs
index a6c16dc8cc23ea..28a7eb347b8d48 100644
--- a/programs/bpf/rust/invoked/src/instruction.rs
+++ b/programs/bpf/rust/invoked/src/instruction.rs
@@ -18,6 +18,7 @@ pub const VERIFY_PRIVILEGE_DEESCALATION_ESCALATION_SIGNER: u8 = 9;
 pub const VERIFY_PRIVILEGE_DEESCALATION_ESCALATION_WRITABLE: u8 = 10;
 pub const WRITE_ACCOUNT: u8 = 11;
 pub const CREATE_AND_INIT: u8 = 12;
+pub const SET_RETURN_DATA: u8 = 13;
 
 pub fn create_instruction(
     program_id: Pubkey,
diff --git a/programs/bpf/rust/invoked/src/processor.rs b/programs/bpf/rust/invoked/src/processor.rs
index 892ba90e631235..7d9aee889e66a0 100644
--- a/programs/bpf/rust/invoked/src/processor.rs
+++ b/programs/bpf/rust/invoked/src/processor.rs
@@ -8,7 +8,7 @@ use solana_program::{
     bpf_loader, entrypoint,
     entrypoint::{ProgramResult, MAX_PERMITTED_DATA_INCREASE},
     msg,
-    program::{invoke, invoke_signed},
+    program::{get_return_data, invoke, invoke_signed, set_return_data},
     program_error::ProgramError,
     pubkey::Pubkey,
     system_instruction,
@@ -27,6 +27,8 @@ fn process_instruction(
         return Ok(());
     }
 
+    assert_eq!(get_return_data(), None);
+
     match instruction_data[0] {
         VERIFY_TRANSLATIONS => {
             msg!("verify data translations");
@@ -286,6 +288,11 @@ fn process_instruction(
                 }
             }
         }
+        SET_RETURN_DATA => {
+            msg!("Set return data");
+
+            set_return_data(b"Set by invoked");
+        }
         _ => panic!(),
     }
 
diff --git a/programs/bpf/tests/programs.rs b/programs/bpf/tests/programs.rs
index 31979d4937eb63..1f3f82a04766f8 100644
--- a/programs/bpf/tests/programs.rs
+++ b/programs/bpf/tests/programs.rs
@@ -227,6 +227,8 @@ fn run_program(
     for i in 0..2 {
         let mut parameter_bytes = parameter_bytes.clone();
         {
+            invoke_context.set_return_data(None);
+
             let mut vm = create_vm(
                 &loader_id,
                 executable.as_ref(),
@@ -432,6 +434,7 @@ fn test_program_bpf_sanity() {
             ("noop++", true),
             ("panic", false),
             ("relative_call", true),
+            ("return_data", true),
             ("sanity", true),
             ("sanity++", true),
             ("secp256k1_recover", true),
@@ -756,6 +759,7 @@ fn test_program_bpf_invoke_sanity() {
     const TEST_WRITABLE_DEESCALATION_WRITABLE: u8 = 14;
     const TEST_NESTED_INVOKE_TOO_DEEP: u8 = 15;
     const TEST_EXECUTABLE_LAMPORTS: u8 = 16;
+    const TEST_RETURN_DATA_TOO_LARGE: u8 = 18;
 
     #[allow(dead_code)]
     #[derive(Debug)]
@@ -878,6 +882,7 @@ fn test_program_bpf_invoke_sanity() {
                 invoked_program_id.clone(),
                 invoked_program_id.clone(),
                 invoked_program_id.clone(),
+                invoked_program_id.clone(),
             ],
             Languages::Rust => vec![
                 system_program::id(),
@@ -902,6 +907,7 @@ fn test_program_bpf_invoke_sanity() {
                 invoked_program_id.clone(),
                 invoked_program_id.clone(),
                 system_program::id(),
+                invoked_program_id.clone(),
             ],
         };
         assert_eq!(invoked_programs.len(), expected_invoked_programs.len());
@@ -1030,6 +1036,12 @@ fn test_program_bpf_invoke_sanity() {
             &[invoke_program_id.clone()],
         );
 
+        do_invoke_failure_test_local(
+            TEST_RETURN_DATA_TOO_LARGE,
+            TransactionError::InstructionError(0, InstructionError::ProgramFailedToComplete),
+            &[],
+        );
+
         // Check resulting state
 
         assert_eq!(43, bank.get_balance(&derived_key1));
@@ -1312,6 +1324,7 @@ fn assert_instruction_count() {
             ("noop", 5),
             ("noop++", 5),
             ("relative_call", 10),
+            ("return_data", 480),
             ("sanity", 169),
             ("sanity++", 168),
             ("secp256k1_recover", 359),
diff --git a/programs/bpf_loader/src/lib.rs b/programs/bpf_loader/src/lib.rs
index ce6465ecba2983..6d14393c1e0a74 100644
--- a/programs/bpf_loader/src/lib.rs
+++ b/programs/bpf_loader/src/lib.rs
@@ -917,6 +917,11 @@ impl Executor for BpfExecutor {
                 let trace_string = String::from_utf8(trace_buffer).unwrap();
                 trace!("BPF Program Instruction Trace:\n{}", trace_string);
             }
+            drop(vm);
+            let return_data = invoke_context.get_return_data();
+            if let Some((program_id, return_data)) = return_data {
+                stable_log::program_return_data(&logger, program_id, return_data);
+            }
             match result {
                 Ok(status) => {
                     if status != SUCCESS {
diff --git a/programs/bpf_loader/src/syscalls.rs b/programs/bpf_loader/src/syscalls.rs
index fa61ce366e9f6d..2fc5639d8862a9 100644
--- a/programs/bpf_loader/src/syscalls.rs
+++ b/programs/bpf_loader/src/syscalls.rs
@@ -24,7 +24,7 @@ use solana_sdk::{
         allow_native_ids, blake3_syscall_enabled, check_seed_length,
         close_upgradeable_program_accounts, demote_program_write_locks, disable_fees_sysvar,
         enforce_aligned_host_addrs, libsecp256k1_0_5_upgrade_enabled, mem_overlap_fix,
-        secp256k1_recover_syscall_enabled,
+        return_data_syscall_enabled, secp256k1_recover_syscall_enabled,
     },
     hash::{Hasher, HASH_BYTES},
     ic_msg,
@@ -33,6 +33,7 @@ use solana_sdk::{
     keyed_account::KeyedAccount,
     native_loader,
     process_instruction::{self, stable_log, ComputeMeter, InvokeContext, Logger},
+    program::MAX_RETURN_DATA,
     pubkey::{Pubkey, PubkeyError, MAX_SEEDS, MAX_SEED_LEN},
     rent::Rent,
     secp256k1_recover::{
@@ -43,6 +44,7 @@ use solana_sdk::{
 use std::{
     alloc::Layout,
     cell::{Ref, RefCell, RefMut},
+    cmp::min,
     mem::{align_of, size_of},
     rc::Rc,
     slice::from_raw_parts_mut,
@@ -62,9 +64,9 @@ pub enum SyscallError {
     Abort,
     #[error("BPF program Panicked in {0} at {1}:{2}")]
     Panic(String, u64, u64),
-    #[error("cannot borrow invoke context")]
+    #[error("Cannot borrow invoke context")]
     InvokeContextBorrowFailed,
-    #[error("malformed signer seed: {0}: {1:?}")]
+    #[error("Malformed signer seed: {0}: {1:?}")]
     MalformedSignerSeed(Utf8Error, Vec<u8>),
     #[error("Could not create program address with signer seeds: {0}")]
     BadSeeds(PubkeyError),
@@ -82,6 +84,8 @@ pub enum SyscallError {
     TooManyAccounts,
     #[error("Overlapping copy")]
     CopyOverlapping,
+    #[error("Return data too large ({0} > {1})")]
+    ReturnDataTooLarge(u64, u64),
 }
 impl From<SyscallError> for EbpfError<BpfError> {
     fn from(error: SyscallError) -> Self {
@@ -173,6 +177,14 @@ pub fn register_syscalls(
     // Memory allocator
     syscall_registry.register_syscall_by_name(b"sol_alloc_free_", SyscallAllocFree::call)?;
 
+    // Return data
+    if invoke_context.is_feature_active(&return_data_syscall_enabled::id()) {
+        syscall_registry
+            .register_syscall_by_name(b"sol_set_return_data", SyscallSetReturnData::call)?;
+        syscall_registry
+            .register_syscall_by_name(b"sol_get_return_data", SyscallGetReturnData::call)?;
+    }
+
     Ok(syscall_registry)
 }
 
@@ -353,6 +365,8 @@ pub fn bind_syscall_context_objects<'a>(
 
     let is_fee_sysvar_via_syscall_active =
         !invoke_context.is_feature_active(&disable_fees_sysvar::id());
+    let is_return_data_syscall_active =
+        invoke_context.is_feature_active(&return_data_syscall_enabled::id());
 
     let invoke_context = Rc::new(RefCell::new(invoke_context));
 
@@ -386,6 +400,25 @@ pub fn bind_syscall_context_objects<'a>(
         None,
     )?;
 
+    // Return data
+    bind_feature_gated_syscall_context_object!(
+        vm,
+        is_return_data_syscall_active,
+        Box::new(SyscallSetReturnData {
+            invoke_context: invoke_context.clone(),
+            loader_id,
+        }),
+    );
+
+    bind_feature_gated_syscall_context_object!(
+        vm,
+        is_return_data_syscall_active,
+        Box::new(SyscallGetReturnData {
+            invoke_context: invoke_context.clone(),
+            loader_id,
+        }),
+    );
+
     // Cross-program invocation syscalls
     vm.bind_syscall_context_object(
         Box::new(SyscallInvokeSignedC {
@@ -2548,6 +2581,142 @@ fn call<'a>(
     Ok(SUCCESS)
 }
 
+// Return data handling
+pub struct SyscallSetReturnData<'a> {
+    invoke_context: Rc<RefCell<&'a mut dyn InvokeContext>>,
+    loader_id: &'a Pubkey,
+}
+impl<'a> SyscallObject<BpfError> for SyscallSetReturnData<'a> {
+    fn call(
+        &mut self,
+        addr: u64,
+        len: u64,
+        _arg3: u64,
+        _arg4: u64,
+        _arg5: u64,
+        memory_mapping: &MemoryMapping,
+        result: &mut Result<u64, EbpfError<BpfError>>,
+    ) {
+        let mut invoke_context = question_mark!(
+            self.invoke_context
+                .try_borrow_mut()
+                .map_err(|_| SyscallError::InvokeContextBorrowFailed),
+            result
+        );
+
+        let budget = invoke_context.get_compute_budget();
+
+        question_mark!(
+            invoke_context
+                .get_compute_meter()
+                .consume(len / budget.cpi_bytes_per_unit + budget.syscall_base_cost),
+            result
+        );
+
+        if len > MAX_RETURN_DATA as u64 {
+            *result = Err(SyscallError::ReturnDataTooLarge(len, MAX_RETURN_DATA as u64).into());
+            return;
+        }
+
+        if len == 0 {
+            invoke_context.set_return_data(None);
+        } else {
+            let return_data = question_mark!(
+                translate_slice::<u8>(memory_mapping, addr, len, self.loader_id, true),
+                result
+            );
+
+            let program_id = *question_mark!(
+                invoke_context
+                    .get_caller()
+                    .map_err(SyscallError::InstructionError),
+                result
+            );
+
+            invoke_context.set_return_data(Some((program_id, return_data.to_vec())));
+        }
+
+        *result = Ok(0);
+    }
+}
+
+pub struct SyscallGetReturnData<'a> {
+    invoke_context: Rc<RefCell<&'a mut dyn InvokeContext>>,
+    loader_id: &'a Pubkey,
+}
+impl<'a> SyscallObject<BpfError> for SyscallGetReturnData<'a> {
+    fn call(
+        &mut self,
+        return_data_addr: u64,
+        len: u64,
+        program_id_addr: u64,
+        _arg4: u64,
+        _arg5: u64,
+        memory_mapping: &MemoryMapping,
+        result: &mut Result<u64, EbpfError<BpfError>>,
+    ) {
+        let invoke_context = question_mark!(
+            self.invoke_context
+                .try_borrow()
+                .map_err(|_| SyscallError::InvokeContextBorrowFailed),
+            result
+        );
+
+        let budget = invoke_context.get_compute_budget();
+
+        question_mark!(
+            invoke_context
+                .get_compute_meter()
+                .consume(budget.syscall_base_cost),
+            result
+        );
+
+        if let Some((program_id, return_data)) = invoke_context.get_return_data() {
+            if len != 0 {
+                let length = min(return_data.len() as u64, len);
+
+                question_mark!(
+                    invoke_context
+                        .get_compute_meter()
+                        .consume((length + size_of::<Pubkey>() as u64) / budget.cpi_bytes_per_unit),
+                    result
+                );
+
+                let return_data_result = question_mark!(
+                    translate_slice_mut::<u8>(
+                        memory_mapping,
+                        return_data_addr,
+                        length,
+                        self.loader_id,
+                        true,
+                    ),
+                    result
+                );
+
+                return_data_result.copy_from_slice(&return_data[..length as usize]);
+
+                let program_id_result = question_mark!(
+                    translate_slice_mut::<Pubkey>(
+                        memory_mapping,
+                        program_id_addr,
+                        1,
+                        self.loader_id,
+                        true,
+                    ),
+                    result
+                );
+
+                program_id_result[0] = *program_id;
+            }
+
+            // Return the actual length, rather the length returned
+            *result = Ok(return_data.len() as u64);
+        } else {
+            *result = Ok(0);
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/runtime/src/message_processor.rs b/runtime/src/message_processor.rs
index 936e53d2e8eff2..f037dff6fb28e0 100644
--- a/runtime/src/message_processor.rs
+++ b/runtime/src/message_processor.rs
@@ -67,6 +67,8 @@ pub struct ThisInvokeContext<'a> {
     sysvars: RefCell<Vec<(Pubkey, Option<Rc<Vec<u8>>>)>>,
     blockhash: &'a Hash,
     fee_calculator: &'a FeeCalculator,
+    // return data and program_id that set it
+    return_data: Option<(Pubkey, Vec<u8>)>,
 }
 impl<'a> ThisInvokeContext<'a> {
     #[allow(clippy::too_many_arguments)]
@@ -116,6 +118,7 @@ impl<'a> ThisInvokeContext<'a> {
             sysvars: RefCell::new(vec![]),
             blockhash,
             fee_calculator,
+            return_data: None,
         };
         invoke_context.push(program_id, message, instruction, program_indices, accounts)?;
         Ok(invoke_context)
@@ -329,6 +332,12 @@ impl<'a> InvokeContext for ThisInvokeContext<'a> {
     fn get_fee_calculator(&self) -> &FeeCalculator {
         self.fee_calculator
     }
+    fn set_return_data(&mut self, return_data: Option<(Pubkey, Vec<u8>)>) {
+        self.return_data = return_data;
+    }
+    fn get_return_data(&self) -> &Option<(Pubkey, Vec<u8>)> {
+        &self.return_data
+    }
 }
 pub struct ThisLogger {
     log_collector: Option<Rc<LogCollector>>,
diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml
index 21a56867d966f2..e5f6bd9492fbf3 100644
--- a/sdk/Cargo.toml
+++ b/sdk/Cargo.toml
@@ -42,6 +42,7 @@ assert_matches = { version = "1.5.0", optional = true }
 bincode = "1.3.3"
 bytemuck = { version = "1.7.2", features = ["derive"] }
 borsh = "0.9.0"
+base64 = "0.13"
 borsh-derive = "0.9.0"
 bs58 = "0.4.0"
 bv = { version = "0.11.1", features = ["serde"] }
diff --git a/sdk/bpf/c/inc/sol/return_data.h b/sdk/bpf/c/inc/sol/return_data.h
new file mode 100644
index 00000000000000..df1182a1f247b8
--- /dev/null
+++ b/sdk/bpf/c/inc/sol/return_data.h
@@ -0,0 +1,41 @@
+#pragma once
+/**
+ * @brief Solana return data system calls
+**/
+
+#include <sol/types.h>
+#include <sol/pubkey.h>
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+/**
+ * Maximum size of return data
+ */
+#define MAX_RETURN_DATA 1024
+
+/**
+ * Set the return data
+ *
+ * @param bytes byte array to set
+ * @param bytes_len length of byte array. This may not exceed MAX_RETURN_DATA.
+ */
+void sol_set_return_data(const uint8_t *bytes, uint64_t bytes_len);
+
+/**
+ * Get the return data
+ *
+ * @param bytes byte buffer
+ * @param bytes_len maximum length of buffer
+ * @param program_id the program_id which set the return data. Only set if there was some return data (the function returns non-zero).
+ * @param result length of return data (may exceed bytes_len if the return data is longer)
+ */
+uint64_t sol_get_return_data(const uint8_t *bytes, uint64_t bytes_len, SolPubkey *program_id);
+
+#ifdef __cplusplus
+}
+#endif
+
+/**@}*/
diff --git a/sdk/bpf/c/inc/solana_sdk.h b/sdk/bpf/c/inc/solana_sdk.h
index 1f30c6a9a8b6b3..829b66486c541d 100644
--- a/sdk/bpf/c/inc/solana_sdk.h
+++ b/sdk/bpf/c/inc/solana_sdk.h
@@ -12,6 +12,7 @@
 #include <sol/keccak.h>
 #include <sol/log.h>
 #include <sol/pubkey.h>
+#include <sol/return_data.h>
 #include <sol/secp256k1.h>
 #include <sol/sha.h>
 #include <sol/string.h>
diff --git a/sdk/program/src/program.rs b/sdk/program/src/program.rs
index 91c1956c8742d8..bf3bd16548481d 100644
--- a/sdk/program/src/program.rs
+++ b/sdk/program/src/program.rs
@@ -1,4 +1,6 @@
-use crate::{account_info::AccountInfo, entrypoint::ProgramResult, instruction::Instruction};
+use crate::{
+    account_info::AccountInfo, entrypoint::ProgramResult, instruction::Instruction, pubkey::Pubkey,
+};
 
 /// Invoke a cross-program instruction
 ///
@@ -35,6 +37,16 @@ pub fn invoke_signed(
 
     #[cfg(target_arch = "bpf")]
     {
+        extern "C" {
+            fn sol_invoke_signed_rust(
+                instruction_addr: *const u8,
+                account_infos_addr: *const u8,
+                account_infos_len: u64,
+                signers_seeds_addr: *const u8,
+                signers_seeds_len: u64,
+            ) -> u64;
+        }
+
         let result = unsafe {
             sol_invoke_signed_rust(
                 instruction as *const _ as *const u8,
@@ -54,13 +66,48 @@ pub fn invoke_signed(
     crate::program_stubs::sol_invoke_signed(instruction, account_infos, signers_seeds)
 }
 
-#[cfg(target_arch = "bpf")]
-extern "C" {
-    fn sol_invoke_signed_rust(
-        instruction_addr: *const u8,
-        account_infos_addr: *const u8,
-        account_infos_len: u64,
-        signers_seeds_addr: *const u8,
-        signers_seeds_len: u64,
-    ) -> u64;
+/// Maximum size that can be set using sol_set_return_data()
+pub const MAX_RETURN_DATA: usize = 1024;
+
+/// Set a program's return data
+pub fn set_return_data(data: &[u8]) {
+    #[cfg(target_arch = "bpf")]
+    {
+        extern "C" {
+            fn sol_set_return_data(data: *const u8, length: u64);
+        }
+
+        unsafe { sol_set_return_data(data.as_ptr(), data.len() as u64) };
+    }
+
+    #[cfg(not(target_arch = "bpf"))]
+    crate::program_stubs::sol_set_return_data(data)
+}
+
+/// Get the return data from invoked program
+pub fn get_return_data() -> Option<(Pubkey, Vec<u8>)> {
+    #[cfg(target_arch = "bpf")]
+    {
+        use std::cmp::min;
+
+        extern "C" {
+            fn sol_get_return_data(data: *mut u8, length: u64, program_id: *mut Pubkey) -> u64;
+        }
+
+        let mut buf = [0u8; MAX_RETURN_DATA];
+        let mut program_id = Pubkey::default();
+
+        let size =
+            unsafe { sol_get_return_data(buf.as_mut_ptr(), buf.len() as u64, &mut program_id) };
+
+        if size == 0 {
+            None
+        } else {
+            let size = min(size as usize, MAX_RETURN_DATA);
+            Some((program_id, buf[..size as usize].to_vec()))
+        }
+    }
+
+    #[cfg(not(target_arch = "bpf"))]
+    crate::program_stubs::sol_get_return_data()
 }
diff --git a/sdk/program/src/program_stubs.rs b/sdk/program/src/program_stubs.rs
index 21321d868198e1..2a9a85f2fbdd52 100644
--- a/sdk/program/src/program_stubs.rs
+++ b/sdk/program/src/program_stubs.rs
@@ -4,7 +4,7 @@
 
 use crate::{
     account_info::AccountInfo, entrypoint::ProgramResult, instruction::Instruction,
-    program_error::UNSUPPORTED_SYSVAR,
+    program_error::UNSUPPORTED_SYSVAR, pubkey::Pubkey,
 };
 use std::sync::{Arc, RwLock};
 
@@ -80,6 +80,10 @@ pub trait SyscallStubs: Sync + Send {
             *val = c;
         }
     }
+    fn sol_get_return_data(&self) -> Option<(Pubkey, Vec<u8>)> {
+        None
+    }
+    fn sol_set_return_data(&mut self, _data: &[u8]) {}
 }
 
 struct DefaultSyscallStubs {}
@@ -153,3 +157,11 @@ pub(crate) fn sol_memset(s: *mut u8, c: u8, n: usize) {
         SYSCALL_STUBS.read().unwrap().sol_memset(s, c, n);
     }
 }
+
+pub(crate) fn sol_get_return_data() -> Option<(Pubkey, Vec<u8>)> {
+    SYSCALL_STUBS.read().unwrap().sol_get_return_data()
+}
+
+pub(crate) fn sol_set_return_data(data: &[u8]) {
+    SYSCALL_STUBS.write().unwrap().sol_set_return_data(data)
+}
diff --git a/sdk/src/compute_budget.rs b/sdk/src/compute_budget.rs
index f16324f4614b03..b608a86d351dcf 100644
--- a/sdk/src/compute_budget.rs
+++ b/sdk/src/compute_budget.rs
@@ -70,6 +70,8 @@ pub struct ComputeBudget {
     pub sysvar_base_cost: u64,
     /// Number of compute units consumed to call secp256k1_recover
     pub secp256k1_recover_cost: u64,
+    /// Number of compute units consumed to do a syscall without any work
+    pub syscall_base_cost: u64,
     /// Optional program heap region size, if `None` then loader default
     pub heap_size: Option<usize>,
 }
@@ -95,6 +97,7 @@ impl ComputeBudget {
             cpi_bytes_per_unit: 250,        // ~50MB at 200,000 units
             sysvar_base_cost: 100,
             secp256k1_recover_cost: 25_000,
+            syscall_base_cost: 100,
             heap_size: None,
         }
     }
diff --git a/sdk/src/feature_set.rs b/sdk/src/feature_set.rs
index ab0e08227dbb0c..08ed1a287d1f11 100644
--- a/sdk/src/feature_set.rs
+++ b/sdk/src/feature_set.rs
@@ -207,6 +207,10 @@ pub mod check_seed_length {
     solana_sdk::declare_id!("8HYXgkoKGreAMA3MfJkdjbKNVbfZRQP3jqFpa7iqN4v7");
 }
 
+pub mod return_data_syscall_enabled {
+    solana_sdk::declare_id!("BJVXq6NdLC7jCDGjfqJv7M1XHD4Y13VrpDqRF2U7UBcC");
+}
+
 lazy_static! {
     /// Map of feature identifiers to user-visible description
     pub static ref FEATURE_NAMES: HashMap<Pubkey, &'static str> = [
@@ -254,6 +258,7 @@ lazy_static! {
         (ed25519_program_enabled::id(), "enable builtin ed25519 signature verify program"),
         (allow_native_ids::id(), "allow native program ids in program derived addresses"),
         (check_seed_length::id(), "Check program address seed lengths"),
+        (return_data_syscall_enabled::id(), "enable sol_{set,get}_return_data syscall")
         /*************** ADD NEW FEATURES HERE ***************/
     ]
     .iter()
diff --git a/sdk/src/process_instruction.rs b/sdk/src/process_instruction.rs
index 3f2d2055e36e07..d2711e529a4017 100644
--- a/sdk/src/process_instruction.rs
+++ b/sdk/src/process_instruction.rs
@@ -117,6 +117,10 @@ pub trait InvokeContext {
     fn get_blockhash(&self) -> &Hash;
     /// Get this invocation's `FeeCalculator`
     fn get_fee_calculator(&self) -> &FeeCalculator;
+    /// Set the return data
+    fn set_return_data(&mut self, return_data: Option<(Pubkey, Vec<u8>)>);
+    /// Get the return data
+    fn get_return_data(&self) -> &Option<(Pubkey, Vec<u8>)>;
 }
 
 /// Convenience macro to log a message with an `Rc<RefCell<dyn Logger>>`
@@ -197,6 +201,8 @@ pub struct BpfComputeBudget {
     pub sysvar_base_cost: u64,
     /// Number of compute units consumed to call secp256k1_recover
     pub secp256k1_recover_cost: u64,
+    /// Number of compute units consumed to do a syscall without any work
+    pub syscall_base_cost: u64,
     /// Optional program heap region size, if `None` then loader default
     pub heap_size: Option<usize>,
 }
@@ -217,6 +223,7 @@ impl From<ComputeBudget> for BpfComputeBudget {
             max_cpi_instruction_size: item.max_cpi_instruction_size,
             cpi_bytes_per_unit: item.cpi_bytes_per_unit,
             sysvar_base_cost: item.sysvar_base_cost,
+            syscall_base_cost: item.syscall_base_cost,
             secp256k1_recover_cost: item.secp256k1_recover_cost,
             heap_size: item.heap_size,
         }
@@ -240,6 +247,7 @@ impl From<BpfComputeBudget> for ComputeBudget {
             cpi_bytes_per_unit: item.cpi_bytes_per_unit,
             sysvar_base_cost: item.sysvar_base_cost,
             secp256k1_recover_cost: item.secp256k1_recover_cost,
+            syscall_base_cost: item.syscall_base_cost,
             heap_size: item.heap_size,
         }
     }
@@ -313,6 +321,25 @@ pub mod stable_log {
         ic_logger_msg!(logger, "Program log: {}", message);
     }
 
+    /// Log return data as from the program itself. This line will not be present if no return
+    /// data was set, or if the return data was set to zero length.
+    ///
+    /// The general form is:
+    ///
+    /// ```notrust
+    /// "Program return data: <program-id> <program-generated-data-in-base64>"
+    /// ```
+    ///
+    /// That is, any program-generated output is guaranteed to be prefixed by "Program return data: "
+    pub fn program_return_data(logger: &Rc<RefCell<dyn Logger>>, program_id: &Pubkey, data: &[u8]) {
+        ic_logger_msg!(
+            logger,
+            "Program return data: {} {}",
+            program_id,
+            base64::encode(data)
+        );
+    }
+
     /// Log successful program execution.
     ///
     /// The general form is:
@@ -397,7 +424,9 @@ pub struct MockInvokeContext<'a> {
     pub disabled_features: HashSet<Pubkey>,
     pub blockhash: Hash,
     pub fee_calculator: FeeCalculator,
+    pub return_data: Option<(Pubkey, Vec<u8>)>,
 }
+
 impl<'a> MockInvokeContext<'a> {
     pub fn new(keyed_accounts: Vec<KeyedAccount<'a>>) -> Self {
         let compute_budget = ComputeBudget::default();
@@ -415,6 +444,7 @@ impl<'a> MockInvokeContext<'a> {
             disabled_features: HashSet::default(),
             blockhash: Hash::default(),
             fee_calculator: FeeCalculator::default(),
+            return_data: None,
         };
         invoke_context
             .invoke_stack
@@ -543,4 +573,10 @@ impl<'a> InvokeContext for MockInvokeContext<'a> {
     fn get_fee_calculator(&self) -> &FeeCalculator {
         &self.fee_calculator
     }
+    fn set_return_data(&mut self, return_data: Option<(Pubkey, Vec<u8>)>) {
+        self.return_data = return_data;
+    }
+    fn get_return_data(&self) -> &Option<(Pubkey, Vec<u8>)> {
+        &self.return_data
+    }
 }